package com.atlassian.stash.internal.jdbc;

import org.springframework.core.InfrastructureProxy;

import javax.annotation.Nonnull;
import javax.annotation.PreDestroy;
import javax.sql.DataSource;
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Logger;

/**
 * An implementation of {@link SwappableDataSource} which delegates all methods to another {@code DataSource}
 * instance. That instance may be swapped out at runtime, allowing the database to be "updated" by tasks such as
 * database migration without requiring the new data source to be re-injected throughout the system.
 * <p>
 * Additionally, this class implements {@code Closeable} and adds a Spring {@code PreDestroy} {@link #close()} method
 * which is used to close the delegate {@code DataSource} when the Spring context is shutdown.
 */
public class DelegatingSwappableDataSource implements Closeable, SwappableDataSource {

    private volatile DataSource delegate;

    public DelegatingSwappableDataSource(DataSource delegate) {
        this.delegate = delegate;
    }

    /**
     * Closes the delegate {@code DataSource}, if it implements {@code Closeable}. Otherwise, does nothing.
     * <p>
     * This method is a workaround for an issue where Spring does not call annotated {@code PreDestroy} methods on
     * objects which are created using {@code BeanFactory.getObject(String, Object...)}. See the documentation in
     * {@link com.atlassian.stash.internal.hikari.ExtendedHikariDataSource ExtendedHikariDataSource} for details.
     */
    @Override
    @PreDestroy
    public void close() {
        if (delegate instanceof Closeable) {
            try {
                ((Closeable) delegate).close();
            } catch (IOException ignored) {
                // Ignore
            }
        }
    }

    @Override
    public Connection getConnection() throws SQLException {
        return delegate.getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return delegate.getConnection(username, password);
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return delegate.getLogWriter();
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return delegate.getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() {
        return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
    }

    @Override
    public DataSource getWrappedObject() {
        // cache the delegate field to a local variable to guard against the delegate changing between the instanceof
        // check and the rest of the ternary
        DataSource delegate = this.delegate;

        return delegate instanceof InfrastructureProxy ?
                (DataSource) ((InfrastructureProxy) delegate).getWrappedObject() :
                delegate;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return delegate.isWrapperFor(iface);
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        delegate.setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        delegate.setLoginTimeout(seconds);
    }

    @Nonnull
    @Override
    public DataSource swap(@Nonnull DataSource target) {
        DataSource old = delegate;
        delegate = target;

        return old;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return delegate.unwrap(iface);
    }
}
