package batch.util;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

/**
 * パラメタユーティリティ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class ParameterUtil {

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

	/**
	 * 値存在確認
	 * @param item 項目名
	 * @param params パラメータマップ
	 * @return 存在する場合 true を返す。
	 */
	public static boolean hasValue(final String item, final Map<String, String[]> params) {
		final String[] pattern = params.get(item);
		return pattern != null && 0 < pattern.length && !Objects.toString(pattern[0], "").isEmpty();
	}

	/**
	 * パラメタ文字列配列化
	 *
	 * @param param パラメタ文字列（Key=Valueの空白区切り）
	 * @return Key=Value毎の配列
	 */
	public static String[] toArray(final String param) {
		if (param != null) {
			final String str = param.trim();
			if (!str.isEmpty()) {
				return Stream.of(split(str)).
					map(ParameterUtil::unescape).toArray(String[]::new);
			}
		}
		return new String[0];
	}

	/**
	 * 分割
	 *
	 * @param str 文字列
	 * @return 分割リスト
	 */
	private static String[] split(final String str) {
		final List<String> ret = new ArrayList<>();

		int quot = 0;
		int i = 0;
		int s = 0;
		while (i < str.length()) {
			if (str.codePointAt(i) == '"') {
				if (quot == '"') {
					quot = 0;
				} else if (quot == 0) {
					quot = '"';
				}
			} else if (str.codePointAt(i) == '\'') {
				if (quot == '\'') {
					quot = 0;
				} else if (quot == 0) {
					quot = '\'';
				}
			} else if (str.codePointAt(i) == ' ') {
				if (quot == 0) {
					ret.add(str.substring(s, i));
					s = i + " ".length();
				}
			}
			i = str.offsetByCodePoints(i, 1);
		}

		if (0 < i && quot == 0) {
			ret.add(str.substring(s, i));
		}

		return ret.toArray(new String[ret.size()]);
	}

	/**
	 * エスケープ解除変換
	 *
	 * @param str 変換対象文字列
	 * @return エスケープ解除文字列
	 */
	private static String unescape(final String str) {
		for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (str.codePointAt(i) == '=') {
				final int next = i + "=".length();
				return str.substring(0, next) + strip(str.substring(next));
			} else if (str.codePointAt(i) == '\'') {
				return strip(str);
			} else if (str.codePointAt(i) == '"') {
				return strip(str);
			}
		}
		return str;
	}

	/**
	 * エスケープ変換
	 *
	 * @param str 変換対象文字列
	 * @return エスケープ文字列
	 */
	private static String escape(final String str) {
		for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (str.codePointAt(i) == '=') {
				final int next = i + "=".length();
				return str.substring(0, next) + toParamValue(str.substring(next));
			} else if (str.codePointAt(i) == '\'') {
				return toParamValue(str);
			} else if (str.codePointAt(i) == '"') {
				return toParamValue(str);
			}
		}
		return str;
	}

	/**
	 * パラメタ文字列化
	 * @param value 値
	 * @return パラメタ文字列
	 */
	private static String toParamValue(final String value) {
		final String val = Objects.toString(value, "").trim();
		if (val.contains(" ")) {
			if (val.contains("\"")) {
				if (val.contains("'")) {
					throw new IllegalArgumentException(value);
				}
				return "'" + val + "'";
			}
			return "\"" + val + "\"";
		}
		return val;
	}

	/**
	 * パラメタ文字列をMapに変換する。
	 * @param params パラメタ文字列（Key=Valueの空白区切り）配列
	 * @return マップ
	 */
	public static Map<String, String[]> toMap(final String... params) {
		final Map<String, String[]> map = new HashMap<>();
		if (params != null) {
			for (final String param : params) {
				int s = 0;
				for (int e = param.indexOf('='); 0 <= e; e = param.indexOf('=', s)) {
					final String key = param.substring(s, e);
					final String value = strip(getValue(param, e + "=".length()));
					addMap(map, key, value);
					s += key.length() + "=".length() + value.length();
					s += spaces(param, s);
				}
			}
		}
		return map;
	}

	/**
	 * マップに追加する。
	 * @param map マップ
	 * @param key キー
	 * @param vals 値
	 */
	private static void addMap(final Map<String, String[]> map,
			final String key, final String... vals) {
		final String[] value = (vals == null) ? new String[0] : vals;
		final Object obj = map.get(key);
		if (obj != null) {
			final int lenA = Array.getLength(obj);
			final int lenB = Array.getLength(value);
			final String[] array = (String[]) Array.newInstance(String.class, lenA + lenB);
			System.arraycopy(obj, 0, array, 0, lenA);
			System.arraycopy(value, 0, array, lenA, lenB);
			map.put(key, array);
		} else {
			map.put(key, value);
		}
	}

	/**
	 * 値を取得する。
	 * @param param パラメタ文字列
	 * @param start 開始位置
	 * @return 値（クォーテーション付きの場合はそのまま）
	 */
	private static String getValue(final String param, final int start) {
		int end = start;
		String search = " ";
		if (start < param.length()) {
			if (param.codePointAt(start) == '"') {
				search = "\"";
				end += search.length();
			} else if (param.codePointAt(start) == '\'') {
				search = "'";
				end += search.length();
			}

			end = param.indexOf(search, end);
			if (end < 0) {
				end = param.length();
			} else if (!" ".equals(search)) {
				end += search.length();
			}
		}
		return param.substring(start, end);
	}

	/**
	 * クォーテーション削除
	 * @param str 対象文字列
	 * @return 削除後文字列
	 */
	private static String strip(final String str) {
		if (str != null) {
			final String s = str.trim();
			if (2 <= s.length()) {
				if ((s.startsWith("\"") && s.endsWith("\""))
						|| (s.startsWith("'") && s.endsWith("'"))) {
					return s.substring(1, s.length() - 1);
				}
			}
		}
		return str;
	}

	/**
	 * スペース以外が存在するまでのスペースの数を返す。
	 * @param param パラメタ文字列
	 * @param start 開始位置
	 * @return スペース数
	 */
	private static int spaces(final String param, final int start) {
		int ret = 0;
		while (start + ret < param.length() && param.codePointAt(start + ret) == ' ') {
			ret += " ".length();
		}
		return ret;
	}

	/**
	 * パラメタマップをマージする。同一項目名は、値の配列に付加される。
	 * @param map1 パラメタマップ1
	 * @param map2 パラメタマップ2
	 * @return マージされたパラメタマップ
	 */
	public static Map<String, String[]> merge(
			final Map<String, String[]> map1, final Map<String, String[]> map2) {
		final Map<String, String[]> ret = new HashMap<>();
		if (map1 != null) {
			ret.putAll(map1);
		}
		if (map2 != null) {
			for (final Entry<String, String[]> me : map2.entrySet()) {
				addMap(ret, me.getKey(), me.getValue());
			}
		}
		return ret;
	}

	/**
	 * パラメタ文字列化
	 *
	 * @param strs Key=Value文字列配列
	 * @return パラメタ文字列（Key=Valueの空白区切り）
	 */
	public static String toParameter(final String... strs) {
		final String[] vals = strs != null ? strs : new String[0];
		return Stream.of(vals).filter(
				s -> !Objects.toString(s, "").trim().isEmpty()
			).map(ParameterUtil::escape).collect(Collectors.joining(" "));
	}

	/**
	 * オブジェクトからパラメタ文字列を作成します。
	 *
	 * @param obj オブジェクト
	 * @return パラメタ文字列（Key=Valueの空白区切り）
	 */
	public static String toParameter(final Object obj) {
		final Function<Object, String> toObjectString = o -> {
			return Date.class.isInstance(o)
					? DateUtil.toString(Date.class.cast(o)) : Objects.toString(o, "");
		};

		final Function<String, String> decapitalize = s -> {
			final int loc = s.offsetByCodePoints(0, 1);
			return s.substring(0, loc).toLowerCase(Locale.ENGLISH) + s.substring(loc);
		};

		final StringJoiner ret = new StringJoiner(" ");
		if (obj != null) {
			for (final Method mt : obj.getClass().getMethods()) {
				if (!mt.getName().startsWith("set")) {
					continue;
				}

				final String mname = Factory.toItemName(mt);
				final Method getter = Factory.getMethod(obj.getClass(), "get" + mname);
				final Object val = Factory.invoke(obj, getter);
				if (val == null) {
					continue;
				}

				if (val.getClass().isArray()) {
					final String decap = decapitalize.apply(mname);
					for (final Object o : Object[].class.cast(val)) {
						ret.add(decap + "=" + toObjectString.apply(o));
					}
				} else {
					ret.add(decapitalize.apply(mname) + "=" + toObjectString.apply(val));
				}
			}
		}
		return ret.toString();
	}
}
