/*
 * 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.sling.datasource.internal;


import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.PoolConfiguration;
import org.apache.tomcat.jdbc.pool.PoolUtilities;
import org.osgi.framework.BundleContext;
import org.slf4j.LoggerFactory;

/**
 * DataSource implementation which only implements the Connection creation part. Tomcat
 * JDBC currently does not support specifying the Drive instance directly. While running
 * in OSGi env DriverRegistry maintains a list of seen driver instances.
 *
 * DriverDataSource make use of the DriverRegistry to lookup right Driver instance. This avoid
 * the requirement of having the Driver OSGi bundle attaches as fragments to our bundle
 */
class DriverDataSource implements DataSource {
    private final PoolConfiguration poolProperties;
    private final DriverRegistry driverRegistry;
    private final BundleContext bundleContext;
    private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
    private Driver driver;
    private final DataSourceFactory dataSourceFactory;

    public DriverDataSource(PoolConfiguration poolProperties, DriverRegistry driverRegistry,
                            BundleContext bundleContext,
                            DataSourceFactory dataSourceFactory) {
        this.poolProperties = poolProperties;
        this.driverRegistry = driverRegistry;
        this.bundleContext = bundleContext;
        this.dataSourceFactory = dataSourceFactory;
    }

    public Connection getConnection() throws SQLException {
        return getConnection(null, null);
    }

    public Connection getConnection(String usr, String pwd) throws SQLException {
        Properties properties = PoolUtilities.clone(poolProperties.getDbProperties());
        if(usr == null){
            usr = poolProperties.getUsername();
        }
        if(pwd == null){
            pwd= poolProperties.getPassword();
        }

        if (usr != null) properties.setProperty(PoolUtilities.PROP_USER, usr);
        if (pwd != null) properties.setProperty(PoolUtilities.PROP_PASSWORD, pwd);

        String driverURL = poolProperties.getUrl();
        Connection connection;
        try {
            connection = getDriver().connect(driverURL, properties);
        } catch (Exception x) {
            if (log.isDebugEnabled()) {
                log.debug("Unable to connect to database.", x);
            }
            //Based on logic in org.apache.tomcat.jdbc.pool.PooledConnection.connectUsingDriver()\
            org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool = getJmxPool();
            if (jmxPool !=null) {
                jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_CONNECT,
                        ConnectionPool.getStackTrace(x));
            }
            if (x instanceof SQLException) {
                throw (SQLException)x;
            } else {
                SQLException ex = new SQLException(x.getMessage());
                ex.initCause(x);
                throw ex;
            }
        }
        if (connection==null) {
            throw new SQLException("Driver:"+driver+" returned null for URL:"+driverURL);
        }

        return connection;
    }

    //~-------------------------------------< DataSource >

    public PrintWriter getLogWriter() throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    public void setLoginTimeout(int seconds) throws SQLException {

    }

    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @SuppressWarnings("UnusedDeclaration") //Part of JDK 7
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new SQLFeatureNotSupportedException();
    }

    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    private org.apache.tomcat.jdbc.pool.jmx.ConnectionPool getJmxPool() {
        if(dataSourceFactory.getPool() != null){
            return dataSourceFactory.getPool().getJmxPool();
        }
        return null;
    }

    private Driver getDriver() throws SQLException {
        if (driver != null) {
            return driver;
        }
        final String url = poolProperties.getUrl();

        Collection<Driver> drivers = driverRegistry.getDrivers();
        if(!drivers.isEmpty()) {
            log.debug("Looking for driver for [{}] against registered drivers", url);
            driver = findMatchingDriver(drivers);
        }

        if(driver == null && poolProperties.getDriverClassName() != null){
            log.debug("Looking for driver for [{}] via provided className [{}]",
                    url, poolProperties.getDriverClassName());
            driver = loadDriverClass();
        }

        if(driver == null){
            //This one is redundant as DriverManager would filter out drivers
            //whose classes are not visible from our bundle classloader which
            //means that this list would be empty in most cases
            log.debug("Looking for driver from DriverManager");
            driver = findMatchingDriver(Collections.list(DriverManager.getDrivers()));
        }

        if(driver == null){
            String msg = String.format("Not able to find any matching driver for url [%s] " +
                    "and driverClassName [%s]",url,poolProperties.getDriverClassName());
            throw new SQLException(msg);
        }

        return driver;
    }

    private Driver loadDriverClass() throws SQLException {
        try {
              log.debug("Instantiating driver using class: {} [url={}]",
                      poolProperties.getDriverClassName(),poolProperties.getUrl());
                return (Driver) bundleContext.getBundle()
                        .loadClass(poolProperties.getDriverClassName()).newInstance();
        } catch (java.lang.Exception cn) {
            log.debug("Unable to instantiate JDBC driver.", cn);
            SQLException ex = new SQLException(cn.getMessage());
            ex.initCause(cn);
            throw ex;
        }
    }

    private Driver findMatchingDriver(Collection<Driver> drivers) {
        final String url = poolProperties.getUrl();
        for (Driver driver : drivers) {
            try {
                if (driver.acceptsURL(url)) {
                    return driver;
                }
            } catch (SQLException e) {
                log.debug("Error occurred while matching driver against url {}", url, e);
            }
        }
        return null;
    }
}
