package common.sql;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

import core.util.DateUtil;

/**
 * パースノードクラス
 *
 * @author Tadashi Nakayama
 */
public interface LineParsedNode {

	/** 連結子 */
	String SEPARATOR = "#";

	/**
	 * クエリ構築
	 *
	 * @param pmap パラメタマップ
	 * @param plist パラメタリスト
	 * @return クエリ
	 */
	String build(Map<String, ?> pmap, List<Object> plist);

	/**
	 * ターゲット判断
	 *
	 * @param pmap パラメタマップ
	 * @return ターゲットの場合 true を返す。
	 */
	boolean isTarget(Map<String, ?> pmap);

	/**
	 * 次セパレータの次位置
	 *
	 * @param val 対象文字列
	 * @param loc 検索開始位置
	 * @return セパレータ位置の次位置
	 */
	default int separatorNext(final String val, final int loc) {
		return val.indexOf('#', loc) + SEPARATOR.length();
	}

	/**
	 * 開始確認
	 *
	 * @param word 開始
	 * @param val 確認対象文字列
	 * @return 開始文字で始まっている場合 true を返す。
	 */
	static boolean starts(final String word, final String val) {
		return val != null && word.length() <= val.length()
			&& (word.startsWith(val.substring(0, word.length()).toUpperCase(Locale.ENGLISH)));
	}

	/**
	 * 終端確認
	 *
	 * @param trm 終端
	 * @param val 確認対象
	 * @return 終端文字列で終わっている場合 true を返す。
	 */
	static boolean ends(final String trm, final String val) {
		final Predicate<Integer> isEnd = l ->
			0 <= l && trm.equalsIgnoreCase(val.substring(l))
			&& (l == 0 || Character.isWhitespace(val.codePointAt(val.offsetByCodePoints(l, -1))));

		return val != null && isEnd.test(val.length() - trm.length());
	}

	/**
	 * 有効確認
	 *
	 * @param obj ターゲット値
	 * @return 有効の場合 true を返す。
	 */
	static boolean isValid(final Object obj) {
		final Predicate<List<?>> isNotEmptyList = list -> list.stream().
				map(o -> Objects.toString(o, "")).anyMatch(Predicate.not(String::isEmpty));

		if (List.class.isInstance(obj)) {
			final var list = List.class.cast(obj);
			return !list.isEmpty() && isNotEmptyList.test(list);
		} else if (obj != null && obj.getClass().isArray()) {
			final var ary = Object[].class.cast(obj);
			return Stream.of(ary).anyMatch(o -> !Objects.toString(o, "").isEmpty());
		}
		return !Collection.class.isInstance(obj) && !Objects.toString(obj, "").isEmpty();
	}

	/**
	 * 複数キー判断
	 *
	 * @param bind bind文字列
	 * @return 複数キーの場合 true を返す。
	 */
	static boolean isMultiKey(final String bind) {
		final var idx = bind.indexOf('#');
		return 0 <= idx && 0 <= bind.indexOf('#', idx + SEPARATOR.length());
	}

	/**
	 * 複数キー取得
	 *
	 * @param bind bind文字列
	 * @return 複数キー配列
	 */
	default List<String> getMultiKeys(final String bind) {
		final var ret = new ArrayList<String>();
		final var idx = bind.lastIndexOf('#');
		if (0 <= idx) {
			final var keys = bind.substring(0, idx).split(SEPARATOR);
			for (var i = 0; i < keys.length; i++) {
				if (isReplace(keys[i].codePointAt(0))) {
					ret.clear();
					break;
				} else if (0 < i && keys[i].codePointAt(0) != keys[i - 1].codePointAt(0)) {
					ret.clear();
					break;
				} else {
					ret.add(keys[i].substring(keys[i].offsetByCodePoints(0, 1)));
				}
			}
		}
		return ret;
	}

	/**
	 * キー取得
	 *
	 * @param bind bind文字列
	 * @return キー
	 */
	default String getKey(final String bind) {
		final var idx = bind.lastIndexOf('#');
		return (0 <= idx) ? bind.substring(bind.offsetByCodePoints(0, 1), idx) : bind;
	}

	/**
	 * パラメタ情報取得
	 *
	 * @param bind bind文字列
	 * @return パラメタ情報
	 */
	default String getLiteral(final String bind) {
		final var idx = bind.lastIndexOf('#');
		return (0 <= idx) ? bind.substring(idx + SEPARATOR.length()).trim() : "";
	}

	/**
	 * BIND文字判断
	 * : 必須 $ 有無 ? 無いとき初期値 @ 置換
	 *
	 * @param cp コードポイント
	 * @return BIND文字の場合 true を返す。
	 */
	static boolean isBindCharacter(final int cp) {
		return cp == ':' || cp == '$' || cp == '?' || cp == '@';
	}

	/**
	 * 項目不在時検索不可判断
	 *
	 * @param bind bind文字列
	 * @return 不在時検索不可の場合 true を返す。
	 */
	static boolean isSearchless(final String bind) {
		return bind.startsWith(":");
	}

	/**
	 * 項目不在時欠落判断
	 *
	 * @param bind bind文字列
	 * @return 項目不在時欠落時 true を返す。
	 */
	static boolean isDrop(final String bind) {
		return bind.startsWith("$");
	}

	/**
	 * 項目不在時デフォルト判断
	 *
	 * @param bind bind文字列
	 * @return 項目不在時デフォルト時 true を返す。
	 */
	static boolean isDefault(final String bind) {
		return bind.startsWith("?");
	}

	/**
	 * 項目存在時置換判断
	 *
	 * @param bind bind文字列
	 * @return 項目存在時置換時 true を返す。
	 */
	static boolean isReplace(final String bind) {
		return bind.startsWith("@");
	}

	/**
	 * 項目存在時置換判断
	 *
	 * @param cp コードポイント
	 * @return 項目存在時置換時 true を返す。
	 */
	static boolean isReplace(final int cp) {
		return cp == '@';
	}

	/**
	 * データタイプ判断
	 *
	 * @param val リテラル値
	 * @return 文字列の場合 true を返す。
	 */
	static boolean isString(final String val) {
		return val != null && val.startsWith("'");
	}

	/**
	 * データタイプ判断
	 *
	 * @param val リテラル値
	 * @return 日付の場合 true を返す。
	 */
	static boolean isDate(final String val) {
		return starts("TO_DATE", val);
	}

	/**
	 * データタイプ判断
	 *
	 * @param val リテラル値
	 * @return 日付の場合 true を返す。
	 */
	static boolean isTimestamp(final String val) {
		return starts("TO_TIMESTAMP", val);
	}

	/**
	 * 括弧判断
	 *
	 * @param val リテラル値
	 * @return 括弧の場合 true を返す。
	 */
	static boolean isEnclosed(final String val) {
		return val != null && val.startsWith("(") && val.endsWith(")");
	}

	/**
	 * 括弧内値取得
	 *
	 * @param val リテラル値
	 * @return 括弧内値
	 */
	default String getInnerValue(final String val) {
		return isEnclosed(val)
			? val.substring("(".length(), val.length() - ")".length()).trim() : val;
	}

	/**
	 * 日付化
	 *
	 * @param obj オブジェクト
	 * @return 日付
	 */
	default Date toDate(final Object obj) {
		Date ret = null;
		if (String.class.isInstance(obj)) {
			ret = toOptional(String.class.cast(obj)).map(DateUtil::toDate).orElse(null);
		} else if (Date.class.isInstance(obj)) {
			ret = DateUtil.toDate(Date.class.cast(obj));
		}
		return ret;
	}

	/**
	 * 日時化
	 *
	 * @param obj オブジェクト
	 * @param val リテラル値
	 * @return 日時
	 */
	default Date toTimestamp(final Object obj, final String val) {
		final Function<String, Timestamp> function = s -> {
			final var diff = (s.length() < getDateFormat(val).length())
					? getDateValue(val).substring(s.length()) : "";
			return DateUtil.toDateTime(s + diff);
		};

		Date ret = null;
		if (String.class.isInstance(obj)) {
			ret = toOptional(String.class.cast(obj)).map(function).orElse(null);
		} else if (Date.class.isInstance(obj)) {
			ret = DateUtil.toDateTime(Date.class.cast(obj));
		}
		return ret;
	}

	/**
	 * Optional化
	 *
	 * @param str 文字列
	 * @return Optional
	 */
	private Optional<String> toOptional(final String str) {
		final String ret;
		if (DateUtil.isAbsolute(str)) {
			ret = DateUtil.removeSeparator(DateUtil.normalizeDate(str));
		} else {
			ret = DateUtil.toAbsolute(str);
		}
		return Optional.ofNullable(ret);
	}

	/**
	 * 日付文字列取得
	 *
	 * @param str 取得対象文字列
	 * @return 日付文字列
	 */
	default String getDateValue(final String str) {
		var s = str.indexOf('\'');
		if (0 <= s) {
			s += "'".length();
			final var e = str.indexOf('\'', s);
			if (0 <= e) {
				return DateUtil.removeSeparator(str.substring(s, e));
			}
		}
		return "";
	}

	/**
	 * 日付フォーマット文字列取得
	 *
	 * @param str 取得対象文字列
	 * @return 日付フォーマット文字列
	 */
	default String getDateFormat(final String str) {
		final var e = str.lastIndexOf('\'', str.indexOf(')'));
		if (0 < e) {
			final var s = str.lastIndexOf('\'', str.offsetByCodePoints(e, -1));
			if (0 <= s) {
				return DateUtil.removeSeparator(str.substring(s + "'".length(), e));
			}
		}
		return "";
	}
}
