package common.db.dao.hibernate;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import common.db.dao.DaoUtil;
import core.config.Factory;
import core.exception.PhysicalException;
import core.util.bean.CamelCase;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.StringMemberValue;

/**
 * エンティティユーティリティ
 *
 * @author Tadashi Nakayama
 */
public final class EntityUtil {

	/**
	 * コンストラクタ
	 */
	private EntityUtil() {
		throw new AssertionError();
	}

	/**
	 * 動的作成確認
	 *
	 * @param cls テーブルクラス
	 * @return 動的作成の場合 true を返す。
	 */
	public static boolean isDynamicInsert(final Class<?> cls) {
		final var ent = cls.getAnnotation(DynamicInsert.class);
		return ent != null && ent.value();
	}

	/**
	 * 動的更新確認
	 *
	 * @param cls テーブルクラス
	 * @return 動的更新の場合 true を返す。
	 */
	public static boolean isDynamicUpdate(final Class<?> cls) {
		final var ent = cls.getAnnotation(DynamicUpdate.class);
		return ent != null && ent.value();
	}

	/**
	 * エンティティクラス拡張
	 *
	 * @param <T> ジェネリクス
	 * @param cls 基底クラス
	 * @param table テーブル名
	 * @return 拡張クラス
	 */
	public static <T extends Serializable> Class<T> extend(
			final Class<T> cls, final String table) {
		final var name = cls.getPackage().getName() + "." + CamelCase.convert(table);

		final Class<T> c = Factory.loadClass(name);
		if (c != null) {
			return c;
		}

		try {
			final var pool = ClassPool.getDefault();
			final var ct = pool.makeClass(name, pool.get(cls.getName()));

			final var cf = ct.getClassFile2();
			final var attr = new AnnotationsAttribute(
							cf.getConstPool(), AnnotationsAttribute.visibleTag);
			attr.addAnnotation(getJpaTable(cf, table));
			if (isDynamicInsert(cls)) {
				attr.addAnnotation(new Annotation(
								DynamicInsert.class.getName(), cf.getConstPool()));
			}
			if (isDynamicUpdate(cls)) {
				attr.addAnnotation(new Annotation(
								DynamicUpdate.class.getName(), cf.getConstPool()));
			}
			ct.setAttribute(AnnotationsAttribute.visibleTag, attr.get());

			return Factory.cast(ct.toClass(cls.getClassLoader(), null));

		} catch (final NotFoundException | CannotCompileException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		}
	}

	/**
	 * JPA Tableアノテーション取得
	 *
	 * @param cf ClassFile
	 * @param table テーブル名
	 * @return アノテーション
	 */
	private static Annotation getJpaTable(final ClassFile cf, final String table) {
		final var anno = new Annotation(DaoUtil.getTableAnnotationClassName(), cf.getConstPool());
		anno.addMemberValue("name", new StringMemberValue(table, cf.getConstPool()));
		return anno;
	}

	/**
	 * 作成SQL化
	 *
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @return SQL
	 */
	public static String toInsertSql(final Serializable obj, final List<Object> param) {
		param.clear();

		final var column = new StringJoiner(", ");
		final var value = new StringJoiner(", ");
		toInsertType(obj, param, column, value, isDynamicInsert(obj.getClass()));
		return "INSERT INTO " + DaoUtil.getTableName(obj.getClass())
				+ "(" + column.toString() + ") VALUES(" + value.toString() + ")";
	}

	/**
	 * Insert形式SQL化
	 *
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @param column カラム名バッファ
	 * @param value 値バッファ
	 * @param dynamic 動的フラグ
	 */
	private static void toInsertType(final Object obj, final List<Object> param,
			final StringJoiner column, final StringJoiner value, final boolean dynamic) {

		final var methods = obj.getClass().getMethods();
		Arrays.sort(methods, Comparator.comparing(Method::getName));

		Stream.of(methods).filter(Factory::isGetter).forEach(m -> {
			final var o = Factory.invoke(obj, m);
			if (DaoUtil.isId(m) && DaoUtil.isEmbeddedId(m)) {
				toInsertType(o, param, column, value, false);
			} else {
				if (o != null || !dynamic) {
					param.add(o);
					column.add(DaoUtil.getColumnName(m));
					value.add(":" + param.size());
				}
			}
		});
	}

	/**
	 * 更新SQL化
	 *
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @return SQL
	 */
	public static String toUpdateSql(final Serializable obj, final List<Object> param) {
		param.clear();

		final var sb = new StringJoiner(", ");
		final var m = toUpdateType(obj, param, sb, isDynamicUpdate(obj.getClass()));
		final var where = toWhereString(Factory.invoke(obj, m), param, m);
		return "UPDATE " + DaoUtil.getTableName(obj.getClass()) + " SET " + sb.toString() + where;
	}

	/**
	 * Update形式SQL化
	 *
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @param sb バッファ
	 * @param dynamic 動的フラグ
	 * @return idメソッド
	 */
	private static Method toUpdateType(final Object obj, final List<Object> param,
			final StringJoiner sb, final boolean dynamic) {

		final var methods = obj.getClass().getMethods();
		Arrays.sort(methods, Comparator.comparing(Method::getName));

		Method id = null;
		for (final var m : methods) {
			if (Factory.isGetter(m)) {
				if (DaoUtil.isId(m)) {
					id = m;
				} else {
					final var o = Factory.invoke(obj, m);
					if (o != null || !dynamic) {
						param.add(o);
						sb.add(DaoUtil.getColumnName(m) + " = :" + param.size());
					}
				}
			}
		}
		return id;
	}

	/**
	 * 削除SQL化
	 *
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @return SQL
	 */
	public static String toDeleteSql(final Serializable obj, final List<Object> param) {
		param.clear();

		final var where = DaoUtil.getIdMethod(obj.getClass()).map(m ->
			toWhereString(Factory.invoke(obj, m), param, m)
		).orElse("");
		return "DELETE FROM " + DaoUtil.getTableName(obj.getClass()) + where;
	}

	/**
	 * Where句作成
	 *
	 * @param o オブジェクト
	 * @param param パラメタ
	 * @param m Idメソッド
	 * @return Where句
	 */
	public static String toWhereString(final Object o, final List<Object> param, final Method m) {
		final var where = new StringJoiner(", ");
		if (DaoUtil.isEmbeddedId(m)) {
			toUpdateType(o, param, where, false);
		} else {
			param.add(o);
			where.add(DaoUtil.getColumnName(m) + " = :" + param.size());
		}
		return " WHERE " + where.toString();
	}

	/**
	 * インスタンス化
	 *
	 * @param <T> ジェネリクス
	 * @param data データ
	 * @param cls クラス
	 * @return インスタンス
	 */
	public static <T> T toInstance(final Map<String, Object> data, final Class<T> cls) {
		try {

			final T ret = cls.getDeclaredConstructor().newInstance();
			Stream.of(cls.getMethods()).filter(Factory::isGetter).forEach(m -> {
				var obj = data.get(DaoUtil.getColumnName(m));
				if (DaoUtil.isId(m) && DaoUtil.isEmbeddedId(m)) {
					obj = toInstance(data, m.getReturnType());
				}

				final var mt = Factory.getMethod(
								cls, "set" + Factory.toItemName(m), m.getReturnType());
				if (obj != null && mt != null) {
					Factory.invoke(ret, mt, toValidType(obj, mt.getParameterTypes()[0]));
				}
			});
			return ret;

		} catch (final ReflectiveOperationException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		}
	}

	/**
	 * 型変換
	 *
	 * @param obj オブジェクト
	 * @param cls クラス
	 * @return 変換後オブジェクト
	 */
	private static Object toValidType(final Object obj, final Class<?> cls) {
		if (!cls.equals(obj.getClass())) {
			if (Number.class.isInstance(obj)) {
				final var c = getConstructor(cls);
				if (c != null) {
					return Factory.construct(c, obj.toString());
				}
			}
		}
		return obj;
	}

	/**
	 * コンストラクタ取得
	 *
	 * @param cls クラス
	 * @return コンストラクタ
	 */
	private static Constructor<?> getConstructor(final Class<?> cls) {
		try {

			if (cls.isPrimitive()) {
				return Factory.toReference(cls).getConstructor(String.class);
			} else if (Number.class.isAssignableFrom(cls)) {
				return cls.getConstructor(String.class);
			}
			return null;

		} catch (final NoSuchMethodException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		}
	}
}
