package jp.agentec.abook.abv.bl.data.dao;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import jp.agentec.abook.abv.bl.common.db.Cursor;
import jp.agentec.abook.abv.bl.common.db.SQLiteDatabase;
import jp.agentec.abook.abv.bl.common.db.SQLiteStatement;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.data.DBConnector;
import jp.agentec.abook.abv.bl.dto.AbstractDto;
import jp.agentec.adf.util.DateTimeFormat;
import jp.agentec.adf.util.DateTimeUtil;

public class AbstractDao {
	private static final String TAG = "AbstractDao";
	@SuppressWarnings("rawtypes")
	private static ConcurrentHashMap<Class, AbstractDao> daoMap = new ConcurrentHashMap<Class, AbstractDao>();
	
	@SuppressWarnings("unchecked")
	public static <T extends AbstractDao> T getDao(Class<T> clazz) {
		T instance;
		if (daoMap.containsKey(clazz)) {
			instance = (T) daoMap.get(clazz);
		}
		else {
//			synchronized (daoMap) { // パフォーマンスを考慮し、排他制御をしない。最悪２重に作られても各Daoは状態を持たないため影響はなく、参照がなくなったDaoはgc対象となる。
//				if (daoMap.containsKey(clazz)) {
//					instance = (T) daoMap.get(clazz);
//				}
//				else {
					try {
                        //noinspection ClassNewInstance
						instance = clazz.newInstance();
						daoMap.put(clazz, instance);
					} catch (InstantiationException e) {
						throw new RuntimeException(e);
					} catch (IllegalAccessException e) {
						throw new RuntimeException(e);
					}
//				}
//			}
		}
		return instance;
	}
	
	protected DBConnector dbConn = DBConnector.getInstance();

	public SQLiteDatabase getDatabase() {
		return dbConn.getDatabase();
	}
	
	/**
	 * トランザクションを開始します。
	 */
	public void beginTransaction() {
		dbConn.getDatabase().beginTransaction();
	}
	
	/**
	 * トランザクションをコミットします。
	 */
	public void commit() {
		SQLiteDatabase db = dbConn.getDatabase();
		if (db.inTransaction()) {
			db.setTransactionSuccessful();
			db.endTransaction();
		}
	}
	
	/**
	 * トランザクションをロールバッグします。
	 */
	public void rollback() {
		SQLiteDatabase db = dbConn.getDatabase();
		if (db.inTransaction()) {
			db.endTransaction();
		}
	}

	/**
	 * 指定しテーブルから指定した条件のデータを削除します。<br>
	 * @param table 削除するテーブル名です。
	 * @param whereClause 削除する条件（where）のsqlです。prestatementを使う場合、パラメータは?で表記します。nullの指定も可能です。
	 * @param whereArgs whereClauseでprestatementを使う場合、その値の配列です。nullの指定も可能です。
	 * @return 削除された行の数を返します。
	 * @throws Exception データベース接続時に例外が発生しました。
	 * @since 1.0.0
	 */
	protected long delete(String table, String whereClause, String[] whereArgs) {
		SQLiteDatabase db = dbConn.getDatabase();
		if (Logger.isVerboseEnabled()) {
			Logger.v(TAG, "table=%s where=%s [%s]", table, whereClause, join(whereArgs));
		}
		return db.delete(table, whereClause, whereArgs);
	}


	/**
	 * 指定したsqlを実行します。prestatementはサポートしません。<br>
	 * 
	 * ★重要： bind()メソッドを使用しないので、Dateを用いる場合は自前でUTCに変換すること
	 * 
	 * @param sql 実行するsql文です。prestatementはサポートしません。
	 * @throws Exception sql実行が失敗しました。
	 */
	protected void execSql(String sql) {
		SQLiteDatabase db = dbConn.getDatabase();
		Logger.v(TAG, sql);
		db.execSQL(sql);
	}
	
	/**
	 * 指定したsqlを実行します。prestatementをサポートします。<br>
	 * 
	 * ★重要： bind()メソッドを使用しないので、Dateを用いる場合は自前でUTCに変換すること
	 * 
	 * @param sql 実行するsql文です。prestatementを使う場合、パラメータは?で表記します。
	 * @param bindArgs sqlでprestatementを使う場合、その値の配列です。byte[], String, Long, Doubleのみサポートします。
	 * @throws Exception sql実行が失敗しました。
	 */
	protected void execSql(String sql, Object[] bindArgs) {
		SQLiteDatabase db = dbConn.getDatabase();
		if (Logger.isVerboseEnabled()) {
			Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
		}
		db.execSQL(sql, bindArgs);
	}

	protected String join(Object[] bindArgs) {
		if (bindArgs == null) {
			return "";
		}
		StringBuffer sb = new StringBuffer();
		for (Object object : bindArgs) {
			sb.append(object);
			sb.append(",");
		}
		return sb.toString();
	}

	/**
	 * insert文を実行します。DTOのリストを順次インサートします。
	 * 
	 * @param sql
	 * @param list
	 * @throws Exception
	 */
	protected void insert(String sql, List<? extends AbstractDto> list) {
		SQLiteDatabase db = dbConn.getDatabase();
		SQLiteStatement stmt = null;
		try {
			stmt = db.compileStatement(sql);
			for (AbstractDto dto : list) {
				if (Logger.isVerboseEnabled()) {
					Logger.v(TAG, "%s [%s]", sql, join(dto.getInsertValues()));
				}
				bind(stmt, dto.getInsertValues());
				stmt.execute();
			}
		} finally {
			if (stmt != null) {
				stmt.close();
			}
		}
	}

	/**
	 * インサート文を実行します。
	 * 基本的にexecSQLと同じですが、呼び出し方が若干異なります。
	 * 
	 * @param sql
	 * @param params
	 * @throws Exception
	 */
	protected void insert(String sql, Object[] params) {
		SQLiteDatabase db = dbConn.getDatabase();
		SQLiteStatement stmt = null;
		try {
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(params));
			}
			stmt = db.compileStatement(sql);
			bind(stmt, params);
			stmt.execute();
		} finally {
			if (stmt != null) {
				stmt.close();
			}
		}
	}

	/**
	 * update文を実行します。
	 * 基本的にexecSQLと同じですが、呼び出し方が若干異なります。
	 * 更新された行数を返します。
	 * 
	 * @param sql
	 * @param params
	 * @return
	 * @throws Exception
	 */
	protected long update(String sql, Object[] params) {
		SQLiteDatabase db = dbConn.getDatabase();
		SQLiteStatement stmt = null;
		SQLiteStatement stmt2 = null;
		try {
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(params));
			}
			stmt = db.compileStatement(sql);
			bind(stmt, params);
			stmt.execute();
			
			stmt2 = db.compileStatement("select changes()");
			long val = stmt2.simpleQueryForLong();
			return val;
		} finally {
			if (stmt != null) {
				stmt.close();
			}
			if (stmt2 != null) {
				stmt2.close();
			}
		}
	}

	/**
	 * クエリを実行し、最初の行の最初の列の値をint型で返します。
	 * 
	 * stmt.simpleQueryForLong()と同じ
	 * 
	 * @param sql
	 * @param bindArgs
	 * @return
	 * @throws Exception
	 */
	public int rawQueryGetInt(String sql, String[] bindArgs) {
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			if (cursor.moveToNext()) {
				return cursor.getInt(0);
			}
			return 0;	
		}
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}

	/**
	 * クエリを実行し、最初の行の最初の列の値をlong型で返します。
	 *
	 * stmt.simpleQueryForLong()と同じ
	 *
	 * @param sql
	 * @param bindArgs
	 * @return
	 * @throws Exception
	 */
	public long rawQueryGetLong(String sql, String[] bindArgs) {
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			if (cursor.moveToNext()) {
				return cursor.getLong(0);
			}
			return 0;
		}
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}

	/**
	 * クエリを実行し、最初の行の最初の列の値をInteger型リストで返します。
	 *
	 * @param sql
	 * @param bindArgs
	 * @return
	 */
	public List<Integer> rawQueryGetIntegerList(String sql, String[] bindArgs) {
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			List<Integer> list = new ArrayList<Integer>();
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			while (cursor.moveToNext()) {
				list.add(cursor.getInt(0));
			}
			return list;
		}
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}
	
	/**
	 * クエリを実行し、最初の行の最初の列の値をLong型リストで返します。
	 * 
	 * @param sql
	 * @param bindArgs
	 * @return
	 */
	public List<Long> rawQueryGetLongList(String sql, String[] bindArgs) {
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			List<Long> list = new ArrayList<Long>();
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			while (cursor.moveToNext()) {
				list.add(cursor.getLong(0));
			}
			return list;
		}
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}
	
	/**
	 * クエリを実行し、最初の行の最初の列の値をString型で返します。
	 * 
	 * stmt.simpleQueryForString()と同じ
	 * 
	 * @param sql
	 * @param bindArgs
	 * @return
	 * @throws Exception
	 */
	public String rawQueryGetString(String sql, String[] bindArgs) { 
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			if (cursor.moveToNext()) {
				return cursor.getString(0);
			}
			return null;	
		}
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}
	
	/**
	 * クエリを実行し、DTOのリストとして返却します。
	 * cursorからDTOへの変換は、convertメソッドを使用するため、当該Daoはconvert()メソッドを実装する必要があります。
	 * 
	 * @param sql
	 * @param bindArgs
	 * @param dtoType
	 * @return
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	protected <Dto extends AbstractDto> List<Dto> rawQueryGetDtoList(String sql, String[] bindArgs, Class<Dto> dtoType) {
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			List<Dto> list = new ArrayList<Dto>();
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			while (cursor.moveToNext()) {
				list.add((Dto)convert(cursor));
			}
			return list;
		} 
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}

	/**
	 * クエリを実行し、DTOとして返却します。
	 * cursorからDTOへの変換は、convertメソッドを使用するため、当該Daoはconvert()メソッドを実装する必要があります。
	 * 
	 * @param sql
	 * @param bindArgs
	 * @param dtoType
	 * @return
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	protected <Dto extends AbstractDto> Dto rawQueryGetDto(String sql, String[] bindArgs, Class<Dto> dtoType) {
		SQLiteDatabase db = dbConn.getDatabase();
		Cursor cursor = null;
		try {
			if (Logger.isVerboseEnabled()) {
				Logger.v(TAG, "%s [%s]", sql, join(bindArgs));
			}
			cursor = db.rawQuery(sql, bindArgs);
			if (cursor.moveToNext()) {
				return (Dto)convert(cursor);
			}
			return null;
		} 
		finally {
			if (cursor != null) {
				cursor.close();
			}
		}
	}

	/**
	 * SQLiteにおけるBoolの表現は、int型で0がfalse, それ以外がtrueのため、
	 * その判定を行います。
	 * 
	 * @param val
	 * @return
	 */
	protected boolean toBool(int val) {
		return val != 0;
	}
	
	/**
	 * ステートメントにパラメータ値をバインドします。
	 * パラメータの型に応じて自動的にsetterメソッドを選択します。
	 * 
	 * @param stmt
	 * @param params
	 */
	protected void bind(SQLiteStatement stmt, Object... params) {
		if (params == null || params.length == 0) {
			return;
		}
		for (int i=1; i <= params.length; i++) {
			Object param = params[i-1];
			if (param == null) {
				stmt.bindNull(i);
			}
			else if (param instanceof String) {
				stmt.bindString(i, (String)param);
			}
			else if (param instanceof Integer) {
				stmt.bindLong(i, (Integer)param);
			}
			else if (param instanceof Boolean) {
				stmt.bindLong(i, (Boolean)param?1:0);
			}
			else if (param instanceof java.util.Date) {
				stmt.bindString(i, DateTimeUtil.toStringInTimeZone(DateTimeUtil.dateToSqlDate((java.util.Date)param), DateTimeFormat.yyyyMMddHHmmssSSS_hyphen, "UTC"));
			}
			else //noinspection ConstantConditions
                if (param instanceof java.sql.Date) {
				stmt.bindString(i, DateTimeUtil.toStringInTimeZone((java.sql.Date)param, DateTimeFormat.yyyyMMddHHmmssSSS_hyphen, "UTC"));
			}
			else if (param instanceof byte[]) {
				stmt.bindBlob(i, (byte[])param);
			}
			else if (param instanceof Long) {
				stmt.bindLong(i, (Long)param);
			}
			else if (param instanceof Short) {
				stmt.bindLong(i, (Short)param);
			}
			else if (param instanceof Byte) {
				stmt.bindLong(i, (Byte)param);
			}
			else if (param instanceof Float) {
				stmt.bindDouble(i, (Float)param);
			}
			else if (param instanceof Double) {
				stmt.bindDouble(i, (Double)param);
			}
			else {
				stmt.bindString(i, "" + param);
			}
		}
	}
	
	/**
	 * カーソルからDtoへのコンバートです。
	 * 各Daoで実装します。
	 * 
	 * @param cursor
	 * @return
	 */
	protected AbstractDto convert(Cursor cursor) {
		return null;
	}
	
	protected String generateInClause(List<Long> ids) {
		StringBuffer sbWhereCluase = new StringBuffer();
		int count = ids.size();
		sbWhereCluase.append("(");
		
		if (!ids.isEmpty()) {
			for (int i = 0; i < count; i++) {
				sbWhereCluase.append(ids.get(i));
				if (i < count -1) {
					sbWhereCluase.append(", ");
				}
			}
		}
		
		sbWhereCluase.append(")");
		return sbWhereCluase.toString();
	}
	
	protected String generateInClause(int[] ids) {
		StringBuffer sbWhereCluase = new StringBuffer();
		int count = ids.length;
		
		sbWhereCluase.append("(");
		for (int i = 0; i < count; i++) {
			sbWhereCluase.append(ids[i]);
			
			if (i < count -1) {
				sbWhereCluase.append(", ");
			}
		}
		sbWhereCluase.append(")");
		
		return sbWhereCluase.toString();
	}

	/**
	 * vaccum処理を行う
	 */
	public void vacuum() {
		Logger.i(TAG, "vacuum start.");
		dbConn.getDatabase().execSQL("vacuum");
		Logger.i(TAG, "vacuum end.");
	}

}
