package common.db.dao;

import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Predicate;
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
 * @version 1.0.0
 */
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 -> {
					GeneratedValue gv = m.getAnnotation(GeneratedValue.class);
					return gv != null && gv.generator() != 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).booleanValue();
	}

	/**
	 * 複合キー確認
	 * @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) {
		Table tbl = cls.getAnnotation(Table.class);
		return tbl != null ? tbl.name() : "";
	}

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

	/**
	 * Null可確認
	 * @param m メソッド
	 * @return Null可の場合 true を返す。
	 */
	public static boolean isNullable(final Method m) {
		Column 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) {
		if (obj != null) {
			return Factory.invoke(obj, Factory.getMethod(obj.getClass(), "get" + name));
		}
		return 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) {
		return Optional.ofNullable(cols).map(
			c -> Stream.of(c).map(DaoUtil::toDBName).toArray(String[]::new)
		).orElse(new String[0]);
	}

	/**
	 * エンティティ確認
	 * @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) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; name != null && i < name.length(); i = name.offsetByCodePoints(i, 1)) {
			int c = name.codePointAt(i);
			if ('A' <= c && c <= 'Z') {
				if (0 < sb.length()) {
					sb.append('_');
				}
			} else if ('a' <= c && c <= 'z') {
				sb.appendCodePoint(c - ('a' - 'A'));
				continue;
			} else if ('0' <= c && c <= '9') {
				if (0 < i) {
					int pre = name.codePointBefore(i);
					if (pre < '0' || '9' < pre) {
						sb.append('_');
					}
				}
			}
			sb.appendCodePoint(c);
		}
		return sb.toString();
	}
}
