/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009 France Telecom R&D
 * Copyright (C) 2012 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.jndi.interceptors.impl.datasource;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.sql.DataSource;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.ow2.jonas.jndi.checker.api.IResourceChecker;
import org.ow2.jonas.jndi.checker.api.IResourceCheckerInfo;
import org.ow2.jonas.jndi.checker.api.IResourceCheckerManager;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * Wrapper of existing datasources. Each call to getConnection(*) is kept in
 * order to check if close have been done when detect method is called.
 * @author Florent Benoit
 */
public class DatasourceWrapper implements DataSource {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(DatasourceWrapper.class);

    /**
     * Close on check ?
     */
    private boolean forceClose = true;

    /**
     * Wrapped datasource.
     */
    private DataSource delegate = null;

    /**
     * Resource Checker Manager.
     */
    private IResourceCheckerManager manager;

    /**
     * JTA Transaction Manager.
     */
    private TransactionManager transactionManager;

    /**
     * Wrap a given datasource.
     * @param delegate the given datasource
     * @param manager checker manager
     */
    public DatasourceWrapper(DataSource delegate,
                             IResourceCheckerManager manager,
                             TransactionManager transactionManager) {
        this.delegate = delegate;
        this.manager = manager;
        this.transactionManager = transactionManager;
    }

    /**
     * <p>
     * Attempts to establish a connection with the data source that this
     * <code>DataSource</code> object represents.
     * @return a connection to the data source
     * @exception java.sql.SQLException if a database access error occurs
     */
    public Connection getConnection() throws SQLException {

        // Wrap the connection
        return wrapConnection(delegate.getConnection());
    }

    /**
     * <p>
     * Attempts to establish a connection with the data source that this
     * <code>DataSource</code> object represents.
     * @param username the database user on whose behalf the connection is being
     *        made
     * @param password the user's password
     * @return a connection to the data source
     * @exception SQLException if a database access error occurs
     */
    public Connection getConnection(final String username, final String password) throws SQLException {

        // And then call the wrapped datasource
        return wrapConnection(delegate.getConnection(username, password));
    }

    /**
     * Wrap the given connection and return it to the client.
     * @param connection the connection to wrap
     * @return the wrapped connection
     */
    protected synchronized Connection wrapConnection(final Connection connection) {

        // Wrap connection
        ConnectionProxy connectionProxy = null;
        if (isTransactional()) {
            connectionProxy = new TransactionalConnectionProxy(connection, getTransaction());
        } else {
            connectionProxy = new NonTransactionalConnectionProxy(connection, manager);
        }

        connectionProxy.setForceClose(forceClose);

        // Register the proxy
        connectionProxy.registerConnection();

        // return the proxy
        return (Connection) Proxy.newProxyInstance(delegate.getClass().getClassLoader(),
                new Class[] {Connection.class}, connectionProxy);
    }

    private boolean isTransactional() {
        return getTransaction() != null;
    }

    private Transaction getTransaction() {
        try {
            return transactionManager.getTransaction();
        } catch (SystemException e) {
            throw new IllegalStateException("Cannot get current Transaction", e);
        }
    }

    /**
     * <p>
     * Retrieves the log writer for this <code>DataSource</code> object.
     * <p>
     * The log writer is a character output stream to which all logging and
     * tracing messages for this data source will be printed. This includes
     * messages printed by the methods of this object, messages printed by
     * methods of other objects manufactured by this object, and so on. Messages
     * printed to a data source specific log writer are not printed to the log
     * writer associated with the <code>java.sql.Drivermanager</code> class.
     * When a <code>DataSource</code> object is created, the log writer is
     * initially null; in other words, the default is for logging to be
     * disabled.
     * @return the log writer for this data source or null if logging is disabled
     * @exception SQLException if a database access error occurs
     * @see #setLogWriter
     */
    public java.io.PrintWriter getLogWriter() throws SQLException {
        return delegate.getLogWriter();
    }

    /**
     * <p>
     * Sets the log writer for this <code>DataSource</code> object to the given
     * <code>java.io.PrintWriter</code> object.
     * <p>
     * The log writer is a character output stream to which all logging and
     * tracing messages for this data source will be printed. This includes
     * messages printed by the methods of this object, messages printed by
     * methods of other objects manufactured by this object, and so on. Messages
     * printed to a data source- specific log writer are not printed to the log
     * writer associated with the <code>java.sql.Drivermanager</code> class.
     * When a <code>DataSource</code> object is created the log writer is
     * initially null; in other words, the default is for logging to be
     * disabled.
     * @param out the new log writer; to disable logging, set to null
     * @exception SQLException if a database access error occurs
     * @see #getLogWriter
     */
    public void setLogWriter(final java.io.PrintWriter out) throws SQLException {
        delegate.setLogWriter(out);
    }

    /**
     * <p>
     * Sets the maximum time in seconds that this data source will wait while
     * attempting to connect to a database. A value of zero specifies that the
     * timeout is the default system timeout if there is one; otherwise, it
     * specifies that there is no timeout. When a <code>DataSource</code> object
     * is created, the login timeout is initially zero.
     * @param seconds the data source login time limit
     * @exception SQLException if a database access error occurs.
     * @see #getLoginTimeout
     */
    public void setLoginTimeout(final int seconds) throws SQLException {
        delegate.setLoginTimeout(seconds);
    }

    /**
     * Gets the maximum time in seconds that this data source can wait while
     * attempting to connect to a database. A value of zero means that the
     * timeout is the default system timeout if there is one; otherwise, it
     * means that there is no timeout. When a <code>DataSource</code> object is
     * created, the login timeout is initially zero.
     * @return the data source login time limit
     * @exception SQLException if a database access error occurs.
     * @see #setLoginTimeout
     */
    public int getLoginTimeout() throws SQLException {
        return delegate.getLoginTimeout();
    }

    /**
     * Method implemented by some datasources.
     * @return the optional mapper name
     */
    public String getMapperName() {
        Method getMapperNameMethod = null;
        try {
            getMapperNameMethod = delegate.getClass().getMethod("getMapperName");
        } catch (NoSuchMethodException e) {
            return null;
        }
        try {
            return (String) getMapperNameMethod.invoke(delegate);
        } catch (IllegalAccessException e) {
            return null;
        } catch (InvocationTargetException e) {
            return null;
        }

    }

    /**
     * Sets the flag for automatically closing or not the connection.
     * @param forceClose the given boolean value
     */
    public void setForceClose(final boolean forceClose) {
        this.forceClose = forceClose;
    }



    /**
     * Change equals method to use the equals on the underlying wrapped object.
     * For example jorm is comparing the datasource objects and if they're different, some merge is not occuring.
     * @param other the other object to compare
     * @return true if objects are equals.
     */
    @Override
    public boolean equals(final Object other) {
        if (this == delegate) {
            return true;
        }
        if ((other == null) || (other.getClass() != this.getClass())) {
            return false;
        }
        DatasourceWrapper otherDatasource = (DatasourceWrapper) other;
        return delegate.equals(otherDatasource.delegate);
    }

    /**
     * @return the hashCode of the wrapped object.
     */
    @Override
    public int hashCode() {
        return delegate.hashCode();
    }

    /**
     * Returns true if this either implements the interface argument or is directly or indirectly a wrapper for an object that does.
     * Returns false otherwise. If this implements the interface then return true, else if this is a wrapper then return the result
     * of recursively calling isWrapperFor on the wrapped object. If this does not implement the interface and is not a wrapper,
     * return false. This method should be implemented as a low-cost operation compared to unwrap so that callers can use this method
     * to avoid expensive unwrap calls that may fail. If this method returns true then calling unwrap with the same argument should succeed.
     * @param iface a Class defining an interface. 
     * @returns true if this implements the interface or directly or indirectly wraps an object that does. 
     * @throws SQLException if an error occurs while determining whether this is a wrapper for an object with the given interface.
     */
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return delegate.isWrapperFor(iface);
    }

    /**
     * Returns an object that implements the given interface to allow access to non-standard methods, or standard methods not exposed by the proxy.
     * If the receiver implements the interface then the result is the receiver or a proxy for the receiver. If the receiver is a wrapper
     * and the wrapped object implements the interface then the result is the wrapped object or a proxy for the wrapped object.
     * Otherwise return the the result of calling unwrap recursively on the wrapped object or a proxy for that result. 
     * If the receiver is not a wrapper and does not implement the interface, then an SQLException is thrown.
     * @param iface A Class defining an interface that the result must implement. 
     * @returns an object that implements the interface. May be a proxy for the actual implementing object. 
     * @throws SQLException If no object found that implements the interface
     */
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return delegate.unwrap(iface);
    }


}
