/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.tomee.jdbc;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.openejb.cipher.PasswordCipher;
import org.apache.openejb.cipher.PasswordCipherFactory;
import org.apache.openejb.monitoring.LocalMBeanServer;
import org.apache.openejb.monitoring.ObjectNameBuilder;
import org.apache.openejb.resource.jdbc.BasicDataSourceUtil;
import org.apache.openejb.resource.jdbc.plugin.DataSourcePlugin;
import org.apache.openejb.resource.jdbc.pool.PoolDataSourceCreator;
import org.apache.openejb.resource.jdbc.pool.XADataSourceResource;
import org.apache.openejb.util.Duration;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.Strings;
import org.apache.openejb.util.SuperProperties;
import org.apache.openejb.util.reflection.Reflections;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.PoolConfiguration;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import org.apache.tomcat.jdbc.pool.PooledConnection;

import javax.management.ObjectName;
import javax.sql.CommonDataSource;
import javax.sql.DataSource;
import javax.sql.XADataSource;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class TomEEDataSourceCreator extends PoolDataSourceCreator {
    private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB, TomEEDataSourceCreator.class);

    @Override
    public DataSource pool(final String name, final DataSource ds, final Properties properties) {
        final Properties converted = new Properties();
        final SuperProperties prop = new SuperProperties().caseInsensitive(true);
        prop.putAll(properties);
        updateProperties(prop, converted, null);

        final PoolConfiguration config = build(PoolProperties.class, converted);
        config.setDataSource(ds);
        final ConnectionPool pool;
        try {
            pool = new ConnectionPool(config);
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
        return build(TomEEDataSource.class, new TomEEDataSource(config, pool, name), converted);
    }

    @Override
    public CommonDataSource pool(final String name, final String driver, final Properties properties) {
        final Properties converted = new Properties();
        converted.setProperty("name", name);

        final SuperProperties prop = new SuperProperties().caseInsensitive(true);
        prop.putAll(properties);

        updateProperties(prop, converted, driver);
        final PoolConfiguration config = build(PoolProperties.class, converted);
        final String xa = String.class.cast(properties.remove("XaDataSource"));
        if (xa != null) {
            final XADataSource xaDs = XADataSourceResource.proxy(Thread.currentThread().getContextClassLoader(), xa);
            final TomEEDataSource instance = new TomEEDataSource(config, name, xaDs);
            return build(TomEEDataSource.class, instance, converted);

        } else {
            final TomEEDataSource instance = new TomEEDataSource(config, name);
            return build(TomEEDataSource.class, instance, converted);
        }

    }

    private void updateProperties(final SuperProperties properties, final Properties converted, final String driver) {
        // some compatibility with old dbcp style
        if (driver != null && !properties.containsKey("driverClassName")) {
            converted.setProperty("driverClassName", driver);
        }
        final String jdbcDriver = (String) properties.remove("JdbcDriver");
        if (jdbcDriver != null && !properties.containsKey("driverClassName")) {
            converted.setProperty("driverClassName", jdbcDriver);
        }
        final String url = (String) properties.remove("JdbcUrl");
        if (url != null && !properties.containsKey("url")) {
            converted.setProperty("url", url);
        }
        final String user = (String) properties.remove("user");
        if (user != null && !properties.containsKey("username")) {
            converted.setProperty("username", user);
        }
        final String maxWait = toMillis((String) properties.remove("maxWaitTime"));
        if (maxWait != null && !properties.containsKey("maxWait")) {
            converted.setProperty("maxWait", maxWait);
        }
        final String tb = toMillis((String) properties.remove("timeBetweenEvictionRuns"));
        if (tb != null && !properties.containsKey("timeBetweenEvictionRunsMillis")) {
            converted.setProperty("timeBetweenEvictionRunsMillis", tb);
        }
        final String minEvict = toMillis((String) properties.remove("minEvictableIdleTime"));
        if (minEvict != null && !properties.containsKey("minEvictableIdleTimeMillis")) {
            converted.setProperty("minEvictableIdleTimeMillis", minEvict);
        }

        final String passwordCipher = properties.getProperty("PasswordCipher");
        if (passwordCipher != null && "PlainText".equals(passwordCipher)) { // no need to warn about it
            properties.remove("PasswordCipher");
        } else {
            final String password = properties.getProperty("Password");
            if (passwordCipher != null) {
                final PasswordCipher cipher = PasswordCipherFactory.getPasswordCipher(passwordCipher);
                final String plainPwd = cipher.decrypt(password.toCharArray());
                converted.setProperty("password", plainPwd);

                // all went fine so remove it to avoid errors later
                properties.remove("PasswordCipher");
                properties.remove("Password");
            }
        }

        for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
            final String key = entry.getKey().toString();
            final String value = entry.getValue().toString().trim();
            if (!converted.containsKey(key)) {
                if (!value.isEmpty()) {
                    if ("MaxOpenPreparedStatements".equalsIgnoreCase(key) || "PoolPreparedStatements".equalsIgnoreCase(key)) {
                        if ("0".equalsIgnoreCase(properties.getProperty("MaxOpenPreparedStatements", "0"))
                                || "false".equalsIgnoreCase(properties.getProperty("PoolPreparedStatements", "false"))) {
                            continue;
                        }

                        final String interceptors = properties.getProperty("jdbcInterceptors");
                        if (interceptors == null) {
                            converted.setProperty("jdbcInterceptors",
                                    "StatementCache(max=" + properties.getProperty("MaxOpenPreparedStatements", "128") + ")");
                            LOGGER.debug("Tomcat-jdbc StatementCache added to handle prepared statement cache/pool");
                        } else if (!interceptors.contains("StatementCache")) {
                            converted.setProperty("jdbcInterceptors", interceptors
                                    + ";StatementCache(max=" + properties.getProperty("MaxOpenPreparedStatements", "128") + ")");
                            LOGGER.debug("Tomcat-jdbc StatementCache added to handle prepared statement cache/pool");
                        }
                        continue;
                    }

                    converted.put(Strings.lcfirst(key), value);
                } else if (key.toLowerCase().equals("username") || key.toLowerCase().equals("password")) { // avoid NPE
                    converted.put(Strings.lcfirst(key), "");
                }
            }
        }

        final String currentUrl = converted.getProperty("url");
        if (currentUrl != null) {
            try {
                final DataSourcePlugin helper = BasicDataSourceUtil.getDataSourcePlugin(currentUrl);
                if (helper != null) {
                    final String newUrl = helper.updatedUrl(currentUrl);
                    if (!currentUrl.equals(newUrl)) {
                        properties.setProperty("url", newUrl);
                    }
                }
            } catch (final SQLException ignored) {
                // no-op
            }
        }
    }

    private String toMillis(final String d) {
        if (d == null) {
            return null;
        }
        return Long.toString(new Duration(d).getTime(TimeUnit.MILLISECONDS));
    }

    @Override
    protected void doDestroy(final CommonDataSource dataSource) throws Throwable {
        final org.apache.tomcat.jdbc.pool.DataSource ds = (org.apache.tomcat.jdbc.pool.DataSource) dataSource;
        if (ds instanceof TomEEDataSource) {
            ((TomEEDataSource) ds).internalJMXUnregister();
        }
        ds.close(true);
    }

    public static class TomEEDataSource extends org.apache.tomcat.jdbc.pool.DataSource {
        private static final Log LOGGER = LogFactory.getLog(TomEEDataSource.class);
        private static final Class<?>[] CONNECTION_POOL_CLASS = new Class<?>[]{ PoolConfiguration.class };

        private ObjectName internalOn;

        public TomEEDataSource(final PoolConfiguration properties, final ConnectionPool pool, final String name) {
            super(readOnly(properties));
            this.pool = pool;
            initJmx(name);
        }

        public TomEEDataSource(final PoolConfiguration poolConfiguration, final String name) {
            this(poolConfiguration, name, null);
        }

        @Override public Future<Connection> getConnectionAsync() throws SQLException {
            return super.getConnectionAsync();
        }

        public TomEEDataSource(final PoolConfiguration poolConfiguration, final String name, final XADataSource xaDs) {
            super(readOnly(poolConfiguration));
            if (xaDs != null) {
                this.setDataSource(xaDs);
            }

            try { // just to force the pool to be created and be able to register the mbean
                createPool();
                initJmx(name);
            } catch (final Throwable e) {
                LOGGER.error("Can't create DataSource", e);
            }
        }

        @Override
        protected void registerJmx() {
            // no-op
        }

        @Override
        protected void unregisterJmx() {
            // no-op
        }

        @Override
        public ConnectionPool createPool() throws SQLException {
            if (pool != null) {
                return pool;
            } else {
                pool = new TomEEConnectionPool(poolProperties, Thread.currentThread().getContextClassLoader()); // to force to init the driver with TCCL
                return pool;
            }
        }

        private static PoolConfiguration readOnly(final PoolConfiguration pool) {
            // if validationQuery is not filled disable testXXX
            if (pool.getValidationQuery() == null || pool.getValidationQuery().isEmpty()) {
                if (pool.isTestOnBorrow()) {
                    LOGGER.info("Disabling testOnBorrow since no validation query is provided");
                    pool.setTestOnBorrow(false);
                }
                if (pool.isTestOnConnect()) {
                    LOGGER.info("Disabling testOnConnect since no validation query is provided");
                    pool.setTestOnConnect(false);
                }
                if (pool.isTestOnReturn()) {
                    LOGGER.info("Disabling testOnReturn since no validation query is provided");
                    pool.setTestOnReturn(false);
                }
                if (pool.isTestWhileIdle()) {
                    LOGGER.info("Disabling testWhileIdle since no validation query is provided");
                    pool.setTestWhileIdle(false);
                }
            }

            // prevent overriding of the configuration
            try {
                return (PoolConfiguration) Proxy.newProxyInstance(TomEEDataSourceCreator.class.getClassLoader(), CONNECTION_POOL_CLASS, new ReadOnlyConnectionpool(pool));
            } catch (final Throwable e) {
                return (PoolConfiguration) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), CONNECTION_POOL_CLASS, new ReadOnlyConnectionpool(pool));
            }
        }

        private void initJmx(final String name) {
            try {
                internalOn = ObjectNameBuilder.uniqueName("datasources", name.replace("/", "_"), this);
                try {
                    if (pool.getJmxPool() != null) {
                        LocalMBeanServer.get().registerMBean(pool.getJmxPool(), internalOn);
                    }
                } catch (final Exception e) {
                    LOGGER.error("Unable to register JDBC pool with JMX", e);
                }
            } catch (final Exception ignored) {
                // no-op
            }
        }

        public void internalJMXUnregister() {
            if (internalOn != null) {
                try {
                    LocalMBeanServer.get().unregisterMBean(internalOn);
                } catch (final Exception e) {
                    LOGGER.error("Unable to unregister JDBC pool with JMX", e);
                }
            }
        }
    }

    private static class ReadOnlyConnectionpool implements InvocationHandler {
        private final PoolConfiguration delegate;

        public ReadOnlyConnectionpool(final PoolConfiguration pool) {
            this.delegate = pool;
        }

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            final String name = method.getName();
            if (!(name.startsWith("set") && args != null && args.length == 1 && Void.TYPE.equals(method.getReturnType()))) {
                return method.invoke(delegate, args);
            }
            if (name.equals("setDataSource")) {
                delegate.setDataSource(args[0]);
            }
            return null;
        }
    }

    private static class TomEEConnectionPool extends ConnectionPool {
        private final ClassLoader creationLoader;

        public TomEEConnectionPool(final PoolConfiguration poolProperties, final ClassLoader creationLoader) throws SQLException {
            super(poolProperties);
            this.creationLoader = creationLoader;
        }

        @Override
        protected PooledConnection create(final boolean incrementCounter) {
            final PooledConnection con = super.create(incrementCounter);

            if (getPoolProperties().getDataSource() == null) { // using driver
                // init driver with TCCL
                ClassLoader cl = Thread.currentThread().getContextClassLoader();
                if (cl == null) {
                    cl = TomEEConnectionPool.class.getClassLoader();
                }
                try {
                    Reflections.set(con, "driver", Class.forName(getPoolProperties().getDriverClassName(), true, cl).newInstance());
                } catch (final ClassNotFoundException cnfe) {
                    try { // custom resource classloader
                        Reflections.set(con, "driver", Class.forName(getPoolProperties().getDriverClassName(), true, creationLoader).newInstance());
                    } catch (final Exception e) {
                        // will fail later, no worry
                    }
                } catch (final Exception cn) {
                    // will fail later, no worry
                }
            }
            return con;
        }
    }
}
