package common.sql;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import core.exception.PhysicalException;
import core.exception.ThrowableUtil;
import core.util.MapUtil;

/**
 * クエリユーティリティ
 *
 * @author Tadashi Nakayama
 */
public final class QueryUtil {

	/** ログ出力クラス */
	private static final Logger LOGGER = LogManager.getLogger(QueryUtil.class);

	/** クエリ格納用ハッシュ */
	private static final AtomicReference<SoftReference<ConcurrentMap<String, LineParsedNodeList>>>
					QUERIES = new AtomicReference<>(new SoftReference<>(null));

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

	/**
	 * LineParsedQuery取得
	 *
	 * @param qry クエリ
	 * @return LineParsedQuery
	 */
	public static LineParsedNodeList getLineParsedQuery(final String qry) {
		final var map = MapUtil.getCacheMap(QUERIES);
		final Supplier<LineParsedNodeList> supplier = () -> {
			final var pq = new LineParsedNodeList(qry);
			final var nl = map.putIfAbsent(qry, pq);
			return (nl != null) ? nl : pq;
		};
		return Objects.requireNonNullElseGet(map.get(qry), supplier);
	}

	/**
	 * クエリファイル変換
	 *
	 * @param val クエリ名（.セパレイト）
	 * @return クエリファイル(.sql)付き
	 */
	private static String toQueryFile(final String val) {
		final var sql = ".sql";
		return Optional.ofNullable(val).map(s ->
			(s.toLowerCase(Locale.ENGLISH).endsWith(sql))
				? s.substring(0, s.length() - sql.length()).replace(".", "/") + sql
				: s.replace(".", "/") + sql
		).orElse(val);
	}

	/**
	 * ファイルからSQL取得
	 *
	 * @param cls ファイル名と同一なクラス
	 * @return SQL
	 */
	public static String getSqlFromFile(final Class<?> cls) {
		return getSql(cls.getName());
	}

	/**
	 * ファイルからSQL取得
	 *
	 * @param name クエリ名
	 * @param cls ファイル名と同一なクラス
	 * @return SQL
	 */
	public static String getSqlFromFile(final String name, final Class<?> cls) {
		return getSql(cls.getName(), name);
	}

	/**
	 * ファイルからSQL取得
	 *
	 * @param name ファイル名（パッケージ付き）または、クエリ名（ファイル名付き）
	 * @return SQL
	 */
	public static String getSqlFromFile(final String name) {
		final var loc = name.lastIndexOf('#');
		return (0 <= loc)
			? getSql(name.substring(0, loc), name.substring(loc + "#".length())) : getSql(name);
	}

	/**
	 * SQL取得
	 *
	 * @param name ファイル名
	 * @return クエリ
	 */
	private static String getSql(final String name) {
		final var file = toQueryFile(name);
		final var cl = Thread.currentThread().getContextClassLoader();
		try (var stream = Files.lines(Paths.get(cl.getResource(file).toURI()), getCharset(file))) {
			return stream.map(String::trim).collect(Collectors.joining("\n"));
		} catch (final IOException | URISyntaxException ex) {
			LogManager.getLogger().error(ex.getMessage(), ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * SQL取得
	 *
	 * @param name ファイル名
	 * @param qry クエリ名
	 * @return クエリ
	 */
	private static String getSql(final String name, final String qry) {
		final var file = toQueryFile(name);
		final var cl = Thread.currentThread().getContextClassLoader();
		try (var stream = Files.lines(Paths.get(cl.getResource(file).toURI()), getCharset(file))) {
			final var found = new Boolean[1];
			final var mark = "-- " + qry + "=";
			final var ret = stream.map(String::trim).map(str -> selectLine(str, mark, found)).
							filter(Objects::nonNull).collect(Collectors.joining("\n"));
			if (ret.isEmpty()) {
				LogManager.getLogger().warn(name + "#" + qry + " is not found.");
			}
			return ret;

		} catch (final IOException | URISyntaxException ex) {
			LogManager.getLogger().error(ex.getMessage(), ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * 行選択
	 *
	 * @param str 行文字列
	 * @param mark 開始マーク文字列
	 * @param found 発見状態配列
	 * @return 選択しない場合 null を返す。
	 */
	private static String selectLine(final String str, final String mark, final Boolean[] found) {
		if (found[0] == null) {
			if (str.startsWith(mark)) {
				found[0] = Boolean.TRUE;
			}
		} else if (Boolean.TRUE.equals(found[0])) {
			if (str.endsWith(";")) {
				found[0] = Boolean.FALSE;
				return str.substring(0, str.length() - ";".length());
			} else if (str.isEmpty()) {
				found[0] = Boolean.FALSE;
			} else {
				return str;
			}
		}
		return null;
	}

	/**
	 * ステートメント作成
	 *
	 * @param qry クエリ
	 * @param param パラメタマップ
	 * @param creator ステートメントクリエーター
	 * @return ステートメント
	 * @throws SQLException SQL例外
	 */
	public static PreparedStatement createStatement(final String qry, final Map<String, ?> param,
			final StatementCreator creator) throws SQLException {
		// SQL文作成
		PreparedStatement psmt = null;
		try {
			if (!Objects.toString(qry, "").isEmpty()) {
				final var list = new ArrayList<>();

				final var query = toPrepareQuery(qry, param, list);
				psmt = creator.create(query);

				// パラメタ設定
				var i = 1;
				for (final var obj : list) {
					psmt.setObject(i++, obj);
				}

				LOGGER.debug(() -> "QUERY=" + toCompleteSql(query, list));
			}
			return psmt;
		} catch (final SQLException ex) {
			if (psmt != null) {
				try {
					psmt.close();
				} catch (final SQLException e) {
					ThrowableUtil.warn(e);
				}
			}
			throw ex;
		}
	}

	/**
	 * PrepareSQL変換
	 *
	 * @param qry 元クエリ
	 * @param map パラメタマップ
	 * @param list パラメタリスト（出力）
	 * @return PrepareSQL
	 */
	public static String toPrepareQuery(final String qry,
			final Map<String, ?> map, final List<Object> list) {
		final var m = Optional.ofNullable(map).orElse(Collections.emptyMap());
		return getLineParsedQuery(qry).build(m, list);
	}

	/**
	 * パラメタ埋込済SQL取得
	 *
	 * @param query 埋込前文字列
	 * @param params パラメタ
	 * @return 埋込済み文字列
	 */
	private static String toCompleteSql(final String query, final List<?> params) {
		final var sb = new StringBuilder();
		if (query != null) {
			for (int i = 0, s = 0, e = query.indexOf('?'); s < query.length();
					s = e + "?".length(), e = query.indexOf('?', s), i++) {
				if (e < 0) {
					sb.append(query, s, query.length());
					break;
				}

				sb.append(query, s, e);
				if (params != null && i < params.size()) {
					if (!Number.class.isInstance(params.get(i))) {
						sb.append("'");
						sb.append(params.get(i));
						sb.append("'");
					} else {
						sb.append(params.get(i));
					}
				}
			}
		}
		return sb.toString();
	}

	/**
	 * エンコード名取得
	 *
	 * @param name リソース名
	 * @return エンコード名
	 */
	private static Charset getCharset(final String name) {
		final var encoding = "--encoding:";
		final var cl = Thread.currentThread().getContextClassLoader();
		try (var is = cl.getResourceAsStream(name)) {
			final var bytes = new byte[128];
			if (is != null && 0 < is.read(bytes)) {
				final var str = new String(bytes, StandardCharsets.ISO_8859_1);
				if (str.startsWith(encoding)) {
					var end = str.indexOf('\n');
					if (0 < end) {
						final var e = str.offsetByCodePoints(end, -1);
						if (str.codePointAt(e) == '\r') {
							end = e;
						}
					}
					return Charset.forName(str.substring(encoding.length(), end));
				}
			}
			return StandardCharsets.UTF_8;

		} catch (final IOException ex) {
			LogManager.getLogger().error(ex.getMessage(), ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ステートメントクリエーター
	 *
	 * @author Tadashi Nakayama
	 */
	@FunctionalInterface
	public interface StatementCreator {
		/**
		 * PreparedStatement作成
		 *
		 * @param query クエリ
		 * @return PreparedStatement
		 * @throws SQLException SQL例外
		 */
		PreparedStatement create(String query) throws SQLException;
	}
}
