package project.common.db;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

import common.db.JdbcSource;
import common.db.jdbc.Jdbc;
import common.sql.QueryUtil;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;

/**
 * DBメタ情報保持実装
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class DBMetaDataImpl implements DBMetaData {

	/** インスタンス */
	private static final AtomicReference<DBMetaDataImpl> INSTANCE = new AtomicReference<>();

	/** テーブル情報保存オブジェクト */
	private final ConcurrentMap<String, String> tableInfo = new ConcurrentHashMap<>();
	/** テーブル情報保存オブジェクト */
	private final ConcurrentMap<String, Map<String, DBColumnInfo>> columnInfo =
					new ConcurrentHashMap<>();

	/**
	 * コンストラクタ
	 */
	private DBMetaDataImpl() {
		if (INSTANCE.get() != null) {
			throw new AssertionError();
		}
	}

	/**
	 * インスタンス取得
	 *
	 * @return インスタンス
	 */
	public static DBMetaDataImpl getInstance() {
		if (INSTANCE.get() == null) {
			INSTANCE.compareAndSet(null, new DBMetaDataImpl());
		}
		return INSTANCE.get();
	}

	/**
	 * テーブルコメント取得
	 * @return テーブルコメント
	 */
	@Override
	public String getTableComment(final String table) {
		return getTableComment(table, null);
	}

	/**
	 * テーブルコメント取得
	 * @return テーブルコメント
	 */
	@Override
	public String getTableComment(final String table, final String name) {
		final var key = table.toUpperCase(Locale.ENGLISH);
		var ret = this.tableInfo.get(key);
		if (ret == null) {
			setMetaInfo(table, name);
			ret = this.tableInfo.get(key);
		}
		return ret;
	}

	/**
	 * テーブル情報取得
	 *
	 * @param table テーブル名
	 * @return テーブル情報
	 */
	@Override
	public Map<String, DBColumnInfo> getColumnInfo(final String table) {
		return getColumnInfo(table, null);
	}

	/**
	 * テーブル情報取得
	 *
	 * @param table テーブル名
	 * @param name 接続名
	 * @return テーブル情報
	 */
	@Override
	public Map<String, DBColumnInfo> getColumnInfo(final String table, final String name) {
		final var key = table.toUpperCase(Locale.ENGLISH);
		var ret = this.columnInfo.get(key);
		if (ret == null) {
			setMetaInfo(table, name);
			ret = this.columnInfo.get(key);
		}
		return ret;
	}

	/**
	 * テーブルメタ情報取得
	 *
	 * @param table テーブル名
	 * @param name 接続名
	 */
	private void setMetaInfo(final String table, final String name) {
		try (var conn = JdbcSource.getConnection(name)) {
			final var tbl = table.toUpperCase(Locale.ENGLISH);
			// タイプ
			final var type = new LinkedHashMap<String, DBColumnInfo>();
			setColumns(conn, tbl, type);
			if (type.isEmpty()) {
				setColumns(conn, table.toLowerCase(Locale.ENGLISH), type);
			}
			this.columnInfo.putIfAbsent(tbl, Collections.unmodifiableMap(type));

			// キー
			var comm = getComment(conn, table);
			if (Objects.toString(comm, "").isEmpty()) {
				comm = getComment(conn, table.toLowerCase(Locale.ENGLISH));
			}
			this.tableInfo.putIfAbsent(tbl, comm);

		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * カラム情報取得
	 * @param conn コネクション
	 * @param table テーブル名
	 * @param type 設定マップ
	 * @throws SQLException SQL例外
	 */
	private void setColumns(final Connection conn, final String table,
			final Map<String, DBColumnInfo> type) throws SQLException {

		final var dmd = conn.getMetaData();
		try (var rs = dmd.getColumns(null, null, table, "%")) {
			var pos = 0;
			while (rs.next()) {
				if (rs.getInt("ORDINAL_POSITION") < pos) {
					break;
				}
				pos = rs.getInt("ORDINAL_POSITION");

				type.put(rs.getString("COLUMN_NAME"), getMetaInfo(conn, table, rs));
			}
		}
	}

	/**
	 * カラム情報取得
	 *
	 * @param conn コネクション
	 * @param table テーブル名
	 * @param rs 結果セット
	 * @return カラム情報
	 * @throws SQLException SQL例外
	 */
	private DBColumnInfo getMetaInfo(final Connection conn, final String table,
			final ResultSet rs) throws SQLException {
		return new DBColumnInfo(rs.getInt("DATA_TYPE"), rs.getInt("COLUMN_SIZE"),
				rs.getInt("NULLABLE") == DatabaseMetaData.procedureNoNulls,
				getColumnComment(conn, table, rs.getString("COLUMN_NAME")),
				rs.getInt("DECIMAL_DIGITS"), rs.getString("COLUMN_DEF"));
	}

	/**
	 * テーブルコメント取得
	 * @param conn コネクション
	 * @param table テーブル名
	 * @return テーブルコメント
	 * @throws SQLException SQL例外
	 */
	private String getComment(final Connection conn,
			final String table) throws SQLException {
		final var query = QueryUtil.getSqlFromFile("SelectTable", this.getClass());
		try (var psmt = QueryUtil.createStatement(query, Collections.singletonMap("Relname", table),
				Jdbc.wrap(conn)::readonlyStatement)) {
			try (var rs = psmt.executeQuery()) {
				if (rs.next()) {
					return rs.getString("COMMENT");
				}
			}
			return "";
		}
	}

	/**
	 * カラムコメント取得
	 * @param conn コネクション
	 * @param table テーブル名
	 * @param col カラム名
	 * @return カラムコメント
	 * @throws SQLException SQL例外
	 */
	private String getColumnComment(final Connection conn, final String table,
			final String col) throws SQLException {
		final var map = new HashMap<String, Object>();
		map.put("Relname", table);
		map.put("Attname", col);

		final var query = QueryUtil.getSqlFromFile("SelectComment", this.getClass());
		try (var psmt = QueryUtil.createStatement(query, map,
				Jdbc.wrap(conn)::readonlyStatement)) {
			try (var rs = psmt.executeQuery()) {
				if (rs.next()) {
					return rs.getString("COMMENT");
				}
			}
			return col;
		}
	}
}
