package com.kurukurupapa.tryandroid.fw.util;

import java.lang.reflect.Field;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;

import com.kurukurupapa.tryandroid.fw.FwException;
import com.kurukurupapa.tryandroid.fw.annotation.Exclude;
import com.kurukurupapa.tryandroid.fw.annotation.GeneratedValue;

import android.content.ContentValues;
import android.database.Cursor;

public class DbUtil {
	// キャメル文字列からアンダースコア区切り文字列へ変換処理のキャッシュ
	private static Hashtable<String, String> toUnderScoreMap = new Hashtable<String, String>();

	/**
	 * キャメル文字列をアンダースコア区切り文字列へ変換
	 *
	 * キャッシュ機能付き
	 *
	 * @param camelCaseString
	 * @return
	 */
	private static String toUnderScore(String camelCaseString) {
		String toUnderScore = toUnderScoreMap.get(camelCaseString);
		if (toUnderScore == null) {
			toUnderScore = StringUtil.toUnderScore(camelCaseString);
			toUnderScoreMap.put(camelCaseString, toUnderScore);
		}
		return toUnderScore;
	}

	/**
	 * DBカラムに紐付けられるフィールドリストを取得
	 *
	 * @param <T>
	 * @param entityClass
	 * @return
	 */
	public static <T> List<Field> getColumnFieldList(Class<T> entityClass) {
		List<Field> list = new ArrayList<Field>();
		for (Field field : entityClass.getDeclaredFields()) {
			if (isExclude(field)) {
				continue;
			}
			list.add(field);
		}
		return list;
	}

	/**
	 * 複数のフィールドに対応するカラム名を取得
	 *
	 * @param <T>
	 * @param entityClass
	 * @return
	 */
	public static <T> String[] getColumnNameArray(Class<T> entityClass) {
		List<String> list = new ArrayList<String>();
		for (Field field : entityClass.getDeclaredFields()) {
			if (isExclude(field)) {
				continue;
			}
			list.add(getColumnName(field));
		}
		return list.toArray(new String[] {});
	}

	/**
	 * 複数のフィールドに対応するカラム名を取得
	 *
	 * @param <T>
	 * @param entityClass
	 * @return
	 */
	public static <T> String[] getColumnNameArray(List<Field> fieldList) {
		List<String> list = new ArrayList<String>();
		for (Field field : fieldList) {
			list.add(getColumnName(field));
		}
		return list.toArray(new String[] {});
	}

	public static <T> String getColumnNamesForJoin(Class<T> entityClass) {
		StringBuilder sb = new StringBuilder();
		String tableName = getTableName(entityClass);
		for (Field field : entityClass.getDeclaredFields()) {
			if (isExclude(field)) {
				continue;
			}
			if (sb.length() > 0) {
				sb.append(",");
			}
			String columnName = getColumnName(field);
			sb.append(tableName + "." + columnName + " as " + columnName);
		}
		return sb.toString();
	}

	/**
	 * フィールドに対応するカラム名を取得
	 *
	 * @param field
	 * @return
	 */
	private static String getColumnName(Field field) {
		return toUnderScore(field.getName());
	}

	/**
	 * フィールドに対応するカラムの型を取得
	 *
	 * @param field
	 * @return
	 */
	private static String getColumnType(Field field) {
		String type = null;
		if (field.getType() == int.class || field.getType() == long.class) {
			type = "integer";
		} else if (field.getType() == float.class
				|| field.getType() == double.class) {
			type = "real";
		} else if (field.getType() == String.class) {
			type = "text";
		} else if (field.getType() == Date.class) {
			type = "text";
			// Date型とString型の変換でパフォーマンス劣化の可能性がある
			LogUtil.w("Performance attention.");
		} else {
			throw new FwException(
					"The type of the field doesn't correspond. Type="
							+ field.getType());
		}
		return type;
	}

	/**
	 * 主キー判定
	 *
	 * @param field
	 * @return 主キーの場合、trueを返却。
	 */
	private static boolean isId(Field field) {
		return field.getName().equals("id");
	}

	/**
	 * 自動インクリメント判定
	 *
	 * @param field
	 * @return 自動インクリメント項目の場合、trueを返却。
	 */
	private static boolean isGeneratedValue(Field field) {
		return field.getAnnotation(GeneratedValue.class) != null;
	}

	/**
	 * 対象外判定
	 *
	 * @param field
	 * @return 対象外項目の場合、trueを返却。
	 */
	private static boolean isExclude(Field field) {
		return field.getAnnotation(Exclude.class) != null;
	}

	/**
	 * カーソルからフィールドに対応する値を取得
	 *
	 * @param cursor
	 * @param index
	 * @param field
	 * @return
	 */
	private static Object getColumnValue(Cursor cursor, int index, Field field) {
		// カラムの値を取得する。
		Object value = null;
		try {
			if (field.getType() == boolean.class) {
				value = cursor.getInt(index) != 0;
			} else if (field.getType() == int.class) {
				value = cursor.getInt(index);
			} else if (field.getType() == long.class) {
				value = cursor.getLong(index);
			} else if (field.getType() == float.class) {
				value = cursor.getFloat(index);
			} else if (field.getType() == double.class) {
				value = cursor.getDouble(index);
			} else if (field.getType() == String.class) {
				value = cursor.getString(index);
			} else if (field.getType() == Date.class) {
				value = DateUtil.parseDateTime(cursor.getString(index));
			} else {
				throw new FwException(
						"The type of the field doesn't correspond. Type="
								+ field.getType());
			}
		} catch (ParseException e) {
			throw new FwException("Field=" + field.getName() + ", Cursor="
					+ ReflectionUtil.toString(cursor), e);
		}

		return value;
	}

	/**
	 * カーソルからフィールドに対応する値を取得
	 *
	 * @param cursor
	 * @param field
	 * @return
	 */
	private static Object getColumnValue(Cursor cursor, Field field) {
		// 対象カラムのインデックスを取得する。
		String columnName = getColumnName(field);
		int index = cursor.getColumnIndex(columnName);
		if (index < 0) {
			throw new FwException(
					"The column corresponding to the field was not found. Field="
							+ field.getName() + ", Column=" + columnName);
		}

		// カラムの値を取得する。
		return getColumnValue(cursor, index, field);
	}

	/**
	 * フィールドの値をDB用に変換
	 *
	 * @param field
	 * @param obj
	 * @return
	 */
	private static String convertToColumnValue(Field field, Object obj) {
		String result = null;

		if (field.getType() == boolean.class) {
			Boolean flag = (Boolean) ReflectionUtil.getFieldValue(obj, field);
			result = String.valueOf(flag ? 1 : 0);
		} else if (field.getType() == int.class
				|| field.getType() == long.class
				|| field.getType() == float.class
				|| field.getType() == double.class
				|| field.getType() == String.class) {
			result = String.valueOf(ReflectionUtil.getFieldValue(obj, field));
		} else if (field.getType() == Date.class) {
			result = DateUtil.formatDateTime((Date) ReflectionUtil
					.getFieldValue(obj, field));
		} else {
			throw new FwException(
					"The type of the field doesn't correspond. Type="
							+ field.getType());
		}

		return result;
	}

	/**
	 * 主キーを条件にしたWhere句を取得
	 *
	 * @param obj
	 * @return
	 */
	public static String getIdWhere(Object obj) {
		return "id=" + String.valueOf(ReflectionUtil.getFieldValue(obj, "id"));
	}

	/**
	 * レコード挿入/更新用のContentValuesを取得
	 *
	 * @param obj
	 * @param insertFlag
	 * @return
	 */
	public static ContentValues createContentValues(Object obj,
			boolean insertFlag) {
		ContentValues values = new ContentValues();
		Field[] fields = obj.getClass().getDeclaredFields();
		for (Field f : fields) {

			// 挿入時、自動生成のキーは、対象外とする。
			if (insertFlag && isGeneratedValue(f)) {
				continue;
			}

			// 対象外項目の判定
			if (isExclude(f)) {
				continue;
			}

			String val = convertToColumnValue(f, obj);
			values.put(getColumnName(f), val);
		}
		return values;
	}

	public static <T> T createEntity(Class<T> targetClass, Cursor cursor) {
		// エンティティのインスタンスを作成
		T entity = ReflectionUtil.newInstance(targetClass);

		// フィールドに値を詰める
		for (Field field : targetClass.getDeclaredFields()) {
			if (isExclude(field)) {
				continue;
			}
			copyColumn(cursor, entity, field);
		}

		return entity;
	}

	public static <T> T createEntity(Class<T> targetClass,
			List<Field> fieldList, Cursor cursor) {
		// エンティティのインスタンスを作成
		T entity = ReflectionUtil.newInstance(targetClass);

		// フィールドに値を詰める
		for (int i = 0; i < fieldList.size(); i++) {
			copyColumn(cursor, i, entity, fieldList.get(i));
		}

		return entity;
	}

	private static void copyColumn(Cursor srcCursor, Object destObj, Field field) {
		Object value = getColumnValue(srcCursor, field);
		ReflectionUtil.setFieldValue(destObj, field, value);
	}

	private static void copyColumn(Cursor srcCursor, int index, Object destObj,
			Field field) {
		Object value = getColumnValue(srcCursor, index, field);
		ReflectionUtil.setFieldValue(destObj, field, value);
	}

	public static String getTableName(Object obj) {
		return getTableName(obj.getClass());
	}

	public static <T> String getTableName(Class<T> entityClass) {
		return toUnderScore(entityClass.getSimpleName());
	}

	public static <T> String getCreateTableSql(Class<T> entityClass) {
		StringBuffer sql = new StringBuffer();

		if (!validateEntityClass(entityClass)) {
			throw new FwException("The content of the entity class is illegal.");
		}

		Field[] fields = entityClass.getDeclaredFields();
		for (Field f : fields) {
			if (isExclude(f)) {
				continue;
			}
			if (sql.length() > 0) {
				sql.append(", ");
			}
			sql.append(getColumnName(f) + " ");
			sql.append(getColumnType(f));
			if (isId(f)) {
				sql.append(" primary key");
			}
			if (isGeneratedValue(f)) {
				sql.append(" autoincrement");
			}
		}

		return "create table " + getTableName(entityClass) + " (" + sql + ");";
	}

	private static <T> boolean validateEntityClass(Class<T> entityClass) {
		Field[] fields = entityClass.getDeclaredFields();

		// フィールドなしはNG
		if (fields.length <= 0) {
			return false;
		}

		// フィールド内容のチェック
		int idCount = 0;
		for (Field f : fields) {
			if (isId(f)) {
				idCount++;
			}
		}

		// 主キーが1つ以外はNG
		if (idCount != 1) {
			return false;
		}

		return true;
	}

	public static <T> String getDropTableSql(Class<T> entityClass) {
		return "drop table if exists " + getTableName(entityClass) + ";";
	}

	public static String formatWithQuotation(Date date) {
		return "'" + DateUtil.formatDateTime(date) + "'";
	}

	public static String formatWithQuotation(Calendar calendar) {
		return "'" + DateUtil.formatDateTime(calendar) + "'";
	}

}
