package online.model;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;
import java.util.stream.Stream;

import core.config.Factory;
import core.util.DateUtil;
import core.util.NumberUtil;

/**
 * モデルユーティリティ
 *
 * @author Tadashi Nakayama
 */
public final class ModelUtil {

	/** タグセパレータ */
	public static final String TAG_SEPARATOR = "_";
	/** 画面情報　ステータス */
	public static final String TAG_STATUS = "STATUS";
	/** 画面情報　メッセージ */
	public static final String TAG_MESSAGE = "MESSAGE";
	/** 画面情報　不正値 */
	public static final String TAG_INVALID = "INVALID";
	/** 復帰値 */
	public static final String TAG_RETURN = "RETURN";

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

	/**
	 * クラスに存在する項目の値無し化
	 *
	 * @param model 汎用モデル
	 * @param cls クラス
	 */
	public static void noValueFields(final UniModel model, final Class<?> cls) {
		if (model != null && cls != null) {
			Stream.of(cls.getMethods()).filter(Factory::isGetter).forEach(Factory::toItemName);
		}
	}

	/**
	 * 文字列取得
	 *
	 * @param model 汎用モデル
	 * @param item 項目名
	 * @param loc 取得位置
	 * @return 文字列
	 */
	public static String getValueAsString(final UniModel model, final String item, final int loc) {
		var ret = "";
		if (0 <= loc && loc < model.getArraySize(item)) {
			final var cls = model.getValueClass(item);
			if (Factory.isSubclassOf(Date[].class, cls) || Factory.isSubclassOf(Date.class, cls)) {
				if (model.getDateArray(item)[loc] != null) {
					ret = Objects.toString(model.getDateArray(item)[loc], null);
				}
			} else if (Boolean[].class.equals(cls) || Boolean.class.equals(cls)) {
				if (model.getBooleanArray(item)[loc] != null) {
					ret = Objects.toString(model.getBooleanArray(item)[loc], null);
				}
			} else if (String[].class.equals(cls) || String.class.equals(cls)) {
				if (model.getStringArray(item)[loc] != null) {
					ret = model.getStringArray(item)[loc];
				}
			} else if (Factory.isSubclassOf(Number[].class, cls)
							|| Factory.isSubclassOf(Number.class, cls)) {
				if (model.getNumberArray(item)[loc] != null) {
					ret = Objects.toString(model.getNumberArray(item)[loc], null);
				}
			}
		}
		return ret;
	}

	/**
	 * リザーブ可能判断
	 *
	 * @param key キー
	 * @return リザーブ可能な場合 true を返す。
	 */
	public static boolean canSet(final String key) {
		return key != null && !isMsgKey(key) && !key.equals(TAG_RETURN);
	}

	/**
	 * メッセージ関連判断
	 *
	 * @param key キー
	 * @return メッセージ関連キーの場合 true を返す。
	 */
	public static boolean isMsgKey(final String key) {
		return key != null && (key.endsWith(TAG_MESSAGE)
				|| key.endsWith(TAG_STATUS) || key.endsWith(TAG_INVALID));
	}

	/**
	 * Beanから汎用モデルを取得します。
	 *
	 * @param bean Bean
	 * @return 汎用モデル
	 */
	public static UniModel getUniModel(final Object bean) {
		final var m = Factory.getMethod(bean.getClass(), "getUniModel");
		return Factory.invoke(bean, m);
	}

	/**
	 * Beanに汎用モデルを設定します。
	 *
	 * @param model 汎用モデル
	 * @param bean Bean
	 */
	public static void setUniModel(final Object bean, final UniModel model) {
		final var m = Factory.getMethod(bean.getClass(), "setUniModel", UniModel.class);
		Factory.invoke(bean, m, model);
	}

	/**
	 * 汎用モデルに、Bean項目の値を設定する。
	 *
	 * @param bean Bean
	 * @param key Beanから取得する項目名
	 * @param um 汎用モデル
	 * @return 設定した場合 true を返す。
	 */
	public static boolean setBeanItemValue(
			final UniModel um, final Object bean, final String key) {
		if (um != null && bean != null) {
			var mt = Factory.getMethod(bean.getClass(), "get" + key);
			if (mt == null) {
				mt = Factory.getMethod(bean.getClass(), "is" + key);
			}
			if (Factory.isGetter(mt) && isTarget(mt.getReturnType())) {
				putTo(um, "setValue", bean, mt);
				return true;
			}
		}
		return false;
	}

	/**
	 * 汎用モデルに、Bean項目の値を追加する。
	 *
	 * @param bean Bean
	 * @param key Beanから取得する項目名
	 * @param um 汎用モデル
	 * @return 設定した場合 true を返す。
	 */
	public static boolean addBeanItemValue(
			final UniModel um, final Object bean, final String key) {
		if (um != null && bean != null) {
			var mt = Factory.getMethod(bean.getClass(), "get" + key);
			if (mt == null) {
				mt = Factory.getMethod(bean.getClass(), "is" + key);
			}
			if (Factory.isGetter(mt) && isTarget(mt.getReturnType())) {
				putTo(um, "addValue", bean, mt);
				return true;
			}
		}
		return false;
	}

	/**
	 * 汎用モデルにBeanの値をセットします。
	 *
	 * @param bean Bean
	 * @param um 汎用モデル
	 * @return 汎用モデル
	 */
	public static UniModel setBeanValue(final UniModel um, final Object bean) {
		if (um != null && bean != null) {
			for (final var mt : bean.getClass().getMethods()) {
				if (Factory.isGetter(mt) && isTarget(mt.getReturnType())) {
					putTo(um, "setValue", bean, mt);
				}
			}
		}
		return um;
	}

	/**
	 * 汎用モデルにBeanの値を追加します。
	 *
	 * @param bean Bean
	 * @param um 汎用モデル
	 * @return 汎用モデル
	 */
	public static UniModel addBeanValue(final UniModel um, final Object bean) {
		if (um != null && bean != null) {
			for (final var mt : bean.getClass().getMethods()) {
				if (Factory.isGetter(mt) && isTarget(mt.getReturnType())) {
					putTo(um, "addValue", bean, mt);
				}
			}
		}
		return um;
	}

	/**
	 * 設定対象判定
	 *
	 * @param cls 判定対象クラス
	 * @return 設定対象の場合 true を返す。
	 */
	static boolean isTarget(final Class<?>... cls) {
		return cls.length == 1 && cls[0] != null
					&& (String.class.equals(cls[0])
					|| String[].class.equals(cls[0])
					|| Boolean.class.equals(cls[0])
					|| Boolean[].class.equals(cls[0])
					|| Factory.isSubclassOf(Number.class, cls[0])
					|| Factory.isSubclassOf(Number[].class, cls[0])
					|| Factory.isSubclassOf(Date.class, cls[0])
					|| Factory.isSubclassOf(Date[].class, cls[0])
					|| cls[0].isPrimitive()
					|| (cls[0].isArray() && cls[0].getComponentType().isPrimitive()));
	}

	/**
	 * 汎用モデルにBeanの値をセットします。
	 *
	 * @param obj Bean
	 * @param mt Beanメソッド
	 * @param um 汎用モデル
	 * @param inst インストラクション
	 */
	private static void putTo(final UniModel um, final String inst,
			final Object obj, final Method mt) {
		var type = mt.getReturnType();
		var val = Factory.invoke(obj, mt);
		if (Factory.isSubclassOf(Date[].class, type)) {
			type = Date[].class;
		} else if (Factory.isSubclassOf(Date.class, type)) {
			type = Date.class;
		} else if (type.isArray() && type.getComponentType().isPrimitive()) {
			final Class<?> oc = toObjectClass(type);
			val = changeClassArray(oc, val);
			type = Array.newInstance(oc, 0).getClass();
		} else if (type.isPrimitive()) {
			type = toObjectClass(type);
		}

		final var m = Factory.getMethod(um.getClass(), inst, String.class, type);
		Factory.invoke(um, m, Factory.toItemName(mt), val);
	}

	/**
	 * オブジェクトクラス取得
	 *
	 * @param type クラス
	 * @return プリミティブでないオブジェクトクラス
	 */
	private static Class<?> toObjectClass(final Class<?> type) {
		return Factory.toReference(Factory.getComponentBaseClass(type));
	}

	/**
	 * Beanに汎用モデルの値をセットします。
	 *
	 * @param model 汎用モデル
	 * @param bean Bean
	 */
	public static void setModelValue(final Object bean, final UniModel model) {
		setModelValue(bean, model, 0);
	}

	/**
	 * Beanに汎用モデルの値をセットします。
	 *
	 * @param model 汎用モデル
	 * @param bean Bean
	 * @param loc Beanが配列で無い場合の位置
	 */
	static void setModelValue(final Object bean, final UniModel model, final int loc) {
		if (bean != null && model != null) {
			final var map = model.toMap();
			for (final var mt : bean.getClass().getMethods()) {
				if (Factory.isSetter(mt)) {
					final var name = Factory.toItemName(mt);
					if (model.containsKey(name) && isTarget(mt.getParameterTypes())) {
						setValue(bean, mt, map.get(name), model.getValueClass(name), loc);
					}
				}
			}
		}
	}

	/**
	 * Beanの指定項目に値を設定する。
	 *
	 * @param bean Bean
	 * @param mt Beanメソッド
	 * @param data データ
	 * @param cls クラス
	 * @param loc Beanが配列で無い場合の位置
	 * @return 設定された場合 true を返す
	 */
	static boolean setValue(final Object bean, final Method mt,
			final Object data, final Class<?> cls, final int loc) {
		final var type = mt.getParameterTypes()[0];
		final var oc = toObjectClass(type);
		var val = data;
		if (cls != null && oc.equals(toObjectClass(cls))) {
			if (!cls.isArray() && type.isArray()) {
				final var ary = Array.newInstance(oc, 1);
				Array.set(ary, 0, data);
				val = ary;
			} else if (cls.isArray() && !type.isArray() && data != null) {
				if (loc < 0 || Array.getLength(data) <= loc) {
					return false;
				}
				val = Array.get(data, loc);
			} else if (cls.isArray() && type.isArray()
							&& type.getComponentType().isPrimitive()) {
				val = changeClassArray(type.getComponentType(), data);
			}
		} else {
			if (type.isArray()) {
				val = changeClassArray(type.getComponentType(), data);
			} else if (data != null && data.getClass().isArray() && !type.isArray()) {
				if (loc < 0 || Array.getLength(data) <= loc) {
					return false;
				}
				val = changeClass(oc, Array.get(data, loc));
			} else {
				val = changeClass(oc, data);
			}
		}
		return invoke(bean, mt, val);
	}

	/**
	 * メソッド呼び出し
	 *
	 * @param bean Bean
	 * @param mt Beanメソッド
	 * @param data データ
	 * @return 呼び出した場合 true を返す。
	 */
	private static boolean invoke(final Object bean, final Method mt, final Object data) {
		final var ret = (data != null) || !mt.getParameterTypes()[0].isPrimitive();
		if (ret) {
			Factory.invoke(bean, mt, data);
		}
		return ret;
	}

	/**
	 * 変換クラス配列取得
	 *
	 * @param cls 配列クラス
	 * @param val 設定値配列
	 * @return 変換配列
	 */
	private static Object changeClassArray(final Class<?> cls, final Object val) {
		if (val == null) {
			return null;
		}

		var ary = val;
		if (!ary.getClass().isArray()) {
			ary = new Object[]{val};
		}

		final var ret = Array.newInstance(cls, Array.getLength(ary));
		final var oc = toObjectClass(cls);
		for (var i = 0; i < Array.getLength(ary); i++) {
			final var obj = changeClass(oc, Array.get(ary, i));
			if (obj != null || !cls.isPrimitive()) {
				Array.set(ret, i, obj);
			}
		}
		return ret;
	}

	/**
	 * 変換オブジェクト取得
	 *
	 * @param cls オブジェクトクラス
	 * @param val 設定値配列
	 * @return 変換配列
	 */
	private static Object changeClass(final Class<?> cls, final Object val) {
		var ret = val;
		if (Factory.isSubclassOf(Date.class, cls)) {
			ret = DateUtil.toDateObject(Objects.toString(val, null), cls.asSubclass(Date.class));
		} else if (Factory.isSubclassOf(Number.class, cls)) {
			ret = NumberUtil.toNumber(Objects.toString(val, null), cls.asSubclass(Number.class));
		} else if (String.class.equals(cls)) {
			ret = Objects.toString(val, null);
		} else if (!Objects.toString(val, "").isEmpty()) {
			if (!cls.equals(val.getClass())) {
				ret = Factory.construct(Factory.getConstructor(cls, val.getClass()), val);
			}
		}
		return ret;
	}
}
