package batch.util;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

/**
 * パラメタユーティリティ
 *
 * @author Tadashi Nakayama
 */
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 var 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) {
		return Optional.ofNullable(param).
				map(String::trim).filter(Predicate.not(String::isEmpty)).
				map(s -> Stream.of(split(s)).map(ParameterUtil::unescape).toArray(String[]::new)
			).orElse(new String[0]);
	}

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

		var quot = 0;
		var i = 0;
		var 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 (var i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (str.codePointAt(i) == '=') {
				final var 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 (var i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (str.codePointAt(i) == '=') {
				final var 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 var val = Objects.toString(value, "").trim();
		var suffix = "";
		if (val.contains(" ")) {
			suffix = "\"";
			if (val.contains("\"")) {
				if (val.contains("'")) {
					throw new IllegalArgumentException(value);
				}
				suffix = "'";
			}
		}
		return suffix + val + suffix;
	}

	/**
	 * パラメタ文字列をMapに変換する。
	 *
	 * @param params パラメタ文字列（Key=Valueの空白区切り）配列
	 * @return マップ
	 */
	public static Map<String, String[]> toMap(final String... params) {
		final var map = new HashMap<String, String[]>();
		if (params != null) {
			for (final var param : params) {
				int s = 0;
				for (var e = param.indexOf('='); 0 <= e; e = param.indexOf('=', s)) {
					final var key = param.substring(s, e);
					final var 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 var value = Optional.ofNullable(vals).orElse(new String[0]);
		final var obj = map.get(key);
		if (obj != null) {
			final var lenA = Array.getLength(obj);
			final var lenB = Array.getLength(value);
			final var 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) {
		var end = start;
		var 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) {
		final Predicate<String> quote = s -> 2 <= s.length()
			&& ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));

		return Optional.ofNullable(str).map(String::trim).filter(quote).map(
				s -> s.substring(1, s.length() - 1)
			).orElse(str);
	}

	/**
	 * スペース以外が存在するまでのスペースの数を返す。
	 *
	 * @param param パラメタ文字列
	 * @param start 開始位置
	 * @return スペース数
	 */
	private static int spaces(final String param, final int start) {
		var 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 var ret = new HashMap<String, String[]>();
		if (map1 != null) {
			ret.putAll(map1);
		}
		if (map2 != null) {
			for (final var 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 var vals = Optional.ofNullable(strs).orElse(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 -> Date.class.isInstance(o)
				? DateUtil.toString(Date.class.cast(o)) : Objects.toString(o, "");

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

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

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

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