/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2010 Bull S.A.
 * 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 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.ee.jdbc;

import java.lang.reflect.Proxy;
import org.ow2.jonas.resource.internal.IJDBCConnection;
import java.io.PrintWriter;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashSet;

import javax.resource.ResourceException;
import javax.resource.spi.ConnectionEvent;
import javax.resource.spi.ConnectionEventListener;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.LocalTransaction;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionMetaData;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.sql.DataSource;
import javax.sql.PooledConnection;
import javax.sql.XAConnection;
import javax.transaction.xa.XAResource;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * Represent an underlying Connection
 * @author Eric hardesty
 */
public class ManagedConnectionImpl implements ManagedConnection, javax.resource.spi.LocalTransaction {

    public Logger trace = null;

    protected PrintWriter out = null;

    protected DataSource factory = null;

    /**
     * Connection Event Listeners
     */
    protected HashSet<ConnectionEventListener> cels = null;

    protected boolean inLocalTransaction = false;

    ManagedConnectionFactoryImpl mcf = null;

    java.sql.Connection connection = null;

    PooledConnection poolCon = null;

    XAConnection xaCon = null;

    PasswordCredential pc = null;

    DriverWrapper wrapper = null;

    private XAResource xares;

    private boolean isAutoCommit = true;

    private boolean closed = false;

    private long[] sigList;

    private int maxSigs;

    private long signature;

    private long lastSig;

    /**
     * Logging (debug enabled)
     */
    private final boolean isDebugOn;

    /**
     * Constructor
     * @param _mcf The calling factory
     * @param _pc username and password.
     * @param _con physical Connection
     * @param _pcon PooledConnection in case of CP, or null.
     * @param _xa XAConnection in case of XA, or null.
     * @param wrp DriverWrapper in case of DM, or null.
     */
    public ManagedConnectionImpl(ManagedConnectionFactoryImpl _mcf, 
                                 PasswordCredential _pc, 
                                 java.sql.Connection _con,
                                 PooledConnection _pcon, 
                                 XAConnection _xa, 
                                 DriverWrapper wrp) {

        mcf = _mcf;
        pc = _pc;
        connection = _con;
        poolCon = _pcon;
        xaCon = _xa;
        wrapper = wrp;
        xares = null;
        out = mcf.pw;
        cels = new HashSet<ConnectionEventListener>();
        maxSigs = 50;
        sigList = new long[maxSigs];
        signature = 0;
        lastSig = 0;
        trace = mcf.trace;
        isDebugOn = trace.isLoggable(BasicLevel.DEBUG);
        if (connection == null) {
            trace.log(BasicLevel.ERROR, "Null Connection");
        } else {
            trace.log(BasicLevel.DEBUG, "New Connection: " + connection);
        }
    }

    /**
     * Signal an event to all the registered listeners.
     * @param code type of event (CONNECTION_ERROR_OCCURRED, CONNECTION_CLOSED, ...)
     * @param ch may be a connection handler or null.
     * @param ex exception if CONNECTION_ERROR_OCCURRED
     */
    public void signalEvent(int code, Object ch, Exception ex) {
        ConnectionEvent ce = null;
        if (ex == null) {
            ce = new ConnectionEvent(this, code);
        } else {
            ce = new ConnectionEvent(this, code, ex);
        }
        if (ch != null) {
            ce.setConnectionHandle(ch);
        }
        for (ConnectionEventListener cel : cels) {
            switch (code) {
                case ConnectionEvent.CONNECTION_CLOSED:
                    cel.connectionClosed(ce);
                    break;
                case ConnectionEvent.LOCAL_TRANSACTION_STARTED:
                    cel.localTransactionStarted(ce);
                    break;
                case ConnectionEvent.LOCAL_TRANSACTION_COMMITTED:
                    cel.localTransactionCommitted(ce);
                    break;
                case ConnectionEvent.LOCAL_TRANSACTION_ROLLEDBACK:
                    cel.localTransactionRolledback(ce);
                    break;
                case ConnectionEvent.CONNECTION_ERROR_OCCURRED:
                    cel.connectionErrorOccurred(ce);
                    break;
                default:
                    throw new IllegalArgumentException("Illegal eventType: " + code);
            }
        }
    }

    public synchronized boolean getAutoCommit() throws ResourceException {
        return isAutoCommit;
    }

    public synchronized void setAutoCommit(boolean ac) throws ResourceException {
        try {
            if (connection != null) {
                connection.setAutoCommit(ac);
            }
            // Don't change the flag if exception raised.
            isAutoCommit = ac;
            if (isDebugOn) {
                trace.log(BasicLevel.DEBUG, "setAutoCommit " + ac);
            }
        } catch (Exception e) {
            trace.log(BasicLevel.WARN, "Cannot set AutoCommit:" + e);
            throw new ResourceException(e.getMessage());
        }
    }

    /**
     * Adds a connection event listener.
     */
    public void addConnectionEventListener(ConnectionEventListener listener) {
        cels.add(listener);
    }

    /**
     * Removes an already registered connection event listener from the
     * ManagedConnection instance
     */
    public void removeConnectionEventListener(ConnectionEventListener listener) {
        cels.remove(listener);
    }

    /**
     * Change the association of a connection handle. Not used.
     */
    public synchronized void associateConnection(Object con) throws ResourceException {
        if (isDebugOn) {
            trace.log(BasicLevel.DEBUG, "connection:" + con);
        }
        if (con instanceof ConnectionImpl) {
            ConnectionImpl ci = (ConnectionImpl) con;
            long sig = getNewSignature(true);
            ci.setSignature(sig);
        } else {
            throw new ResourceException("The managedConnection cannot associate this connection: " + con);
        }
    }

    /**
     * Cleanup the ManagedConnection instance.
     * All the Connection Handle should be invalidated.
     * Prepare the CM to be put back in the pool or destroyed.
     * @throws ResourceException Error while cleaning up.
     * @throws IllegalStateException A transaction is not ended.
     */
    public void cleanup() throws ResourceException {
        if (inLocalTransaction) {
            trace.log(BasicLevel.WARN, "A local transaction is not complete");
            throw new IllegalStateException("A local transaction is not complete");
        }
        // Reset autocommit mode before returning to the pool.
        if (!isAutoCommit) {
            try {
                setAutoCommit(true);
            } catch (Exception exc) {
                trace.log(BasicLevel.WARN, "in global transaction");
                throw new IllegalStateException("in global transaction");
            }
        }
        // Invalidate all Connection Handles
        clearSignature();
    }

    /**
     * Destroys the physical connection.
     */
    public synchronized void destroy() throws ResourceException {
        if (isDebugOn) {
            trace.log(BasicLevel.DEBUG, "destroy mc=" + this + " with connection=" + connection);
        }
        // cannot destroy if not clean up before.
        cleanup();
        if (xaCon != null) {
            // XA case
            try {
                // must call this to physically close connection. (bug JONAS-159)
                xaCon.close();
            } catch (Exception e) {
                trace.log(BasicLevel.ERROR, "Cannot close xaConnection:" + e);
            }
            xaCon = null;
        } else if (poolCon != null) {
            // CP case
            try {
                poolCon.close();
            } catch (Exception e) {
                trace.log(BasicLevel.ERROR, "Cannot close PoolConnection:" + e);
            }
            poolCon = null;
        } else {
            if (wrapper != null) {
                // DM case
                try {
                    DriverManager.deregisterDriver(wrapper);
                } catch (Exception e) {
                    trace.log(BasicLevel.ERROR, "Unable to deregister Driver wrapper", e);
                }
            }
            if (connection != null) {
                // DM or DS case
                try {
                    connection.close();
                } catch (Exception e) {
                    trace.log(BasicLevel.ERROR, "", e, "ManagedConnectionImpl", "destroy");
                    throw new ResourceException(e);
                } finally {
                    connection = null;
                }
            }
        }

        // free the XAResourceImpl
        xares = null;
    }

    /**
     * Create a connection handle for user purpose.
     * @return a ConnectionImpl
     * @throws ResourceException Cannot create Handle
     */
    public Object getConnection(Subject subject, ConnectionRequestInfo cxRequestInfo) throws ResourceException {
        if (isDebugOn) {
            trace.log(BasicLevel.DEBUG, "subject:" + subject + " connectionRequest:" + cxRequestInfo);
        }

        long sig = getNewSignature(true);

        if (connection == null) {
            trace.log(BasicLevel.ERROR, "null Connection");
            throw new ResourceException("Cannot create Handle on a null Connection");
        }
        try {
                return Proxy.newProxyInstance(IJDBCConnection.class.getClassLoader(), new Class[] {IJDBCConnection.class},
                new ConnectionImpl(this, connection, sig, out));
        } catch (Exception e) {
            trace.log(BasicLevel.ERROR, "", e, "ManagedConnectionImpl", "getConnection");
            throw new ResourceException(e.getMessage());
        }
    }

    /**
     * The Connection Handler has been closed by its user.
     * @param ch Connection Handler
     * @throws ResourceException error while signaling the event to listeners
     */
    public void close(IJDBCConnection ch) throws ResourceException {
        clearSignature(ch.getKey(), true);
        signalEvent(ConnectionEvent.CONNECTION_CLOSED, ch, null);
    }

    /**
     * Returns an javax.resource.spi.LocalTransaction instance.
     */
    public LocalTransaction getLocalTransaction() throws ResourceException {
        return (LocalTransaction) this;
    }

    /**
     * Gets the log writer for this ManagedConnection instance.
     */
    public PrintWriter getLogWriter() {
        return out;
    }

    /**
     * Gets the metadata information for this connection's underlying EIS
     * resource manager instance.
     */
    public ManagedConnectionMetaData getMetaData() throws ResourceException {
        return new MetaDataImpl(this);
    }

    /**
     * Get an XAResource to enlist with the Transaction Manager.
     * @return the XAResource
     * @throws ResourceException cannot get it.
     */
    public XAResource getXAResource() throws ResourceException {
        if (isDebugOn) {
            trace.log(BasicLevel.DEBUG, "");
        }
        if (inLocalTransaction) {
            throw new ResourceException(
                    "The managedConnection is already in a local transaction and an XA resource cannot be obtained");
        }
        if (xares == null) {
            if (xaCon != null) {
                // XA case
                try {
                    xares = xaCon.getXAResource();
                } catch (Exception ex) {
                    throw new ResourceException("Unable to obtain XAResource: ", ex);
                }
            } else {
                // other cases
                if (isDebugOn) {
                    trace.log(BasicLevel.DEBUG, "JDBC driver doesn't support XA, simulate it");
                }
            }
            // Always return a wrapper.
            xares = new XAResourceImpl(this, xares);
        }
        return xares;
    }

    /**
     * Sets the log writer for this ManagedConnection instance.
     */
    public void setLogWriter(PrintWriter _out) {
        out = _out;
    }

    public void addSignature(long sig) {
        int off = -1;
        for (int i = 0; i < maxSigs; i++)
            if (sigList[i] == 0) {
                off = i;
                break;
            }

        if (off > -1)
            sigList[off] = sig;
        else {
            maxSigs += 20;
            long[] tmp = new long[maxSigs];
            System.arraycopy(sigList, 0, tmp, 0, maxSigs - 20);
            sigList = tmp;
            sigList[maxSigs - 20] = sig;
        }
    }

    public void clearSignature() {
        for (int i = 0; i < maxSigs; i++)
            sigList[i] = 0;
        signature = 0;
    }

    public void clearSignature(long sig, boolean clear) {
        for (int i = 0; i < maxSigs; i++) {
            if (sig == sigList[i]) {
                sigList[i] = 0;
                break;
            }
        }
        if (clear) {
            signature = 0;
        }
    }

    public void setSignature(long sig) {
        if (signature == 0) {
            signature = sig;
        }
    }

    /**
     * Gets the signature value passed in if found in the signature hash table
     * @param sig value to find in signature table
     * @return long signature value or 0 if not found
     */
    public long getSignature(long sig) {
        // find signature
        long retSig = 0;
        if (sig > 0) {
            for (int i = 0; i < maxSigs; i++)
                if (sig == sigList[i]) {
                    retSig = sig;
                    break;
                }
        }
        return (retSig);
    }

    /**
     * Gets the current signature value
     * @return long current signature value
     */
    public long getSignature() {
        return (signature);
    }

    /**
     * Gets the next signature value
     * @return long next signature value
     */
    public long getNewSignature(boolean setSig) {
        long sig = System.currentTimeMillis();
        if (sig <= lastSig && lastSig != 0) {
            sig = lastSig++;
        }
        if (isDebugOn) {
            trace.log(BasicLevel.DEBUG, "Sig is " + sig + " last Sig was " + lastSig);
        }
        lastSig = sig;
        addSignature(sig);
        if (setSig) {
            signature = sig;
        }
        return sig;
    }

    // ---------------------------------------------------------------
    // Implementation of Interface LocalTransaction
    // ---------------------------------------------------------------

    /**
     * start a new local transaction.
     */
    public void begin() throws ResourceException {
        if (inLocalTransaction) {
            throw new ResourceException("The managedConnection is already in a LocalTransaction");
        }
        trace.log(BasicLevel.DEBUG, "");
        try {
            inLocalTransaction = true;
            connection.setAutoCommit(false);
        } catch (Exception ex) {
        }
    }

    /**
     * commit the local transaction.
     */
    public void commit() throws ResourceException {
        trace.log(BasicLevel.DEBUG, "");
        Exception commitException = null;
        try {
            connection.commit();
        } catch (Exception e) {
            commitException = e;
            trace.log(BasicLevel.WARN, "Could not commit local tx: " + e);
        } finally {
        	// Reset the auto-commit flag, even in case of exception
            // make sure that the connection can be destroyed if problems
            try {
                connection.setAutoCommit(true);
                // this must be done after the setAutoCommit(true) to make remove Connection work.
                if (commitException != null) {
                    signalEvent(ConnectionEvent.CONNECTION_ERROR_OCCURRED, null, commitException);
                }
            } catch (Exception ex) {
                trace.log(BasicLevel.WARN, ex.getMessage());
            }
            inLocalTransaction = false;
        }
    }

    /**
     * rollback the local transaction.
     */
    public void rollback() throws ResourceException {
        trace.log(BasicLevel.DEBUG, "");
        Exception rbException = null;
        try {
            connection.rollback();
        } catch (Exception e) {
            rbException = e;
            trace.log(BasicLevel.WARN, "Could not rollback local tx: " + e);
        } finally {
        	// Reset the auto-commit flag, even in case of exception
            // make sure that the connection can be destroyed if problems
            try {
                connection.setAutoCommit(true);
                // this must be done after the setAutoCommit(true) to make remove Connection work.
                if (rbException != null) {
                    signalEvent(ConnectionEvent.CONNECTION_ERROR_OCCURRED, null, rbException);
                }
            } catch (Exception ex) {
                trace.log(BasicLevel.WARN, ex.getMessage());
            }
            inLocalTransaction = false;
        }
    }

    /**
     * Check if Connection is still valid
     */
    public boolean isValid() {
        boolean ret = true;
        if (connection == null) {
            trace.log(BasicLevel.WARN, "No connection");
            return false;
        }
        // Try directly the physical Connection
        // The Connection is supposed to be locked in the free list
        java.sql.Statement stmt = null;
        try {
            stmt = connection.createStatement();
            stmt.execute(mcf.getJdbcConnTestStmt());
        } catch (SQLException se) {
            trace.log(BasicLevel.WARN, "Connection not valid");
            ret = false;
        } finally {
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (Exception e) {
                }
            }
        }
        return ret;
    }
}
