package core.config;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Objects;
import java.util.Optional;

import org.apache.logging.log4j.LogManager;

import core.exception.PhysicalException;
import core.exception.ThrowableUtil;

/**
 * クラスFactory
 *
 * @author Tadashi Nakayama
 */
public abstract class Factory {

	/** インスタンス */
	private static final Factory INSTANCE = toInstance(loadClass(getClassName(Factory.class)));

	/**
	 * コンストラクタ
	 */
	protected Factory() {
		if (INSTANCE != null) {
			throw new AssertionError();
		}
	}

	/**
	 * インタフェースクラスに対応するインスタンスを返却
	 *
	 * @param <T> ジェネリックス
	 * @param cls インスタンス化クラス
	 * @return インスタンス
	 */
	public static <T> T create(final Class<T> cls) {
		return INSTANCE.getClassInstance(cls, null);
	}

	/**
	 * インタフェースクラスに対応するインスタンスを返却
	 *
	 * @param <T> ジェネリックス
	 * @param <U> ジェネリックス
	 * @param cls インスタンス化クラス
	 * @param caller 呼び出し元クラス
	 * @return インスタンス
	 */
	public static <T, U> T create(final Class<T> cls, final Class<U> caller) {
		return INSTANCE.getClassInstance(cls, caller);
	}

	/**
	 * インスタンス作成
	 *
	 * @param <T> ジェネリックス
	 * @param <U> ジェネリックス
	 * @param cls インスタンス化クラス
	 * @param caller 呼び出し元クラス
	 * @return インスタンス
	 */
	protected abstract <T, U> T getClassInstance(Class<T> cls, Class<U> caller);

	/**
	 * クラス名取得
	 *
	 * @param cls クラス
	 * @return クラス名
	 */
	protected static String getClassName(final Class<?> cls) {
		return getClassName(cls, null);
	}

	/**
	 * クラス名取得
	 *
	 * @param cls クラス
	 * @param caller 呼び出し元クラス
	 * @return クラス名
	 */
	protected static String getClassName(final Class<?> cls, final Class<?> caller) {
		var val = getFromEnv(cls, caller);
		if (val.isEmpty()) {
			if (cls.isInterface() || Modifier.isAbstract(cls.getModifiers())) {
				val = addImpl(cls.getName());
			} else {
				val = cls.getName();
			}
		}
		return val;
	}

	/**
	 * 設定からクラス名取得
	 *
	 * @param cls インスタンス化クラス
	 * @param caller 呼び出し元クラス
	 * @return クラス名
	 */
	protected static String getFromEnv(final Class<?> cls, final Class<?> caller) {
		var ret = "";
		if (caller != null) {
			ret = Objects.toString(Env.getEnv(cls.getName() + ":" + caller.getName()), "").trim();
		}
		if (ret.isEmpty()) {
			ret = Objects.toString(Env.getEnv(cls.getName()), "").trim();
		}
		return ret;
	}

	/**
	 * Impl追加処理
	 *
	 * @param name クラス名
	 * @return Impl追加クラス名
	 */
	protected static String addImpl(final String name) {
		final var impl = "Impl";
		final var loc = name.lastIndexOf('$');
		return ((0 <= loc) ? addImpl(name.substring(0, loc)) + name.substring(loc) : name) + impl;
	}

	/**
	 * クラスローダー取得
	 *
	 * @return クラスローダー
	 */
	protected static ClassLoader getClassLoader() {
		return Objects.requireNonNullElseGet(
				Thread.currentThread().getContextClassLoader(),
				Factory::getClassLoader);
	}

	/**
	 * クラス取得
	 *
	 * @param <T> Type
	 * @param name クラス名
	 * @return クラス
	 */
	public static <T> Class<T> loadClass(final String name) {
		try {
			return cast(getClassLoader().loadClass(name));
		} catch (final ClassNotFoundException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		}
	}

	/**
	 * インスタンス作成
	 *
	 * @param <T> ジェネリックス
	 * @param cls インタフェースクラス
	 * @return インスタンス
	 */
	protected static <T> T toInstance(final Class<T> cls) {
		try {
			return (cls != null) ? cls.getDeclaredConstructor().newInstance() : null;
		} catch (final ReflectiveOperationException ex) {
			LogManager.getLogger().error(ex.getMessage(), ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * サブクラス判断
	 *
	 * @param parent 親クラス
	 * @param subclass サブクラス
	 * @return サブクラスの場合 true を返す。
	 */
	public static boolean isSubclassOf(final Class<?> parent, final Class<?> subclass) {
		if (parent != null && subclass != null) {
			var ret = isSubclassOf(parent.getComponentType(), subclass.getComponentType());
			if (ret && !parent.isArray() && !subclass.isArray()) {
				ret = parent.isAssignableFrom(subclass);
			}
			return ret;
		}
		return parent == null && subclass == null;
	}

	/**
	 * オブジェクトのクラスを返す。
	 * 配列時は配列を構成しているクラスを返す。
	 *
	 * @param <T> Type
	 * @param cls 指定クラス
	 * @return クラスオブジェクト
	 */
	public static <T> Class<T> getComponentBaseClass(final Class<?> cls) {
		var c = cls;
		while (c != null && c.isArray()) {
			c = c.getComponentType();
		}
		return cast(c);
	}

	/**
	 * プリミティブ型クラスから参照型クラスへ変換する。
	 *
	 * @param <T> ジェネリックス
	 * @param cls クラス
	 * @return 参照型クラスを返却
	 */
	public static <T> Class<T> toReference(final Class<?> cls) {
		return cast((cls != null && cls.isPrimitive())
				? Array.get(Array.newInstance(cls, 1), 0).getClass() : cls);
	}

	/**
	 * メソッド取得
	 *
	 * @param cls クラス
	 * @param name メソッド名
	 * @param prm メソッドパラメタクラス
	 * @return メソッド
	 */
	public static Method getMethod(final Class<?> cls, final String name, final Class<?>... prm) {
		try {
			return (cls != null && name != null) ? cls.getMethod(name, prm) : null;
		} catch (final NoSuchMethodException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		}
	}

	/**
	 * メソッド呼出
	 *
	 * @param <T> Type
	 * @param obj オブジェクト
	 * @param mt メソッド
	 * @param prm パラメタ
	 * @return 復帰値
	 */
	public static <T> T invoke(final Object obj, final Method mt, final Object... prm) {
		try {
			return (mt != null) ? cast(mt.invoke(obj, prm)) : null;
		} catch (final InvocationTargetException ex) {
			final var th = ex.getCause();
			if (RuntimeException.class.isInstance(th)) {
				throw RuntimeException.class.cast(th);
			}
			ThrowableUtil.error(th);
			throw new PhysicalException(th);
		} catch (final IllegalAccessException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * コンストラクタ取得
	 *
	 * @param <T> ジェネリックス
	 * @param cls クラス
	 * @param arg コンストラクタ引数クラス
	 * @return コンストラクタ
	 */
	public static <T> Constructor<T> getConstructor(final Class<T> cls, final Class<?>... arg) {
		try {
			return cls.getConstructor(arg);
		} catch (final NoSuchMethodException e) {
			LogManager.getLogger().info(e.getMessage());
			return null;
		}
	}

	/**
	 * オブジェクト作成
	 *
	 * @param <T> ジェネリックス
	 * @param cons コンストラクタ
	 * @param val コンストラクタ引数
	 * @return 作成オブジェクト
	 */
	public static <T> T construct(final Constructor<T> cons, final Object... val) {
		try {
			return (cons != null) ? cons.newInstance(val) : null;
		} catch (final InstantiationException e) {
			LogManager.getLogger().info(e.getMessage());
			return null;
		} catch (final IllegalAccessException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		} catch (final InvocationTargetException e) {
			final var th = e.getCause();
			if (RuntimeException.class.isInstance(th)) {
				throw RuntimeException.class.cast(th);
			}
			ThrowableUtil.error(th);
			throw new PhysicalException(th);
		}
	}

	/**
	 * Getter確認
	 *
	 * @param mt メソッド
	 * @return Getterの場合 true を返す。
	 */
	public static boolean isGetter(final Method mt) {
		return mt != null && Modifier.isPublic(mt.getModifiers())
				&& !Modifier.isStatic(mt.getModifiers()) && !mt.isBridge() && !mt.isSynthetic()
				&& (mt.getName().startsWith("get") || mt.getName().startsWith("is"))
				&& mt.getParameterCount() == 0
				&& !Object.class.equals(mt.getDeclaringClass());
	}

	/**
	 * Setter確認
	 *
	 * @param mt メソッド
	 * @return Setterの場合 true を返す。
	 */
	public static boolean isSetter(final Method mt) {
		return mt != null && Modifier.isPublic(mt.getModifiers())
				&& !Modifier.isStatic(mt.getModifiers()) && !mt.isBridge() && !mt.isSynthetic()
				&& mt.getName().startsWith("set")
				&& mt.getParameterCount() == 1
				&& !Object.class.equals(mt.getDeclaringClass());
	}

	/**
	 * メソッドから項目名取得
	 *
	 * @param mt メソッド
	 * @return 項目名
	 */
	public static String toItemName(final Method mt) {
		return Optional.ofNullable(mt).map(Method::getName).map(
			m -> m.substring(m.startsWith("is") ? "is".length() : "get".length())
		).orElse(null);
	}

	/**
	 * キャスト
	 *
	 * @param <T> ジェネリクス
	 * @param obj オブジェクト
	 * @return キャストオブジェクト
	 */
	public static <T> T cast(final Object obj) {
		@SuppressWarnings("unchecked")
		final T ret = (T)obj;
		return ret;
	}
}
