package core.config;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * Factory実装
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public class FactoryImpl extends Factory {

	/** Lookupモード */
	private static final Integer ALL_MODES = Integer.valueOf(
			Lookup.PUBLIC | Lookup.PRIVATE | Lookup.PROTECTED | Lookup.PACKAGE);

	/** Lookupコンストラクタ */
	private static final Constructor<Lookup> CONSTRUCTOR;

	static {
		try {
			CONSTRUCTOR = Lookup.class.getDeclaredConstructor(Class.class, int.class);
			CONSTRUCTOR.setAccessible(true);
		} catch (final NoSuchMethodException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * @see core.config.Factory#getClassInstance(java.lang.Class, java.lang.Class)
	 */
	@Override
	protected <T, U> T getClassInstance(final Class<T> cls, final Class<U> caller) {
		final String name = Factory.getFromEnv(cls, caller);
		Class<T> c = cls;
		if (name.isEmpty()) {
			if (cls.isInterface() || Modifier.isAbstract(cls.getModifiers())) {
				c = Factory.loadClass(addImpl(cls.getName()));
				if (c == null && cls.isInterface()) {
					return proxyInstance(cls);
				}
			}
		} else {
			c = Factory.loadClass(name);
		}
		final T obj = Factory.invoke(null, Factory.getMethod(c, "getInstance"));
		return obj == null ? Factory.toInstance(c) : obj;
	}

	/**
	 * Proxyインスタンス作成
	 * @param <T> ジェネリックス
	 * @param cls クラス
	 * @return インスタンス
	 */
	private static <T> T proxyInstance(final Class<T> cls) {
		return Factory.cast(Proxy.newProxyInstance(
			Factory.getClassLoader(), new Class<?>[]{cls}, (p, m, a) -> {
				if (Modifier.isStatic(m.getModifiers())) {
					return m.invoke(null, a);
				} else if (!Modifier.isAbstract(m.getModifiers())) {
					final Lookup lookup = CONSTRUCTOR.newInstance(m.getDeclaringClass(), ALL_MODES);
					final MethodHandle handle = lookup.unreflectSpecial(m, m.getDeclaringClass());
					return handle.bindTo(p).invokeWithArguments(a);
				} else if ("iterator".equals(m.getName()) && m.getParameterCount() == 0) {
					return Arrays.stream(a).iterator();
				}
				return null;
			}
		));
	}
}
