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 org.apache.logging.log4j.LogManager;

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

/**
 * クラスFactory
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
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) {
		String 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) {
		String 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 String impl = "Impl";
		final int loc = name.lastIndexOf('$');
		return (0 <= loc ? addImpl(name.substring(0, loc)) + name.substring(loc) : name) + impl;
	}

	/**
	 * クラスローダー取得
	 *
	 * @return クラスローダー
	 */
	protected static ClassLoader getClassLoader() {
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (cl == null) {
			cl = Factory.class.getClassLoader();
		}
		return cl;
	}

	/**
	 * クラス取得
	 * @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.newInstance() : null;
		} catch (final IllegalAccessException | InstantiationException 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) {
			final boolean ret = isSubclassOf(
					parent.getComponentType(), subclass.getComponentType());
			if (ret && !parent.isArray() && !subclass.isArray()) {
				return 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) {
		Class<?> 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 Throwable 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 Throwable 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) {
		if (mt != null) {
			if (mt.getName().startsWith("is")) {
				return mt.getName().substring("is".length());
			}
			return mt.getName().substring(mt.getName().offsetByCodePoints(0, 3));
		}
		return null;
	}

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