/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * Contact: jonas-team@objectweb.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.objectweb.jonas.dbm;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import javax.sql.XAConnection;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.objectweb.jonas.common.Log;
import org.objectweb.transaction.jta.ResourceManagerEvent;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * This class represents the physical connection, managed by the pool.
 * It implements the PooledConnection interface, in fact via the derived
 * XAConnection interface.
 * @author durieuxp
 */
public class JManagedConnection implements Comparable, XAConnection, XAResource, ResourceManagerEvent, Synchronization {

    static private Logger logger = Log.getLogger(Log.JONAS_DBM_PREFIX);
    static private Logger loggerxa = Log.getLogger(Log.JONAS_DBM_PREFIX + ".xa");
    static private Logger loggerps = Log.getLogger(Log.JONAS_DBM_PREFIX + ".ps");

    Connection implConn = null;
    Connection actConn = null;
    ConnectionManager ds;
    private int pstmtmax;

    // Most of the time, only 1 element, but must keep a Vector here.
    private Vector eventListeners = new Vector();

    private int timeout;
    private String rmid = null;

    private int open;            // >0 if connection is open
    private Transaction tx;  // transaction the connection is involved with
    private Transaction enlistedInTx; /// current transaction in case of late enlistment
    private boolean rme = false; // true if registered as ResourceManagerEvent
    private long deathTime;     // When it is expected to die (for closed connections)
    private long closeTime;     // When it is expected to be closed (for opened connections)

    private static int objcount = 10;   // to have a minimum of prepare statements.
    private int ident;
    private int hitCount = 0;

    /**
     * List of PreparedStatement in the pool.
     */
    private Map psList = null;

    //  -----------------------------------------------------------------
    //  Constructors
    //  -----------------------------------------------------------------

    public JManagedConnection(Connection conn, ConnectionManager ds) {

        logger.log(BasicLevel.DEBUG, "constructor");

        this.actConn = conn;
        this.ds = ds;

        // An XAConnection holds 2 objects: 1 Connection + 1 XAResource
        this.implConn = new JConnection(this, conn);
        this.rmid = ds.getDatasourceName();

        open = 0;
        deathTime = System.currentTimeMillis() + ds.getMaxAgeMilli();

        // getPstmtMax() = 0 -> no pool for PreparedStatement's
        // Not all connections have the same number of prepare statement.
        ident = objcount++;
        if (ds.getPstmtMax() > 0) {
            int pstmtmin = 1 + ds.getPstmtMax() / 4;
            pstmtmax = ident % ds.getPstmtMax();
            if (pstmtmax < pstmtmin) {
                pstmtmax = pstmtmin;
            }
            psList = Collections.synchronizedMap(new HashMap(pstmtmax));
        }
    }

    /**
     * @return The ident of this JManagedConnection
     */
    public int getIdent() {
        return ident;
    }

    /**
     * Dynamically change the prepared statement pool size
     */
    public void setPstmtMax(int max) {
        pstmtmax = max;
        if (psList == null) {
            psList = Collections.synchronizedMap(new HashMap(pstmtmax));
        }
    }

    //  -----------------------------------------------------------------
    //  XAResource implementation
    //  -----------------------------------------------------------------

    /**
     * Commit the global transaction specified by xid.
     * @param xid transaction xid
     * @param onePhase true if one phase commit
     * @throws XAException XA protocol error
     */
    public void commit(Xid xid, boolean onePhase) throws XAException {
        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-COMMIT for " + xid);
        }

        // Avoids a NPE.
        // This should not occur because a close could not be called
        // on a connection implied inside a transaction.
        if (actConn == null) {
            loggerxa.log(BasicLevel.ERROR, "commit on a closed connection");
            return;
        }

        // Commit the transaction
        try {
            actConn.commit();
        } catch (SQLException e) {
            loggerxa.log(BasicLevel.ERROR, "Cannot commit transaction:" + e);
            notifyError(e);
            throw(new XAException("Error on commit"));
        }
    }

    /**
     * Ends the work performed on behalf of a transaction branch.
     * @param xid transaction xid
     * @param flags currently unused
     * @throws XAException XA protocol error
     */
    public void end(Xid xid, int flags) throws XAException {
        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-END for " + xid);
        }
    }

    /**
     * Tell the resource manager to forget about a heuristically
     * completed transaction branch.
     * @param xid transaction xid
     * @throws XAException XA protocol error
     */
    public void forget(Xid xid) throws XAException {
        // not implemented.
        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-FORGET for " + xid);
        }
    }

    /**
     * Obtain the current transaction timeout value set for this
     * XAResource instance.
     * @return the current transaction timeout in seconds
     * @throws XAException XA protocol error
     */
    public int getTransactionTimeout() throws XAException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "getTransactionTimeout for " + this);
        }
        return timeout;
    }

    /** Determine if the resource manager instance represented by the
     * target object is the same as the resource manager instance
     * represented by the parameter xares
     * @param xares An XAResource object
     * @return True if same RM instance, otherwise false.
     * @throws XAException XA protocol error
     */
    public boolean isSameRM(XAResource xares) throws XAException {


        // In this pseudo-driver, we must return true only if
        // both objects refer to the same XAResource, and not
        // the same Resource Manager, because actually, we must
        // send commit/rollback on each XAResource involved in
        // the transaction.
        if (xares.equals(this)) {
            if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
                loggerxa.log(BasicLevel.DEBUG, "isSameRM = true " + this);
            }
            return true;
        }
        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "isSameRM = false " + this);
        }
        return false;
    }


    /**
     * Ask the resource manager to prepare for a transaction commit
     * of the transaction specified in xid.
     * @param xid transaction xid
     * @throws XAException XA protocol error
     */
    public int prepare(Xid xid) throws XAException {

        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-PREPARE for " + xid);
        }
        // No 2PC on standard JDBC drivers
        return XA_OK;
    }

    /**
     * Obtain a list of prepared transaction branches from a resource
     * manager.
     * @return an array of transaction Xids
     * @throws XAException XA protocol error
     */
    public Xid[] recover(int flag) throws XAException {

        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-RECOVER for " + this);
        }
        // Not implemented
        return null;
    }

    /**
     * Inform the resource manager to roll back work done on behalf
     * of a transaction branch
     * @param xid transaction xid
     * @throws XAException XA protocol error
     */
    public void rollback(Xid xid) throws XAException {

        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-ROLLBACK for " + xid);
        }

        // Avoids a NPE.
        // This should not occur because a close could not be called
        // on a connection implied inside a transaction.
        if (actConn == null) {
            loggerxa.log(BasicLevel.ERROR, "rollback on a closed connection");
            return;
        }

        // Make sure that we are not in AutoCommit mode
        try {
            if (actConn.getAutoCommit() == true) {
                loggerxa.log(BasicLevel.ERROR, "Rollback called on XAResource with AutoCommit set");
                throw (new XAException(XAException.XA_HEURCOM));
            }
        } catch (SQLException e) {
            loggerxa.log(BasicLevel.ERROR, "Cannot getAutoCommit:" + e);
            notifyError(e);
            throw(new XAException("Error on getAutoCommit"));
        }

        // Rollback the transaction
        try {
            actConn.rollback();
        } catch (SQLException e) {
            loggerxa.log(BasicLevel.ERROR, "Cannot rollback transaction:" + e);
            notifyError(e);
            throw(new XAException("Error on rollback"));
        }
    }

    /**
     * Set the current transaction timeout value for this XAResource
     * instance.
     * @param seconds timeout value, in seconds.
     * @return always true
     * @throws XAException XA protocol error
     */
    public boolean setTransactionTimeout(int seconds) throws XAException {

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "setTransactionTimeout " + this);
        }
        timeout = seconds;
        return true;
    }

    /**
     * Start work on behalf of a transaction branch specified in xid
     * @param xid transaction xid
     * @throws XAException XA protocol error
     */
    public void start(Xid xid, int flags) throws XAException {

        if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
            loggerxa.log(BasicLevel.DEBUG, "XA-START for " + xid);
        }
    }

    //  -----------------------------------------------------------------
    //  XAConnection implementation
    //  -----------------------------------------------------------------

    /**
     * Return an XA resource to the caller.
     * @return The XAResource
     * @exception SQLException - if a database-access error occurs
     */
    public XAResource getXAResource() throws SQLException {
        return this;
    }

    //  -----------------------------------------------------------------
    // Comparable implementation
    //  -----------------------------------------------------------------

    public int compareTo(Object o) {
        JManagedConnection other = (JManagedConnection) o;
        int diff = psNumber() - other.psNumber();
        if (diff == 0) {
            return getIdent() - other.getIdent();
        } else {
            return diff;
        }
    }

    public int psNumber() {
        return hitCount;
    }

    //  -----------------------------------------------------------------
    //  PooledConnection implementation
    //  -----------------------------------------------------------------

    /**
     * Create an object handle for a database connection.
     * @exception SQLException - if a database-access error occurs
     */
    public Connection getConnection() throws SQLException {
        // Just return the already created object.
        return implConn;
    }

    /**
     * Close the database connection.
     *
     * @exception SQLException - if a database-access error occurs
     */
    public void close() throws SQLException {
        logger.log(BasicLevel.DEBUG, "");

        // Close the actual Connection here.
        if (actConn != null) {
            actConn.close();
        } else {
            logger.log(BasicLevel.ERROR, "Connection already closed");
        }
        actConn = null;
        implConn = null;
    }

    /**
     * Add an event listener.
     *
     * @param listener event listener
     */
    public void addConnectionEventListener(ConnectionEventListener listener) {
        logger.log(BasicLevel.DEBUG, "");
        eventListeners.addElement(listener);
    }

    /**
     * Remove an event listener.
     *
     * @param listener event listener
     */
    public void removeConnectionEventListener(ConnectionEventListener listener) {
        logger.log(BasicLevel.DEBUG, "");
        eventListeners.removeElement(listener);
    }

    /**
     *  implementation of resource manager event
     */
    public void enlistConnection(Transaction transaction) throws javax.transaction.SystemException  {
        try {
            if (rme) {
                if (implConn == null) {
                    loggerxa.log(BasicLevel.ERROR, "Cannot enlist a closed connection");
                    return;
                }
                implConn.setAutoCommit(false);
                // the tx value of the pool item does not match with the enlisted value !?
                enlistedInTx = transaction;
                // enlist the resource
                if (loggerxa.isLoggable(BasicLevel.DEBUG)) {
                    loggerxa.log(BasicLevel.DEBUG, "enlist XAResource on " + transaction);
                }
                transaction.enlistResource(getXAResource());
            }
        } catch (javax.transaction.RollbackException e) {
            javax.transaction.SystemException se = new javax.transaction.SystemException("Unexpected RollbackException exception");
            se.initCause(e);
            throw se;
        } catch (java.sql.SQLException e) {
            javax.transaction.SystemException se = new javax.transaction.SystemException("Unexpected SQL exception");
            se.initCause(e);
            throw se;
        }
    }

    /**
     *  synchronization implementation
     */
    public void beforeCompletion() {
        // nothing to do
    }

    /**
     *  synchronization implementation
     */
    public void afterCompletion(int status) {
        if (tx == null ) {
            loggerxa.log(BasicLevel.ERROR, "NO TX!");
        }
        ds.freeConnections(tx != null ? tx : enlistedInTx);
    }

    /**
     * @return true if connection max age has expired
     */
    public boolean isAged() {
        return (deathTime < System.currentTimeMillis());
    }

    /**
     * @return true if connection is still open
     */
    public boolean isOpen() {
        return (open > 0);
    }

    /**
     * @return open count
     */
    public int getOpenCount() {
        return open;
    }

    /**
     * Check if the connection has been unused for too long time.
     * This occurs usually when the caller forgot to call close().
     * @return true if open time has been reached, and not involved in a tx.
     */
    public boolean inactive() {
        return (open > 0 && tx == null && enlistedInTx == null && closeTime < System.currentTimeMillis());
    }

    /**
     * @return true if connection is closed
     */
    public boolean isClosed() {
        return (open <= 0);
    }

    /**
     * Notify as opened
     */
    public void hold() {
        open++;
        closeTime = System.currentTimeMillis() + ds.getMaxOpenTimeMilli();
    }

    /**
     * notify as closed
     * @return true if normal close.
     */
    public boolean release() {
        open--;
        if (open < 0) {
            logger.log(BasicLevel.WARN, "connection was already closed");
            open = 0;
            return false;
        }
        if (tx == null && open > 0) {
            logger.log(BasicLevel.ERROR, "connection-open counter overflow");
            open = 0;
        }

        // Close all PreparedStatement not already closed
        // When a Connection has been closed, no PreparedStatement should
        // remain open. This can avoids lack of cursor on some databases.
        if (open == 0 && pstmtmax > 0) {
            synchronized(psList) {
                JStatement jst = null;
                Iterator i = psList.values().iterator();
                while (i.hasNext()) {
                    jst = (JStatement) i.next();
                    jst.forceClose();
                }
            }
        }

        return true;
    }

    /**
     * Set the associated transaction
     * @param tx Transaction
     */
    public void setTx(Transaction tx) {
        this.tx = tx;
    }

    /**
     * @return the Transaction
     */
    public Transaction getTx() {
        return tx;
    }

    /**
     * @return true if registered as RME
     */
    public boolean isRME() {
        return rme;
    }

    /**
     * set/unset as RME
     */
    public void setRME(boolean rme) {
        this.rme = rme;
    }

    /**
     * remove this item, ignoring exception on close.
     */
    public void remove() {
        // Close the physical connection
        try {
            close();
        } catch (java.sql.SQLException ign) {
            logger.log(BasicLevel.ERROR, "Could not close Connection: ", ign);
        }

        // remove all references (for GC)
        tx = null;
        enlistedInTx = null;
    }

    //  -----------------------------------------------------------------
    //  Other methods
    //  -----------------------------------------------------------------

    /**
     * Try to find a PreparedStatement in the pool
     */
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
        throws SQLException {

        loggerps.log(BasicLevel.DEBUG, sql);
        if (pstmtmax == 0) {
            return actConn.prepareStatement(sql, resultSetType, resultSetConcurrency);
        }
        JStatement ps = null;
        synchronized (psList) {
            ps = (JStatement) psList.get(sql);
            if (ps != null) {
                if (! ps.isClosed()) {
                    loggerps.log(BasicLevel.WARN, "reuse an open pstmt");
                }
                ps.reuse();
                hitCount++;
            } else {
                // Not found in cache. Create a new one.
                PreparedStatement aps = actConn.prepareStatement(sql, resultSetType, resultSetConcurrency);
                ps = new JStatement(aps, this, sql);
                psList.put(sql, ps);
            }
        }
        return ps;
    }

    /**
     * Try to find a PreparedStatement in the pool
     */
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
    }

    /**
     * A PreparedStatement has been logically closed.
     * @return
     */
    public void notifyPsClose(JStatement ps) {
        loggerps.log(BasicLevel.DEBUG, ps.getSql());
        if (pstmtmax == 0) {
            return;
        }
        synchronized (psList) {
            if (psList.size() >= pstmtmax) {
                // Choose a closed element to remove.
                // TODO: Should be the LRU.
                JStatement lru = null;
                Iterator i = psList.values().iterator();
                while (i.hasNext()) {
                    lru = (JStatement) i.next();
                    if (lru.isClosed()) {
                        // actually, remove the first closed element.
                        i.remove();
                        lru.forget();
                        break;
                    }
                }
            }
        }
    }

    /*
     * get the RMID
     * @return the rmid.
     */
    public String getRMID() {
        return rmid;
    }
    /*
     * Notify a Close event on Connection
     */
    public void notifyClose() {
        logger.log(BasicLevel.DEBUG, "");

        // Notify event to listeners
        logger.log(BasicLevel.DEBUG, "");
        for (int i = 0; i < eventListeners.size(); i++) {
            ConnectionEventListener l = (ConnectionEventListener) eventListeners.elementAt(i);
            l.connectionClosed(new ConnectionEvent(this));
        }
    }

    /*
     * Notify an Error event on Connection
     */
    public void notifyError(SQLException ex) {
        logger.log(BasicLevel.DEBUG, "");

        // Notify event to listeners
        for (int i = 0; i < eventListeners.size(); i++) {
            ConnectionEventListener l = (ConnectionEventListener) eventListeners.elementAt(i);
            l.connectionErrorOccurred(new ConnectionEvent(this, ex));
        }
    }

}
