package project.common.db;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

import common.db.dao.Dao.JdbcWork;
import common.db.dao.DaoUtil;
import common.db.dao.hibernate.EntityUtil;
import common.db.dao.hibernate.HibernateJdbcWork;
import common.db.jdbc.Jdbc;
import core.config.Factory;
import core.exception.ThrowableUtil;

/**
 * バッチインサートワーク
 * @author Tadashi Nakayama
 */
public final class InsertWork extends HibernateJdbcWork implements JdbcWork {
	/** モデルクラス */
	private final Class<? extends Serializable> cls;
	/** 作成データリスト */
	private final List<?> list;
	/** 取得メソッド */
	private final List<Method> method;
	/** アイテム名 */
	private final List<String> item;

	/**
	 * コンストラクタ
	 * @param c 作成モデルクラス
	 * @param l 作成データ
	 */
	public InsertWork(final Class<? extends Serializable> c, final List<?> l) {
		this.cls = c;
		this.list = l;

		final var m = new ArrayList<Method>();
		final var i = new ArrayList<String>();
		for (final var mt : c.getMethods()) {
			if (Factory.isGetter(mt)) {
				m.add(mt);
				i.add(Factory.toItemName(mt));
			}
		}
		this.method = Collections.unmodifiableList(m);
		this.item = Collections.unmodifiableList(i);
	}

	/**
	 * @see common.db.dao.Dao.JdbcWork#execute(java.sql.Connection)
	 */
	@Override
	public void execute(final Connection conn) throws SQLException {
		if (this.list != null && !this.list.isEmpty()) {
			final var dynamic = EntityUtil.isDynamicInsert(this.cls);
			try (var psmt = Jdbc.wrap(conn).prepareStatement(createInsertQuery());
					var pssq = getSequenceStatement(conn)) {
				var i = 0;
				for (final var obj : this.list) {
					setSequence(pssq, obj);
					addBatch(psmt, obj, dynamic);
					i++;
					if (i == 30000) {
						psmt.executeBatch();
						i = 0;
					}
				}
				if (0 < i) {
					psmt.executeBatch();
				}
			}
		}
	}

	/**
	 * インサートクエリ作成
	 * @return インサートクエリ
	 */
	private String createInsertQuery() {
		final var column = new StringJoiner(", ");
		final var value = new StringJoiner(", ");
		for (final var m : this.method) {
			column.add(DaoUtil.getColumnName(m));
			value.add("?");
		}
		return "INSERT INTO " + DaoUtil.getTableName(this.cls)
			+ "(" + column.toString() + ") VALUES(" + value.toString() + ")";
	}

	/**
	 * ID用ステートメント
	 * @param conn コネクション
	 * @return ステートメント
	 */
	private PreparedStatement getSequenceStatement(final Connection conn) {
		if (!DaoUtil.isEmbeddedId(this.cls) && !DaoUtil.getSequenceName(this.cls).isEmpty()) {
			try {
				return Jdbc.wrap(conn).prepareStatement(
					getSequenceNextValString(DaoUtil.getSequenceName(this.cls)));
			} catch (final SQLException ex) {
				ThrowableUtil.error(ex);
			}
		}
		return null;
	}

	/**
	 * シーケンス設定
	 * @param psmt ステートメント
	 * @param obj 設定オブジェクト
	 * @throws SQLException SQL例外
	 */
	private void setSequence(final PreparedStatement psmt, final Object obj) throws SQLException {
		if (psmt != null) {
			try (var rs = psmt.executeQuery()) {
				if (rs.next()) {
					setId(obj, rs.getLong(1));
				}
			}
		}
	}

	/**
	 * バインド値設定
	 * @param psmt PreparedStatement
	 * @param obj バインド値保持オブジェクト
	 * @param dynamic ダイナミックフラグ
	 * @throws SQLException SQL例外
	 */
	private void addBatch(final PreparedStatement psmt, final Object obj,
			final boolean dynamic) throws SQLException {
		for (var i = 0; i < this.method.size(); i++) {
			psmt.setObject(i + 1, getObject(i, obj, dynamic));
		}
		psmt.addBatch();
	}

	/**
	 * バインド値取得
	 * @param loc アイテム位置
	 * @param obj 値保持オブジェクト
	 * @param dynamic ダイナミックフラグ
	 * @return バインド値
	 */
	private Object getObject(final int loc, final Object obj, final boolean dynamic) {
		var o = getValue(this.method.get(loc), this.item.get(loc), obj);
		if (o == null && dynamic) {
			final var md = Factory.create(DBMetaData.class);
			final var map = md.getColumnInfo(DaoUtil.getTableName(this.cls));
			final var mi = map.get(DaoUtil.getColumnName(this.method.get(loc)));
			if (mi != null) {
				o = mi.getDefault();
			}
		}
		return o;
	}

	/**
	 * バインド値
	 * @param mt 取得メソッド
	 * @param name 取得名
	 * @param obj 値保持オブジェクト
	 * @return バインド値
	 */
	private Object getValue(final Method mt, final String name, final Object obj) {
		if (Map.class.isInstance(obj)) {
			final Map<String, Object> map = Factory.cast(obj);
			return map.get(name);
		}
		return Factory.invoke(obj, mt);
	}

	/**
	 * ID設定
	 * @param obj 設定オブジェクト
	 * @param val 設定値
	 */
	private void setId(final Object obj, final Long val) {
		if (Map.class.isInstance(obj)) {
			final Map<String, Object> map = Factory.cast(obj);
			map.put("Id", val);
		} else {
			final var m = Factory.getMethod(this.cls, "setId", Long.class);
			Factory.invoke(obj, m, val);
		}
	}
}
