package common.db.dao.hibernate;

import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import org.hibernate.HibernateException;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;

import common.db.dao.Dao;
import common.db.dao.DaoSession;
import common.transaction.XATransaction;
import core.config.Factory;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;

/**
 * DAOセション
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class DaoSessionImpl implements DaoSession {

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

	/** 設定保存マップ */
	private final ConcurrentMap<String, Config> config = new ConcurrentHashMap<>();

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

	/**
	 * ファクトリメソッド
	 *
	 * @return インスタンス
	 */
	public static DaoSessionImpl getInstance() {
		if (INSTANCE.get() == null) {
			INSTANCE.compareAndSet(null, new DaoSessionImpl());
		}
		return INSTANCE.get();
	}

	/**
	 * DAO作成
	 *
	 * @param cname 設定名
	 * @return DAOオブジェクト
	 */
	@Override
	public Dao getDao(final String cname) {
		try {
			BaseDao bd = null;
			final var cfg = getConfig(cname);
			final var prop = cfg.getConfiguration().getProperties();
			if (prop != null) {
				if (prop.getProperty(AvailableSettings.JTA_PLATFORM) != null) {
					final var xa = Factory.create(XATransaction.class);
					if (xa.isInTransaction()) {
						bd = new JtaDao(cfg);
					} else {
						bd = new MainDao(cfg);
					}
					bd.setNoWait(isNoWaitAccess(prop));
				} else {
					bd = new MainDao(cfg);
				}
			}
			return bd;
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * 設定取得
	 *
	 * @param cname 設定名
	 * @return 設定
	 */
	private Config getConfig(final String cname) {
		final var key = toConfigName(cname);
		final Supplier<Config> supplier = () -> {
			final var cfr = new Configuration().configure(key);
			final var cfg = new Config(cfr, cfr.buildSessionFactory());
			final var cnf = this.config.putIfAbsent(key, cfg);
			return (cnf != null) ? cnf : cfg;
		};
		return Objects.requireNonNullElseGet(this.config.get(key), supplier);
	}

	/**
	 * 設定ファイル名化
	 *
	 * @param cname 設定名
	 * @return 設定ファイル名
	 */
	private String toConfigName(final String cname) {
		return Optional.ofNullable(cname).
				map(name -> name.startsWith("/") ? name : "/" + name).
				map(name -> name.endsWith(".cfg.xml") ? name : name + ".cfg.xml").
				orElse("/hibernate.cfg.xml");
	}

	/**
	 * NO WAIT アクセス判断
	 *
	 * @param prop Properties
	 * @return NO WAIT アクセスの場合 true を返す。
	 */
	private boolean isNoWaitAccess(final Properties prop) {
		return Boolean.parseBoolean(prop.getProperty("no_wait_access"));
	}
}
