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.CtClass;
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) {
        DynamicInsert ent = cls.getAnnotation(DynamicInsert.class);
        return ent != null && ent.value();
    }

    /**
     * 動的更新確認
     * @param cls テーブルクラス
     * @return 動的更新の場合 true を返す。
     */
    public static boolean isDynamicUpdate(final Class<?> cls) {
        DynamicUpdate 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) {
        String name = cls.getPackage().getName() + "." + CamelCase.convert(table);

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

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

            ClassFile cf = ct.getClassFile2();
            AnnotationsAttribute 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 ct.toClass(cls.getClassLoader(), null).asSubclass(Serializable.class);

        } 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) {
        Annotation 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();

        StringJoiner column = new StringJoiner(", ");
        StringJoiner 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) {

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

        Arrays.stream(methods).filter(Factory::isGetter).forEach(m -> {
            Object 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();

        StringJoiner sb = new StringJoiner(", ");
        Method m = toUpdateType(obj, param, sb, isDynamicUpdate(obj.getClass()));
        String 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) {

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

        Method id = null;
        for (final Method m : methods) {
            if (Factory.isGetter(m)) {
                if (DaoUtil.isId(m)) {
                    id = m;
                } else {
                    Object 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();

        String 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) {
        StringJoiner 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 toEntity(final Map<String, Object> data, final Class<T> cls) {
        return toInstance(data, cls);
    }

    /**
     * エンティティリスト化
     * @param <T> ジェネリクス
     * @param data データ
     * @param cls クラス
     * @return エンティティリスト
     */
    public static <T> Stream<T> toEntityStream(
                    final Stream<Map<String, Object>> data, final Class<T> cls) {
        return data.map(m -> toInstance(m, cls));
    }

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

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

                Method 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 InstantiationException | IllegalAccessException 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)) {
                Constructor<?> c = getConstructor(cls, String.class);
                if (c != null) {
                    return Factory.construct(c, obj.toString());
                }
            }
        }
        return obj;
    }

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

            if (cls.isPrimitive()) {
                return Factory.toReference(cls).getConstructor(arg);
            } 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);
        }
    }
}
