package com.thinkbiganalytics.db;

/*-
 * #%L
 * thinkbig-feed-manager-controller
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */


import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;

import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import javax.sql.DataSource;

/**
 * A Connection Pooling service to return a new DataSource.
 * <p>Used for the {@code DBSchemaParser} class.</p>
 */
public class PoolingDataSourceService {

    /**
     * Cache of data sources.
     *
     * <p>Expires unused entries to avoid long-term caching of data sources with invalid credentials.</p>
     */
    private static final LoadingCache<DataSourceProperties, DataSource> DATA_SOURCES = CacheBuilder.newBuilder()
        .expireAfterAccess(60, TimeUnit.MINUTES)
        .build(new CacheLoader<DataSourceProperties, DataSource>() {
            @Override
            public DataSource load(final DataSourceProperties key) throws Exception {
                return createDatasource(key);
            }
        });

    /**
     * Gets the data source using the specified properties.
     *
     * @param props the data source properties
     * @return the data source
     */
    public static DataSource getDataSource(final DataSourceProperties props) {
        try {
            return DATA_SOURCES.get(props);
        } catch (final ExecutionException e) {
            if (e.getCause() != null) {
                throw Throwables.propagate(e.getCause());
            } else {
                throw new RuntimeException(e);
            }
        }
    }

    private static DataSource createDatasource(DataSourceProperties props) {
        DataSourceBuilder builder = DataSourceBuilder.create().url(props.getUrl()).username(props.getUser()).password(props.getPassword());
        if (StringUtils.isNotBlank(props.getDriverClassName())) {
            builder.driverClassName(props.getDriverClassName());
        }
        DataSource ds = builder.build();
        if (props.isTestOnBorrow() && StringUtils.isNotBlank(props.getValidationQuery())) {
            if (ds instanceof org.apache.tomcat.jdbc.pool.DataSource) {
                ((org.apache.tomcat.jdbc.pool.DataSource) ds).setTestOnBorrow(true);
                ((org.apache.tomcat.jdbc.pool.DataSource) ds).setValidationQuery(props.getValidationQuery());
            } else if (ds instanceof org.apache.commons.dbcp2.BasicDataSource) {
                ((org.apache.commons.dbcp2.BasicDataSource) ds).setValidationQuery(props.getValidationQuery());
                ((org.apache.commons.dbcp2.BasicDataSource) ds).setTestOnBorrow(true);
            } else if (ds instanceof org.apache.commons.dbcp.BasicDataSource) {
                ((org.apache.commons.dbcp.BasicDataSource) ds).setValidationQuery(props.getValidationQuery());
                ((org.apache.commons.dbcp.BasicDataSource) ds).setTestOnBorrow(true);
            }
        }
        return ds;
    }

    public static class DataSourceProperties {

        String user;
        String password;
        String url;
        String driverClassName;
        boolean testOnBorrow;
        String validationQuery;

        public DataSourceProperties(String user, String password, String url) {
            this.user = user;
            this.password = password;
            this.url = url;
        }

        public DataSourceProperties(String user, String password, String url, String driverClassName, boolean testOnBorrow, String validationQuery) {
            this.user = user;
            this.password = password;
            this.url = url;
            this.driverClassName = driverClassName;
            this.testOnBorrow = testOnBorrow;
            this.validationQuery = validationQuery;
        }

        public String getUser() {
            return user;
        }

        public void setUser(String user) {
            this.user = user;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public boolean isTestOnBorrow() {
            return testOnBorrow;
        }

        public void setTestOnBorrow(boolean testOnBorrow) {
            this.testOnBorrow = testOnBorrow;
        }

        public String getValidationQuery() {
            return validationQuery;
        }

        public void setValidationQuery(String validationQuery) {
            this.validationQuery = validationQuery;
        }

        public String getDriverClassName() {
            return driverClassName;
        }

        public void setDriverClassName(String driverClassName) {
            this.driverClassName = driverClassName;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            DataSourceProperties that = (DataSourceProperties) o;
            return Objects.equals(user, that.user) &&
                   Objects.equals(password, that.password) &&
                   Objects.equals(url, that.url) &&
                   Objects.equals(driverClassName, that.driverClassName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(user, password, url, driverClassName);
        }
    }

    /**
     * Instances of {@code PoolingDataSourceService} may not be constructed.
     *
     * @throws UnsupportedOperationException always
     */
    private PoolingDataSourceService() {
        throw new UnsupportedOperationException();
    }
}
