package org.postgresforest.tool.lib;

import java.sql.*;
import java.util.*;
import org.postgresforest.tool.lib.Table;
import org.postgresforest.tool.lib.ForestToolException;
import org.postgresforest.tool.lib.SqlTokenizer;
import org.postgresforest.tool.ArrayUtil;
import org.postgresforest.tool.Logger;

public class Database {
	private GSCdata gsc = null;
	private int[] instanceIds = null;
	private String dbName = null;

	/**
	 * Databaseクラスコンストラクタ
	 *
	 * @param gsc GSCdataオブジェクト
	 * @param instanceIds このデータベースを保持しているインスタンスのインスタンスIDの配列
	 * @param dbName データベース名
	 */
	public Database(GSCdata gsc, int[] instanceIds, String dbName)
	{
		this.gsc = gsc;
		this.instanceIds = instanceIds;
		this.dbName = dbName;
	}

	/**
	 * データベース名を取得する
	 *
	 * @return String データベース名
	 */
	public String getDatabaseName()
	{
		return dbName;
	}

	/**
	 * 複数のインスタンスにデータベースが配置されている場合に、
	 * そのデータベース番号（dbno）を配列で取得する。
	 *
	 * @return int[] データベース番号（dbno）の配列
	 */
	public int[] getDatabaseNumbers()
	{
		ResultSet rs = null;
		ArrayList a = new ArrayList();

		try {
			rs = gsc.executeQueryGSC("SELECT dbno FROM forest_servdb WHERE dbname='" + dbName + "'");

			while ( rs.next() )
				a.add( new Integer(rs.getInt("dbno")) );
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
		}

		return ArrayUtil.array2intarray(a);
	}

	/**
	 * 渡されたDDL文を使って多重化テーブルを作成する。
	 * 実テーブルを作った後、GSCへの登録を行う。
	 *
	 * @param tableDef 作成するテーブル定義（DDL文）
	 *
	 * @return Table 作成されたテーブルを示すTableオブジェクト
	 */
	public Table createTable(String tableDef)
		throws ForestToolException
	{
		Instance ins = null;
		Connection con = null;

		String tableName = Database.parseTableName(tableDef);

		for (int i=0 ; i<instanceIds.length ; i++)
		{
			ins = gsc.getInstance(instanceIds[i]);
			con = ins.getDatabaseConnection(dbName, gsc.getUser(), gsc.getPassword());

			try {
				Statement stmt = con.createStatement();

				int rc = stmt.executeUpdate(tableDef);

				stmt.close();
				con.close();
			}
			catch (Exception e)
			{
				throw new ForestToolException(e);
			}
		}

		String sql = "INSERT INTO forest_tablepart(dbname,table_name,part_count,part_type,hash_name,status) " +
			" VALUES ('" + dbName + "','" + tableName + "', 1, 0,null,0);";

		int[] dbno = getDatabaseNumbers();
		for (int i=0 ; i<dbno.length ; i++)
		{
			sql = sql + "INSERT INTO forest_tablepartdtl(dbno,dbname,table_name,part_no,priority) VALUES (" + 
			            "" + dbno[i] + "," +
			            "'" + dbName + "'," +
			            "'" + tableName + "'," +
			            "0," +
			            "0);";
		}

		gsc.executeUpdateGSC(sql);

		return getTable(tableName);
	}


	private boolean _dropView(int[] instanceIds, String viewName)
	{
		return _dropObject(instanceIds, viewName, "VIEW");
	}

	private boolean _dropTable(int[] instanceIds, String tableName)
	{
		return _dropObject(instanceIds, tableName, "TABLE");
	}

	/*
	 * 指定したすべてのインスタンスから指定したオブジェクトを削除する
	 * （物理オブジェクト名指定）
	 */
	private boolean _dropObject(int[] instanceIds, String tableName, String obj)
	{
		for (int i=0 ; i<instanceIds.length ; i++)
		{
			Instance ins = gsc.getInstance(instanceIds[i]);

			try {
				Connection con = ins.getDatabaseConnection(getDatabaseName(),
														   gsc.getUser(),
														   gsc.getPassword());

				con.createStatement().executeUpdate("DROP " + obj + " " + tableName);
				
				con.close();
			}
			catch (Exception e)
			{
				Logger.error(e.getMessage());
				return false;
			}
		}

		return true;
	}

	/**
	 * データベースからテーブルを削除し、GSCからも削除する。
	 * パーティションテーブル対応。
	 *
	 * @param table 削除するテーブルのTableオブジェクト
	 *
	 * @return boolean 削除に成功したらtrue、失敗したらfalse
	 */
	public boolean dropTable(Table table)
	{
		return dropTable(table.getName());
	}

	/**
	 * データベースからテーブルを削除し、GSCからも削除する。
	 * パーティションテーブル対応。
	 *
	 * @param tableName 削除するテーブル名
	 *
	 * @return boolean 削除に成功したらtrue、失敗したらfalse
	 */
	public boolean dropTable(String tableName)
	{
		Table t = getTable(tableName);
		boolean rc = false;

		try {
			if ( t.getTableType()==Table.TYPE_REPLICATED )
			{
				/*
				 * 多重化テーブル削除
				 */
				int[] instanceIds = t.getInstanceIds(0);
				
				rc = _dropTable(instanceIds, tableName);
			}
			else
			{
				/*
				 * UNION ALL VIEWを削除（RULEはカスケード削除）
				 */
				int[] instanceIds = t.getInstanceIds();
				rc = _dropView(instanceIds, tableName);
				
				/*
				 * パーティション削除
				 */
				for (int part=0 ; part<t.getPartCount() ; part++)
				{
					instanceIds = t.getInstanceIds(part);
					
					String partName = PartitionUtils.buildPartitionName(tableName, part);
					
					rc = _dropTable(instanceIds, partName);
				}
			}
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			rc = false;
		}

		if ( !rc )
			return false;

		/*
		 * GSCから削除（tablepart, tablepartdtl, partatr）
		 */
		rc = false;

		try {
			String sql = "";

			if ( t.getTableType()!=Table.TYPE_REPLICATED )
				sql = sql + "DELETE FROM forest_partatr WHERE dbname='" + dbName + "' AND table_name='" + tableName + "';";
			sql = sql + "DELETE FROM forest_tablepartdtl WHERE dbname='" + dbName + "' AND table_name='" + tableName + "';";
			sql = sql + "DELETE FROM forest_tablepart WHERE dbname='" + dbName + "' AND table_name='" + tableName + "';";

			if ( gsc.executeUpdateGSC(sql) > 0 )
				rc = true;
		}
		catch (Exception e)
		{			
			Logger.error(e.getMessage());
		}

		return rc;
	}


	/**
	 * このデータベースを保持しているインスタンスの一覧を取得する
	 *
	 * @return int[] インスタンスIDのint配列
	 */
	public int[] getInstanceIds()
	{
		return instanceIds;
	}

	/**
	 * データベース内にあるテーブル名の一覧を取得する
	 *
	 * @return String[] テーブル名のString配列
	 */
	public String[] getTableNames()
		throws ForestToolException
	{
		String sql = "SELECT table_name FROM forest_tablepart WHERE dbname='" + dbName + "' ORDER BY table_name";
		ArrayList a = new ArrayList();

		try {
			ResultSet rs = gsc.executeQueryGSC(sql);

			while ( rs.next() )
			{
				a.add(rs.getString("table_name"));
			}

			rs.close();
		}
		catch (Exception e)
		{
			throw new ForestToolException(e);
		}

		return ArrayUtil.array2stringarray(a);
	}


	/**
	 * 指定した名前のテーブルのTableオブジェクトを取得する
	 *
	 * @param tableName テーブル名
	 *
	 * @return Table Tableオブジェクト
	 */
	public Table getTable(String tableName)
	{
		return new Table(this, tableName);
	}

	/**
	 * テーブル定義用DDLから、テーブル名を取得する。
	 * スキーマ修飾がある場合には、'.' で連結した形で取り出す。
	 *
	 * @param tableDef テーブル定義DDL文
	 *
	 * @return String テーブル名（スキーマ修飾付き）
	 */
    public static String parseTableName(String tableDef)
    {
        SqlTokenizer t = new SqlTokenizer(tableDef);
        String tableName = null;

        while ( t.hasMoreToken() )
		{
			String token = t.nextToken();
			//			System.out.println("[token]" + token);
			//			System.out.println("[ttype]" + t.ttype);

			if ( token.equals("(") )
				break;

			if ( t.ttype == SqlTokenizer.TT_WORD )
				tableName = token;
		}

		tableName = tableName.replaceAll("\"", "");
		tableName = tableName.replaceAll("public.", "");

        return tableName;
    }

	/**
	 * テーブル作成用のDDL文から、カラム名の一覧を取得する
	 *
	 * @param tableDef テーブル定義用DDL文字列
	 *
	 * @return String[] カラム名を保持したString配列
	 */
    public static String[] parseColumnNames(String tableDef)
    {
        SqlTokenizer t = new SqlTokenizer(tableDef);
		String[] columnNames = null;
		ArrayList a = new ArrayList();

		/*
		 * カッコの開始まで読み飛ばす
		 */
        while ( t.hasMoreToken() )
        {
            if ( t.nextToken().equals("(") )
                break;
        }

		/*
		 * 最初のtokenがカラム名
		 */
        boolean comming = true;
		int nesting = 1;

		while ( t.hasMoreToken() )
        {
			String token = t.nextToken();
			//			System.out.println("[token]" + token);

			/*
			 * カンマの次に来るtokenがカラム名
			 */
            if ( token.equals(",") )
				comming = true;

			if ( comming==true && t.ttype==SqlTokenizer.TT_WORD )
			{
				//				System.out.println("[column]" + token);
				a.add(token);

				comming = false;
			}

            if ( token.equals("(") )
				nesting++;
            if ( token.equals(")") )
				nesting--;

			if ( nesting==0 )
				break;
        }

		columnNames = new String[a.size()];

		for (int i=0 ; i<columnNames.length ; i++)
		{
			columnNames[i] = (String)a.get(i);
		}

        return columnNames;
    }

	/**
	 * GSCdataオブジェクトを取得する。
	 * TableオブジェクトなどからGSCdataを辿る場合に使用する。
	 *
	 * @return GSCdata GSCdataオブジェクト
	 */
	protected GSCdata getGSCdata()
	{
		return gsc;
	}

	public boolean validate(int flags)
	{
		Logger.debug("Validating database " + getDatabaseName() + "....");

		boolean rc = true;

		/*
		 * 全インスタンスにデータベースが存在するかどうかを確認
		 */
		int[] ids = getInstanceIds();
		for (int i=0 ; i<ids.length ; i++)
		{
			Instance instance = gsc.getInstance(ids[i]);
			try {
				Connection con = instance.getDatabaseConnection(getDatabaseName(),
																gsc.getUser(),
																gsc.getPassword());
				con.close();
			}
			catch (Exception e)
			{
				Logger.error(instance.toString() + ": " + e.getMessage());
				Logger.trace(e);

				rc = false;
			}
		}

		try {
			String[] tableNames = this.getTableNames();

			for (int j=0 ; j<tableNames.length ; j++)
			{
				Table t = this.getTable( tableNames[j] );
				rc = t.validate(flags) && rc;
			}
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			Logger.trace(e);
			return false;
		}

		return rc;
	}

	/**
	 * 既存のデータベースにインスタンスを追加する。
	 * 多重化テーブルのみ対応。
	 * GSC情報のみを登録する。
	 * データベースの内容はユーザがdump/restoreでコピーする必要がある。
	 *
	 * @param instance 追加するインスタンスのInstnaceオブジェクト
	 * @param force インスタンスにある既存のデータを上書きする
	 *
	 * @return boolean 追加登録に成功したらtrue、失敗したらfalse
	 */
	public boolean addInstance(Instance instance, boolean force)
	{
		int dbno = -1;
		ArrayList a = new ArrayList();

		try {
			ResultSet rs = null;

			rs = gsc.executeQueryGSC("SELECT max(dbno) FROM forest_servdb WHERE dbname='" + getDatabaseName() + "'");
			if ( rs.next() )
				dbno = rs.getInt(1);
			rs.close();

			if ( dbno<0 )
			{
				Logger.error("Cannot register " + instance.toString() + " as a new instance.");
				return false;
			}

			String sql = "SELECT table_name FROM forest_tablepart " +
			             " WHERE dbname='" + getDatabaseName() + "' " +
			             "   AND part_type=" + Table.TYPE_REPLICATED;

			rs = gsc.executeQueryGSC(sql);
			while ( rs.next() )
			{
				a.add( rs.getString(1) );
			}
			rs.close();

			String newSql = "INSERT INTO forest_servdb (dbno,dbname,serverid) VALUES (" + (dbno+1) + ",'" + getDatabaseName() + "'," + instance.getId() + ");\n";
			for (int i=0 ; i<a.size() ; i++)
			{
				String t = (String)a.get(i);
				newSql = newSql + "INSERT INTO forest_tablepartdtl (dbno,dbname,table_name,part_no,priority) VALUES (" + (dbno+1) + ",'" + getDatabaseName() + "','" + t + "',0,0);\n";
			}

			Logger.debug(newSql);

			int rc = gsc.executeUpdateGSC(newSql);
			if ( rc==0 )
			{
				Logger.error("GSC has not been updated while adding an instance.");
				return false;
			}
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			Logger.trace(e);
			return false;
		}
		return true;
	}
}
