package online.jpa.provider;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;

import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Unbox;
import org.hibernate.HibernateException;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.service.UnknownUnwrapTypeException;
import org.hibernate.service.spi.Configurable;
import org.hibernate.service.spi.Stoppable;

/**
 * DbcpConnectionProvider
 *
 * @author Tadashi Nakayama
 */
public class DbcpConnectionProvider implements ConnectionProvider, Configurable, Stoppable {

    /** serialVersionUID */
    private static final long serialVersionUID = 1L;

    /** PREFIX */
    private static final String PREFIX = "hibernate.dbcp.";

    /** AutoCommit */
    private static final String AUTOCOMMIT = "hibernate.connection.autocommit";

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

    /** BasicDataSource */
    private transient BasicDataSource ds;

    /**
     * @see org.hibernate.service.spi.Configurable#configure(java.util.Map)
     */
    @Override
    public void configure(final Map props) {

        // DBCP properties used to create the BasicDataSource
        final Map<String, String> propMap = ((Map<?, ?>) props).entrySet().stream().collect(
            Collectors.toMap(
                e -> Objects.toString(e.getKey(), null),
                e -> Objects.toString(e.getValue(), null)
           )
        );

        final Properties dbcpProperties = setToProperties(propMap);

        // Some debug info
        LOGGER.debug("DBCP factory properties:",
            () -> {
                final var sw = new StringWriter();
                dbcpProperties.list(new PrintWriter(sw, true));
                return sw.toString();
            }
        );

        try {
            // Let the factory create the pool
            this.ds = BasicDataSourceFactory.createDataSource(dbcpProperties);

            // The BasicDataSource has lazy initialization
            // borrowing a connection will start the DataSource
            // and make sure it is configured correctly.
            try (Connection conn = this.ds.getConnection()) {
                LOGGER.info("connection info: ", conn);
            }

            // Log pool statistics before continuing.
            logStatistics();

        } catch (final Exception e) {
            final var message = "Could not create a DBCP pool";
            LOGGER.error(message, e);
            if (this.ds != null) {
                try {
                    this.ds.close();
                } catch (final SQLException ex) {
                    LOGGER.warn(ex.getMessage());
                }
                this.ds = null;
            }
            throw new HibernateException(message, e);
        }
    }

    /**
     * get Properties to create the BasicDataSource
     *
     * @param propMap Map
     * @return Properties
     */
    private Properties setToProperties(final Map<String, String> propMap) {
        final var dbcpProperties = new Properties();

        // DriverClass & url
        final var jdbcDriverClass = propMap.get(AvailableSettings.DRIVER);
        final var jdbcUrl = propMap.get(AvailableSettings.URL);
        dbcpProperties.put("driverClassName", jdbcDriverClass);
        dbcpProperties.put("url", jdbcUrl);

        // Username / password
        final var username = propMap.get(AvailableSettings.USER);
        final var password = propMap.get(AvailableSettings.PASS);
        dbcpProperties.put("username", username);
        dbcpProperties.put("password", password);

        // Isolation level
        final var isolationLevel = propMap.get(AvailableSettings.ISOLATION);
        if (!Objects.toString(isolationLevel, "").isEmpty()) {
            dbcpProperties.put("defaultTransactionIsolation", isolationLevel);
        }

        // Turn off autocommit (unless autocommit property is set)
        final var autocommit = Optional.ofNullable(propMap.get(AUTOCOMMIT)).
                                        orElse(String.valueOf(Boolean.FALSE));
        if (!autocommit.isEmpty()) {
            dbcpProperties.put("defaultAutoCommit", autocommit);
        }

        // Pool size
        final var poolSize = getPoolSize(propMap);
        if (0 < poolSize) {
            dbcpProperties.put("maxTotal", poolSize);
        }

        // Copy all "driver" properties into "connectionProperties"
        final var driverProps = ConnectionProviderInitiator.getConnectionProperties(propMap);
        if (!driverProps.isEmpty()) {
            dbcpProperties.put("connectionProperties", getConnectionProperties(driverProps));
        }

        // Copy all DBCP properties removing the prefix
        for (final var ent : propMap.entrySet()) {
            if (ent.getKey().startsWith(PREFIX)) {
                final var property = ent.getKey().substring(PREFIX.length());
                final var value = ent.getValue();
                dbcpProperties.put(property, value);
            }
        }

        return dbcpProperties;
    }

    /**
     * get Integer Value
     *
     * @param propMap PropertyMap
     * @return Integer Value
     */
    private Integer getPoolSize(final Map<String, String> propMap) {
        var ret = 0;
        final var val = propMap.get(AvailableSettings.POOL_SIZE);
        if (!Objects.toString(val, "").isEmpty()) {
            try {
                ret = Integer.valueOf(val);
            } catch (final NumberFormatException e) {
                LOGGER.warn(e.getMessage());
            }
        }
        return ret;
    }

    /**
     * Copy all "driver" properties
     *
     * @param driverProps Properties
     * @return connectionProperties
     */
    private String getConnectionProperties(final Properties driverProps) {
        final var connectionProperties = new StringBuilder();
        for (final var key : driverProps.stringPropertyNames()) {
            if (0 < connectionProperties.length()) {
                connectionProperties.append(';');
            }
            connectionProperties.append(key);
            connectionProperties.append('=');
            connectionProperties.append(driverProps.getProperty(key));
        }
        return connectionProperties.toString();
    }

    /**
     * @see org.hibernate.engine.jdbc.connections.spi.ConnectionProvider#getConnection()
     */
    @Override
    public Connection getConnection() throws SQLException {
        final Connection conn;
        try {
            conn = this.ds.getConnection();
        } finally {
            logStatistics();
        }
        return conn;
    }

    /**
     * @see org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
     * #closeConnection(java.sql.Connection)
     */
    @Override
    public void closeConnection(final Connection conn) throws SQLException {
        try {
            conn.close();
        } finally {
            logStatistics();
        }
    }

    /**
     * @see org.hibernate.service.spi.Stoppable#stop()
     */
    @Override
    public void stop() {
        logStatistics();
        try {
            if (this.ds != null) {
                this.ds.close();
                this.ds = null;
            } else {
                LOGGER.warn("Cannot stop DBCP pool (not initialized)");
            }
        } catch (final SQLException e) {
            throw new HibernateException("Could not stop DBCP pool", e);
        }
    }

    /**
     * @see org.hibernate.engine.jdbc.connections.spi.ConnectionProvider#supportsAggressiveRelease()
     */
    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }

    /**
     * @see org.hibernate.service.spi.Wrapped#isUnwrappableAs(java.lang.Class)
     */
    @Override
    public boolean isUnwrappableAs(final Class unwrapType) {
        return ConnectionProvider.class.equals(unwrapType)
                || DbcpConnectionProvider.class.isAssignableFrom(unwrapType);
    }

    /**
     * @see org.hibernate.service.spi.Wrapped#unwrap(java.lang.Class)
     */
    @Override
    public <T> T unwrap(final Class<T> unwrapType) {
        if (isUnwrappableAs(unwrapType)) {
            return unwrapType.cast(this);
        }
        throw new UnknownUnwrapTypeException(unwrapType);
    }

    /**
     * output log
     */
    private void logStatistics() {
        LOGGER.info("active: {} (max: {})   idle: {} (max: {})",
            Unbox.box(this.ds.getNumActive()), Unbox.box(this.ds.getMaxTotal()),
            Unbox.box(this.ds.getNumIdle()), Unbox.box(this.ds.getMaxIdle()));
    }
}
