/*
 * Copyright 2004-2014 the Seasar Foundation and the Others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

package org.seasar.extension.dbcp.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import javax.sql.XAConnection;
import javax.sql.XADataSource;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAResource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.seasar.extension.dbcp.ConnectionPool;
import org.seasar.extension.dbcp.ConnectionWrapper;
import org.seasar.extension.timer.TimeoutManager;
import org.seasar.extension.timer.TimeoutTarget;
import org.seasar.extension.timer.TimeoutTask;

/**
 * {@link ConnectionPool}の実装クラスです。
 *
 * @author higa
 *
 */
public class ConnectionPoolImpl implements ConnectionPool {

    /** デフォルトのトランザクション分離レベルです。 */
    public static final int DEFAULT_TRANSACTION_ISOLATION_LEVEL = -1;

    /** LOGGER */
    private static final Logger LOGGER = LogManager.getLogger(ConnectionPoolImpl.class);

    /** activePool */
    private final Set<ConnectionWrapper> activePool = new HashSet<>();
    /** txActivePool */
    private final Map<Transaction, ConnectionWrapper> txActivePool = new HashMap<>();
    /** freePool */
    private final LinkedList<FreeItem> freePool = new LinkedList<>();
    /** タイムアウトを管理 */
    private final TimeoutTask timeoutTask;

    /** XAデータソース */
    private XADataSource xaDataSource;
    /** トランザクションマネージャ */
    private TransactionManager transactionManager;
    /** 空きコネクションをクローズするまでのタイムアウト秒 */
    private int timeout = 600;
    /** コネクションをプールする上限 */
    private int maxPoolSize = 10;
    /** コネクションをプールする下限 */
    private int minPoolSize = 0;
    /** 空きコネクションを待機する上限をミリ秒 */
    private long maxWait = -1;
    /** トランザクション外でコネクションの取得を許すかどうか */
    private boolean allowLocalTx = true;
    /** 読み取り専用かどうか */
    private boolean readOnly = false;
    /** トランザクション分離レベル */
    private int transactionIsolationLevel = DEFAULT_TRANSACTION_ISOLATION_LEVEL;
    /** コネクションの死活を確認するクエリ */
    private String validationQuery;
    /** コネクションの死活を検証する間隔（ミリ秒） */
    private long validationInterval;

    /**
     * {@link ConnectionPoolImpl}を作成します。
     */
    public ConnectionPoolImpl() {
        this.timeoutTask = TimeoutManager.getInstance().addTimeoutTarget(
                TimeoutTarget::nop, Integer.MAX_VALUE, true);
    }

    /**
     * XAデータソースを返します。
     *
     * @return XAデータソース
     */
    public XADataSource getXADataSource() {
        return this.xaDataSource;
    }

    /**
     * XAデータソースを設定します。
     *
     * @param xads XAデータソース
     */
    public void setXADataSource(final XADataSource xads) {
        this.xaDataSource = xads;
    }

    /**
     * トランザクションマネージャを返します。
     *
     * @return トランザクションマネージャ
     */
    public TransactionManager getTransactionManager() {
        return this.transactionManager;
    }

    /**
     * トランザクションマネージャを設定します。
     *
     * @param manager トランザクションマネージャ
     */
    public void setTransactionManager(final TransactionManager manager) {
        this.transactionManager = manager;
    }

    /**
     * 空きコネクションをクローズするまでのタイムアウトを秒単位で返します。
     *
     * @return 空きコネクションをクローズするまでのタイムアウト(秒単位)
     */
    public int getTimeout() {
        return this.timeout;
    }

    /**
     * * 空きコネクションをクローズするまでのタイムアウトを秒単位で設定します。
     *
     * @param sec 空きコネクションをクローズするまでのタイムアウト(秒単位)
     */
    public void setTimeout(final int sec) {
        this.timeout = sec;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#getMaxPoolSize()
     */
    @Override
    public int getMaxPoolSize() {
        return this.maxPoolSize;
    }

    /**
     * コネクションをプールする上限を設定します。
     *
     * @param size コネクションをプールする上限
     */
    public void setMaxPoolSize(final int size) {
        this.maxPoolSize = size;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#getMinPoolSize()
     */
    @Override
    public int getMinPoolSize() {
        return this.minPoolSize;
    }

    /**
     * コネクションをプールする下限を設定します。
     *
     * @param size コネクションをプールする下限
     */
    public void setMinPoolSize(final int size) {
        this.minPoolSize = size;
    }

    /**
     * 空きコネクションを待機する上限をミリ秒単位で返します。
     *
     * @return 空きコネクションを待機する上限(ミリ秒単位)
     */
    public long getMaxWait() {
        return this.maxWait;
    }

    /**
     * 空きコネクションを待機する上限をミリ秒単位で設定します。
     * <p>
     * <code>-1</code> (デフォルト) だと無制限に待機します。 <code>0</code> だと待機しません。
     * </p>
     *
     * @param wait 空きコネクションを待機する上限 (ミリ秒単位)
     */
    public void setMaxWait(final long wait) {
        this.maxWait = wait;
    }

    /**
     * トランザクション外でコネクションの取得を許すかどうかを返します。
     *
     * @return トランザクション外でコネクションの取得を許すかどうか
     */
    public boolean isAllowLocalTx() {
        return this.allowLocalTx;
    }

    /**
     * トランザクション外でコネクションの取得を許すかどうかを設定します。
     *
     * @param allow トランザクション外でコネクションの取得を許すかどうか
     */
    public void setAllowLocalTx(final boolean allow) {
        this.allowLocalTx = allow;
    }

    /**
     * 読み取り専用かどうかを返します。
     *
     * @return 読み取り専用かどうか
     */
    public boolean isReadOnly() {
        return this.readOnly;
    }

    /**
     * 読み取り専用かどうかを設定します。
     *
     * @param bool 読み取り専用かどうか
     */
    public void setReadOnly(final boolean bool) {
        this.readOnly = bool;
    }

    /**
     * トランザクション分離レベルを設定します。
     *
     * @return トランザクション分離レベル
     */
    public int getTransactionIsolationLevel() {
        return this.transactionIsolationLevel;
    }

    /**
     * トランザクション分離レベルを設定します。
     *
     * @param level トランザクション分離レベル
     */
    public void setTransactionIsolationLevel(final int level) {
        this.transactionIsolationLevel = level;
    }

    /**
     * コネクションの死活を確認する検証用クエリを返します。
     *
     * @return 検証用クエリ
     */
    public String getValidationQuery() {
        return this.validationQuery;
    }

    /**
     * コネクションの死活を確認する検証用クエリを設定します。
     * <p>
     * <code>null</code>または空文字を指定した場合、検証は行われません。
     * </p>
     *
     * @param query 検証用クエリ
     */
    public void setValidationQuery(final String query) {
        this.validationQuery = query;
    }

    /**
     * コネクションの死活を検証する間隔（ミリ秒）を返します。
     *
     * @return 検証する間隔（ミリ秒）
     */
    public long getValidationInterval() {
        return this.validationInterval;
    }

    /**
     * コネクションの死活を検証する間隔（ミリ秒）を設定します。
     * <p>
     * <code>0</code>以下の値を指定した場合、検証は行われません。
     * </p>
     *
     * @param interval 検証する間隔（ミリ秒）
     */
    public void setValidationInterval(final long interval) {
        this.validationInterval = interval;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#getActivePoolSize()
     */
    @Override
    public int getActivePoolSize() {
        return this.activePool.size();
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#getTxActivePoolSize()
     */
    @Override
    public int getTxActivePoolSize() {
        return this.txActivePool.size();
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#getFreePoolSize()
     */
    @Override
    public int getFreePoolSize() {
        return this.freePool.size();
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#checkOut()
     */
    @Override
    public synchronized ConnectionWrapper checkOut() throws SQLException {
        final Transaction tx = getTransaction();
        if (tx == null && !isAllowLocalTx()) {
            throw new IllegalStateException("It is not Allow LocalTx.");
        }

        ConnectionWrapper con = getConnectionTxActivePool(tx);
        if (con != null) {
            LOGGER.debug(tx);
            return con;
        }

        long wait = this.maxWait;
        while (getMaxPoolSize() > 0
                && getActivePoolSize() + getTxActivePoolSize() >= getMaxPoolSize()) {
            if (wait == 0L) {
                throw new SQLException("wait is zero.");
            }
            final long startTime = System.currentTimeMillis();
            try {
                wait((this.maxWait == -1L) ? 0L : wait);
            } catch (final InterruptedException e) {
                throw new SQLException(e.getMessage());
            }
            final long elapseTime = System.currentTimeMillis() - startTime;
            if (wait > 0L) {
                wait -= Math.min(wait, elapseTime);
            }
        }

        con = checkOutFreePool(tx);
        if (con == null) {
            con = createConnection(tx);
        }
        if (tx == null) {
            setConnectionActivePool(con);
        } else {
            enlistResource(tx, con.getXAResource());
            registerSynchronization(tx, new SynchronizationImpl(() -> checkInTx(tx)));
            setConnectionTxActivePool(tx, con);
        }
        con.setReadOnly(this.readOnly);
        if (this.transactionIsolationLevel != DEFAULT_TRANSACTION_ISOLATION_LEVEL) {
            con.setTransactionIsolation(this.transactionIsolationLevel);
        }
        LOGGER.debug(tx);
        return con;
    }

    /**
     * トランザクション取得
     * @return トランザクション
     */
    private Transaction getTransaction() {
        return getTransaction(this.transactionManager);
    }

    /**
     * トランザクションを返します。
     *
     * @param tm トランザクションマネージャ
     * @return トランザクション
     */
    public static Transaction getTransaction(final TransactionManager tm) {
        try {
            return tm.getTransaction();
        } catch (final SystemException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * プールから取得
     * @param tx Transaction
     * @return コネクション
     */
    private ConnectionWrapper getConnectionTxActivePool(final Transaction tx) {
        return this.txActivePool.get(tx);
    }

    /**
     * チェックアウト
     * @param tx Transaction
     * @return コネクション
     */
    private ConnectionWrapper checkOutFreePool(final Transaction tx) {
        if (!this.freePool.isEmpty()) {
            final FreeItem item = this.freePool.removeLast();
            final ConnectionWrapper con = item.getConnection();
            con.init(tx);
            item.destroy();
            if (this.validationQuery == null || this.validationQuery.isEmpty()
                    || validateConnection(con, item.getPooledTime())) {
                return con;
            }
        }
        return null;
    }

    /**
     * validate
     *
     * @param con ConnectionWrapper
     * @param pooledTime プールされた時刻（ミリ秒）
     * @return boolean
     */
    private boolean validateConnection(final ConnectionWrapper con, final long pooledTime) {
        final long currentTime = System.currentTimeMillis();
        if (currentTime - pooledTime < this.validationInterval) {
            return true;
        }

        try {
            try (
                PreparedStatement ps = con.prepareStatement(this.validationQuery);
                ResultSet rs = ps.executeQuery()
            ) {
                rs.next();
            }
            return true;
        } catch (final SQLException e) {
            try {
                con.close();
            } catch (final SQLException ex) {
                LOGGER.info(ex.getMessage());
            }

            for (final FreeItem item : this.freePool) {
                try {
                    item.getConnection().closeReally();
                } catch (final RuntimeException ex) {
                    LOGGER.info(ex.getMessage());
                }
            }
            this.freePool.clear();

            LOGGER.info(e.getMessage());
            return false;
        }
    }

    /**
     * 物理的なコネクション取得
     * @param tx Transaction
     * @return 物理的なコネクション
     * @throws SQLException SQL例外
     */
    private ConnectionWrapper createConnection(final Transaction tx) throws SQLException {
        final XAConnection xaConnection = this.xaDataSource.getXAConnection();
        final Connection connection = xaConnection.getConnection();
        final ConnectionWrapper con = new ConnectionWrapperImpl(xaConnection, connection, this, tx);
        LOGGER.debug("物理的なコネクションを取得しました");
        return con;
    }

    /**
     *
     * @param tx Transaction
     * @param conn ConnectionWrapper
     */
    private void setConnectionTxActivePool(final Transaction tx, final ConnectionWrapper conn) {
        this.txActivePool.put(tx, conn);
    }

    /**
     *
     * @param connection ConnectionWrapper
     */
    private void setConnectionActivePool(final ConnectionWrapper connection) {
        this.activePool.add(connection);
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool
     * #release(org.seasar.extension.dbcp.ConnectionWrapper)
     */
    @Override
    public synchronized void release(final ConnectionWrapper connection) {
        this.activePool.remove(connection);
        final Transaction tx = getTransaction();
        if (tx != null) {
            this.txActivePool.remove(tx);
        }
        releaseInternal(connection);
    }

    /**
     * releaseInternal
     * @param connection ConnectionWrapper
     */
    private void releaseInternal(final ConnectionWrapper connection) {
        connection.closeReally();
        notify();
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool
     * #checkIn(org.seasar.extension.dbcp.ConnectionWrapper)
     */
    @Override
    public synchronized void checkIn(final ConnectionWrapper connection) {
        this.activePool.remove(connection);
        checkInFreePool(connection);
    }

    /**
     * @param conn ConnectionWrapper
     */
    private void checkInFreePool(final ConnectionWrapper conn) {
        if (conn == null) {
            return;
        }

        if (getMaxPoolSize() > 0) {
            try {
                final Connection pc = conn.getPhysicalConnection();
                try {
                    pc.setAutoCommit(true);
                } catch (final SQLException e) {
                    releaseInternal(conn);
                    throw e;
                }
                final ConnectionWrapper newCon = new ConnectionWrapperImpl(
                        conn.getXAConnection(), pc, this, null);
                conn.cleanup();
                this.freePool.addLast(
                        new FreeItem(newCon, this.timeout, this.minPoolSize, this.freePool));
                notify();
            } catch (final SQLException e) {
                throw new RuntimeException(e);
            }
        } else {
            conn.closeReally();
        }
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#checkInTx(javax.transaction.Transaction)
     */
    @Override
    public synchronized void checkInTx(final Transaction tx) {
        if (tx != null && getTransaction() == null) {
            checkInFreePool(this.txActivePool.remove(tx));
        }
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionPool#close()
     */
    @Override
    public synchronized void close() {
        for (final FreeItem item : this.freePool) {
            item.getConnection().closeReally();
            item.destroy();
        }
        this.freePool.clear();

        for (final ConnectionWrapper connectionWrapper : this.txActivePool.values()) {
            connectionWrapper.closeReally();
        }
        this.txActivePool.clear();

        for (final ConnectionWrapper connectionWrapper : this.activePool) {
            connectionWrapper.closeReally();
        }
        this.activePool.clear();

        this.timeoutTask.cancel();
    }

    /**
     * トランザクションに参加します。
     *
     * @param tx Transaction
     * @param xaResource XAResource
     */
    private static void enlistResource(final Transaction tx, final XAResource xaResource) {
        try {
            tx.enlistResource(xaResource);
        } catch (final SystemException | RollbackException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * {@link Synchronization}を登録します。
     *
     * @param tx Transaction
     * @param sync Synchronization
     */
    private static void registerSynchronization(final Transaction tx, final Synchronization sync) {
        try {
            tx.registerSynchronization(sync);
        } catch (final SystemException | RollbackException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * FreeItem
     *
     */
    private class FreeItem implements TimeoutTarget {

        /** connectionWrapper */
        private final ConnectionWrapper connectionWrapper;
        /** TimeoutTask */
        private final TimeoutTask task;
        /** pooledTime */
        private final long pooledTime;
        /** コネクションをプールする下限 */
        private final int minSize;
        /** freePool */
        private final LinkedList<FreeItem> pool;

        /**
         * Constructor
         * @param wrapper ConnectionWrapper
         * @param sec timeout
         * @param size minPoolSize
         * @param list freePool
         */
        FreeItem(final ConnectionWrapper wrapper, final int sec,
                final int size, final LinkedList<FreeItem> list) {
            this.connectionWrapper = wrapper;
            this.task = TimeoutManager.getInstance().addTimeoutTarget(this, sec, false);
            this.pooledTime = System.currentTimeMillis();
            this.minSize = size;
            this.pool = list;
        }

        /**
         * コネクションを返します。
         *
         * @return コネクション
         */
        public ConnectionWrapper getConnection() {
            return this.connectionWrapper;
        }

        /**
         * プールされた時刻（ミリ秒）を返します。
         *
         * @return プールされた時刻（ミリ秒）
         */
        public long getPooledTime() {
            return this.pooledTime;
        }

        /**
         * @see org.seasar.extension.timer.TimeoutTarget#expired()
         */
        @Override
        public void expired() {
            synchronized (ConnectionPoolImpl.this) {
                if (this.pool.size() <= this.minSize) {
                    return;
                }
                this.pool.remove(this);
            }
            synchronized (this) {
                if (this.connectionWrapper != null) {
                    this.connectionWrapper.closeReally();
                }
            }
        }

        /**
         * 破棄します。
         */
        public synchronized void destroy() {
            if (this.task != null) {
                this.task.cancel();
            }
        }
    }

    /**
     * {@link Synchronization}の実装です。
     *
     * @author koichik
     */
    private static final class SynchronizationImpl implements Synchronization {

        /** Runnable */
        private final Runnable runnable;

        /**
         * インスタンスを構築します。
         *
         * @param val Runnable
         */
        SynchronizationImpl(final Runnable val) {
            this.runnable = val;
        }

        /**
         * @see javax.transaction.Synchronization#beforeCompletion()
         */
        @Override
        public void beforeCompletion() {
            return;
        }

        /**
         * @see javax.transaction.Synchronization#afterCompletion(int)
         */
        @Override
        public void afterCompletion(final int status) {
            if (status == Status.STATUS_COMMITTED || status == Status.STATUS_ROLLEDBACK) {
                this.runnable.run();
            }
        }
    }
}
