package common.db.dao;

import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import core.config.Factory;

/**
 * DAO共通
 *
 * @author Tadashi Nakayama
 */
public final class DaoUtil {

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

	/**
	 * Id取得メソッド取得
	 *
	 * @param <T> ジェネリックス
	 * @param cls エンティティクラス
	 * @return ID取得メソッド
	 */
	public static <T> Optional<Method> getIdMethod(final Class<T> cls) {
		return Stream.of(cls.getMethods()).
				filter(Factory::isGetter).filter(DaoUtil::isId).findFirst();
	}

	/**
	 * シーケンス名取得
	 *
	 * @param <T> ジェネリクス
	 * @param cls エンティティクラス
	 * @return シーケンス名
	 */
	public static <T> String getSequenceName(final Class<T> cls) {
		return getIdMethod(cls).flatMap(
			m -> Optional.ofNullable(m.getAnnotation(SequenceGenerator.class)).filter(
				s -> {
					final var gv = m.getAnnotation(GeneratedValue.class);
					return gv != null && gv.generator().equals(s.name());
				}
			)
		).map(SequenceGenerator::sequenceName).orElse("");
	}

	/**
	 * 複合キー確認
	 *
	 * @param cls エンティティクラス
	 * @return 複合キークラスの場合 true を返す。
	 */
	public static boolean isEmbeddedId(final Class<?> cls) {
		return getIdMethod(cls).map(DaoUtil::isEmbeddedId).orElse(Boolean.FALSE);
	}

	/**
	 * 複合キー確認
	 *
	 * @param mt メソッド
	 * @return 複合キーメソッドの場合 true を返す。
	 */
	public static boolean isEmbeddedId(final Method mt) {
		return mt != null && mt.getAnnotation(EmbeddedId.class) != null;
	}

	/**
	 * テーブル名取得
	 *
	 * @param cls モデルクラス
	 * @return テーブル名
	 */
	public static String getTableName(final Class<?> cls) {
		final var tbl = cls.getAnnotation(Table.class);
		return Optional.ofNullable(tbl).map(Table::name).orElse("");
	}

	/**
	 * カラム名取得
	 *
	 * @param m メソッド
	 * @return カラム名
	 */
	public static String getColumnName(final Method m) {
		final var col = m.getAnnotation(Column.class);
		return Optional.ofNullable(col).map(Column::name).orElse("");
	}

	/**
	 * Null可確認
	 *
	 * @param m メソッド
	 * @return Null可の場合 true を返す。
	 */
	public static boolean isNullable(final Method m) {
		final var col = m.getAnnotation(Column.class);
		return col != null && col.nullable();
	}

	/**
	 * Id確認
	 *
	 * @param m メソッド
	 * @return Idの場合 true を返す。
	 */
	public static boolean isId(final Method m) {
		return m.getAnnotation(Id.class) != null;
	}

	/**
	 * Id以外確認
	 *
	 * @param m メソッド
	 * @return Idでない場合 true を返す。
	 */
	public static boolean isNotId(final Method m) {
		return !isId(m);
	}

	/**
	 * 値取得
	 *
	 * @param <T> Type
	 * @param obj DAOモデル
	 * @param name 設定項目名
	 * @return 取得値
	 */
	public static <T> T getValue(final Object obj, final String name) {
		return (obj != null)
			? Factory.invoke(obj, Factory.getMethod(obj.getClass(), "get" + name)) : null;
	}

	/**
	 * オブジェクトが指定アイテム名のセッター、ゲッターを持っているか
	 *
	 * @param cls DAOモデルクラス
	 * @param name 指定アイテム
	 * @return 持っている場合 true を返す。
	 */
	public static boolean hasItem(final Class<?> cls, final String name) {
		return Stream.of(cls.getMethods()).
				filter(Factory::isGetter).filter(DaoUtil::isNotId).
				map(Factory::toItemName).anyMatch(Predicate.isEqual(name));
	}

	/**
	 * DAOモデルクラスからDB項目名を配列で返却する。
	 *
	 * @param cls DAOモデルクラス
	 * @return DB項目名配列
	 */
	public static String[] getDBNames(final Class<?> cls) {
		return Stream.of(cls.getMethods()).
				filter(Factory::isGetter).filter(DaoUtil::isNotId).
				map(Factory::toItemName).map(DaoUtil::toDBName).toArray(String[]::new);
	}

	/**
	 * DAOモデルクラスから項目名を配列で返却する。
	 *
	 * @param cls DAOモデルクラス
	 * @return 項目名配列
	 */
	public static String[] getModelNames(final Class<?> cls) {
		return Stream.of(cls.getMethods()).
				filter(Factory::isGetter).filter(DaoUtil::isNotId).
				map(Factory::toItemName).toArray(String[]::new);
	}

	/**
	 * モデル項目名取得
	 *
	 * @param cols DBカラム名配列
	 * @return モデル項目名配列
	 */
	public static String[] toDBNames(final String... cols) {
		final var array = Optional.ofNullable(cols).orElse(new String[0]);
		return Stream.of(array).map(DaoUtil::toDBName).toArray(String[]::new);
	}

	/**
	 * エンティティ確認
	 *
	 * @param cls クラス
	 * @return エンティティの場合 true を返す。
	 */
	public static boolean isEntity(final Class<?> cls) {
		return cls != null && cls.getAnnotation(Entity.class) != null;
	}

	/**
	 * Tableアノテーション名取得
	 *
	 * @return Tableアノテーション名
	 */
	public static String getTableAnnotationClassName() {
		return Table.class.getName();
	}

	/**
	 * DB名称取得
	 *
	 * @param name 項目名称
	 * @return DB名称
	 */
	public static String toDBName(final String name) {
		final var separator = "_";
		final var pattern = Pattern.compile("([A-Z][^A-Z0-9]*)|([0-9]+)");
		return Optional.ofNullable(name).map(pattern::matcher).filter(Matcher::find).
			map(mc -> mc.replaceAll(m -> separator + m.group())).
			map(s -> s.startsWith(separator) ? s.substring(separator.length()) : s).
			map(s -> s.toUpperCase(Locale.ENGLISH)
		).orElse(name);
	}
}
