package common.sql;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * パースクエリ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
final class LineParsedNodeList implements LineParsedNode {

	/** クエリリスト */
	private final List<LineParsedNode> qlist;
	/** ノード */
	private final String node;

	/**
	 * コンストラクタ
	 *
	 * @param val クエリ
	 */
	LineParsedNodeList(final String val) {
		this.qlist = parseQuery(new ListParser(val));
		this.node = null;
	}

	/**
	 * コンストラクタ
	 *
	 * @param val クエリ
	 * @param nd ノード
	 */
	private LineParsedNodeList(final ListParser val, final String nd) {
		this.qlist = parseQuery(val);
		this.node = nd;
	}

	/**
	 * パース処理
	 *
	 * @param qp クエリパーサ
	 * @return パース結果リスト
	 */
	private List<LineParsedNode> parseQuery(final ListParser qp) {
		final List<LineParsedNode> ret = new ArrayList<>();

		for (String str = qp.getNextLine(); str != null; str = qp.getNextLine()) {
			if (qp.getType() == ListParser.START) {
				ret.add(new LineParsedNodeList(qp, str.trim()));
			} else if (qp.getType() == ListParser.END) {
				break;
			} else if (qp.getType() == ListParser.ITEM) {
				ret.add(new LineParsedNodeItem(str.trim()));
			} else {
				ret.add(new LineParsedNodeString(str.trim()));
			}
		}

		return ret;
	}

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

		final StringBuilder sb = new StringBuilder();
		String prev = null;
		for (final LineParsedNode pi : this.qlist) {
			if (pi.isTarget(pmap)) {
				String str = pi.build(pmap, plist).trim();
				if (!str.isEmpty()) {
					if (prev != null) {
						final int ret = isValid(prev, str);
						if (0 <= ret) {
							sb.append(prev).append(" \n");
							str = str.substring(ret);
						}
					}
					prev = str;
				}
			}
		}

		if (prev != null && isValid(prev, "") == 0) {
			sb.append(prev).append(" \n");
		}

		return sb.toString();
	}

	/**
	 * ターゲット判断
	 * @param pmap パラメタマップ
	 * @return ターゲットの場合 true を返す。
	 */
	@Override
	public boolean isTarget(final Map<String, ?> pmap) {
		if (this.node != null) {
			final String[] item = this.node.split(" ");

			final boolean not = this.node.startsWith("!");
			final Object obj = pmap.get(not ? item[0].substring("!".length()) : item[0]);

			if (item.length == 1) {
				return not ^ LineParsedNode.isValid(obj);
			}

			item[0] = null;
			return not ^ isValidList(obj, Arrays.asList(item));
		}
		return true;
	}

	/**
	 * 有効確認
	 * @param obj ターゲット値
	 * @param vals 値
	 * @return 有効の場合 true を返す。
	 */
	private boolean isValidList(final Object obj, final List<String> vals) {
		final Predicate<Object[]> contains =
				array -> Stream.of(array).filter(Objects::nonNull).
						map(Objects::toString).anyMatch(vals::contains);

		if (obj != null) {
			if (List.class.isInstance(obj)) {
				final List<?> list = List.class.cast(obj);
				return contains.test(list.toArray(new Object[list.size()]));
			} else if (obj.getClass().isArray()) {
				return contains.test(Object[].class.cast(obj));
			} else if (!Collection.class.isInstance(obj)) {
				return contains.test(new Object[]{obj});
			}
		}
		return false;
	}

	/**
	 * 適正確認
	 *
	 * @param prev 前文字列
	 * @param next 次文字列
	 * @return 前文字列と次文字列が有効の場合 0 を返す。
	 * 前文字列が不要の場合 -1 を返す。
	 * 次文字列が不要の場合 0より大きい不要文字数を返す。
	 */
	private int isValid(final String prev, final String next) {
		if (LineParsedNode.ends("WHERE", prev)) {
			if (next.isEmpty()
					|| LineParsedNode.starts(")", next)
					|| LineParsedNode.starts("HAVING ", next)
					|| LineParsedNode.starts("ORDER BY", next)
					|| LineParsedNode.starts("LIMIT", next)
					|| LineParsedNode.starts("OFFSET", next)
					|| LineParsedNode.starts("GROUP BY", next)) {
				return -1;
			}

			if (LineParsedNode.starts("AND ", next)) {
				return 4;
			}

			if (LineParsedNode.starts("OR ", next)) {
				return 3;
			}
		}
		return 0;
	}

	/**
	 * クエリパース
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	private static final class ListParser {
		/** 行タイプ 通常 */
		public static final int NORMAL = 1;
		/** 行タイプ 開始 */
		public static final int START = 2;
		/** 行タイプ 終了 */
		public static final int END = 3;
		/** 行タイプ アイテムあり */
		public static final int ITEM = 4;

		/** クエリ文字列 */
		private final String qry;

		/** 読込位置 */
		private int loc = 0;
		/** 行タイプ */
		private int type = NORMAL;
		/** コメント状態 0:コメント外 1:ラインコメント内 2:ブロックコメント内 3:バインドブロックコメント内 */
		private int comment = 0;

		/**
		 * コンストラクタ
		 *
		 * @param query クエリ文字列
		 */
		ListParser(final String query) {
			this.qry = query;
		}

		/**
		 * タイプ取得
		 *
		 * @return タイプ
		 */
		public int getType() {
			return this.type;
		}

		/**
		 * 次行取得
		 *
		 * @return 次行
		 */
		public String getNextLine() {

			if (this.qry.length() <= this.loc) {
				return null;
			}

			this.type = NORMAL;
			this.comment = 0;
			boolean quote = false;
			final StringBuilder sb = new StringBuilder();
			while (this.loc < this.qry.length()) {
				final int ch = this.qry.codePointAt(this.loc);
				this.loc = this.qry.offsetByCodePoints(this.loc, 1);
				if (ch == '\r') {
					if (doCr(sb)) {
						break;
					}
				} else if (ch == '\n') {
					if (doLf(sb)) {
						break;
					}
				} else {
					if (ch == '*') {
						if (doAsterisk(sb)) {
							continue;
						}
					}

					if (this.type == NORMAL || this.type == ITEM) {
						if (this.comment == 1 || this.comment == 2) {
							continue;
						}

						if (ch == '\'') {
							quote = doQuote(sb, quote);
						}

						if (!quote && ch == '-') {
							if (doHyphen()) {
								continue;
							}
						} else if (!quote && ch == '/') {
							if (doSlash()) {
								continue;
							}
						}
					}

					sb.appendCodePoint(ch);
				}
			}

			return sb.toString();
		}

		/**
		 * CR処理
		 * @param sb StringBuilder
		 * @return 制御変更有無
		 */
		private boolean doCr(final StringBuilder sb) {
			if (!isNextLf()) {
				if (this.comment == 1) {
					this.comment = 0;
				}
				if (0 < sb.length()) {
					return true;
				}
			}
			return false;
		}

		/**
		 * LF処理
		 * @param sb StringBuilder
		 * @return 制御変更有無
		 */
		private boolean doLf(final StringBuilder sb) {
			if (this.comment == 1) {
				this.comment = 0;
			}
			return 0 < sb.length() || this.type == START || this.type == END;
		}

		/**
		 * アスタリスク処理
		 * @param sb StringBuilder
		 * @return 制御変更有無
		 */
		private boolean doAsterisk(final StringBuilder sb) {
			if (isEndComment() && this.comment >= 2) {
				this.loc += "/".length();
				if (this.comment == 3) {
					sb.append(skip());
				}
				this.comment = 0;
				return true;
			}
			return false;
		}

		/**
		 * クウォート処理
		 * @param sb StringBuilder
		 * @param quote クウォートフラグ
		 * @return クウォートフラグ
		 */
		private boolean doQuote(final StringBuilder sb, final boolean quote) {
			if (!isEscaped()) {
				return !quote;
			}

			this.loc += "'".length();
			sb.append("'");
			return quote;
		}

		/**
		 * ハイフン処理
		 * @return 制御変更有無
		 */
		private boolean doHyphen() {
			if (isLineComment()) {
				this.comment = 1;
				if (setLineType()) {
					this.loc += "--".length();
				}
				return true;
			}
			return false;
		}

		/**
		 * スラッシュ処理
		 * @return 制御変更有無
		 */
		private boolean doSlash() {
			if (isBeginComment()) {
				if (setLineType()) {
					this.comment = 3;
				} else {
					this.comment = 2;
				}
				this.loc += "*".length();
				return true;
			}
			return false;
		}

		/**
		 * 読み飛ばし
		 * @return 読み飛ばし文字列
		 */
		private String skip() {
			final String sub = this.qry.substring(this.loc);
			if (LineParsedNode.isDate(sub) || LineParsedNode.isTimestamp(sub)) {
				return SEPARATOR + getSkipString(false, ')');
			} else if (this.qry.codePointAt(this.loc) == '\'') {
				this.loc += "'".length();
				return SEPARATOR + "'" + getSkipString(false, '\'');
			}
			return SEPARATOR + getSkipString(true,
					'-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
		}

		/**
		 * 読み飛ばし文字列取得
		 * @param not true の場合含む間の文字列　falseの場合含むまでの文字列
		 * @param target 対象文字配列
		 * @return 読み飛ばし文字列
		 */
		private String getSkipString(final boolean not, final int... target) {
			final StringBuilder sb = new StringBuilder();
			while (this.loc < this.qry.length()) {
				final int val = this.qry.codePointAt(this.loc);

				final boolean found = 0 <= Arrays.binarySearch(target, val);
				if (!found && not) {
					break;
				}

				sb.appendCodePoint(val);
				this.loc = this.qry.offsetByCodePoints(this.loc, 1);

				if (found && !not) {
					break;
				}
			}
			return sb.toString();
		}

		/**
		 * 次文字LF判断
		 *
		 * @return 次文字がLFの場合 true を返す。
		 */
		private boolean isNextLf() {
			return this.loc < this.qry.length() && this.qry.codePointAt(this.loc) == '\n';
		}

		/**
		 * コメント判断
		 *
		 * @return コメントの場合 true を返す。
		 */
		private boolean isLineComment() {
			return this.loc < this.qry.length() && this.qry.codePointAt(this.loc) == '-';
		}

		/**
		 * コメント判断
		 *
		 * @return コメントの場合 true を返す。
		 */
		private boolean isBeginComment() {
			return this.loc < this.qry.length() && this.qry.codePointAt(this.loc) == '*';
		}

		/**
		 * コメント判断
		 *
		 * @return コメントの場合 true を返す。
		 */
		private boolean isEndComment() {
			return this.loc < this.qry.length() && this.qry.codePointAt(this.loc) == '/';
		}

		/**
		 * クォートエスケープ判断
		 * @return エスケープの場合 true を返す。
		 */
		private boolean isEscaped() {
			return this.loc < this.qry.length() && this.qry.codePointAt(this.loc) == '\'';
		}

		/**
		 * 行タイプ設定
		 * @return 行タイプが確定した場合 true を返す。
		 */
		private boolean setLineType() {
			if (isStart()) {
				this.type = START;
				return true;
			} else if (isEnd()) {
				this.type = END;
				return true;
			} else if (isItem()) {
				this.type = ITEM;
				return true;
			}
			return false;
		}

		/**
		 * 開始判断
		 *
		 * @return 開始(--<)の場合 true を返す。
		 */
		private boolean isStart() {
			final int n = this.loc + "-".length();
			return n < this.qry.length() && this.qry.codePointAt(n) == '<';
		}

		/**
		 * 終了判断
		 *
		 * @return 終了(-->)の場合 true を返す。
		 */
		private boolean isEnd() {
			final int n = this.loc + "-".length();
			return n < this.qry.length() && this.qry.codePointAt(n) == '>';
		}

		/**
		 * ITEM開始判断
		 * @return アイテム開始の場合 true を返す。
		 */
		private boolean isItem() {
			final int n = this.loc + "*".length();
			return n < this.qry.length()
					&& LineParsedNode.isBindCharacter(this.qry.codePointAt(n));
		}
	}
}
