/*
 * Bitronix Transaction Manager
 *
 * Copyright (c) 2010, Bitronix Software.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program 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 distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA 02110-1301 USA
 */
package bitronix.tm.resource.jdbc;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;

/**
 * Caching {@link PreparedStatement} wrapper.
 * <p/>
 * This class is a proxy handler for a PreparedStatement.  It does not
 * implement the PreparedStatement interface or extend a class directly,
 * but you methods implemented here will override those of the
 * underlying delegate.  Simply implement a method with the same
 * signature, and the local method will be called rather than the delegate.
 * <p/>
 *
 * @author lorban, brettw
 */
public class JdbcPreparedStatementHandle extends BaseProxyHandlerClass { // implements PreparedStatement

    private PreparedStatement delegate;
    private boolean pretendClosed = false;

    // The 'parent' connection. Used to return the connection to the pool upon
    // close().
    private JdbcPooledConnection parentConnection;

    // Brett Wooldridge: the following must be taken into account when caching a
    // prepared statement. Defaults are per JDBC-specification.
    //
    // All of these attributes must match a proposed statement before the
    // statement can be considered "the same" and delivered from the cache.
    private final String sql;
    private int resultSetType = ResultSet.TYPE_FORWARD_ONLY;
    private int resultSetConcurrency = ResultSet.CONCUR_READ_ONLY;
    private Integer resultSetHoldability;
    private Integer autoGeneratedKeys;
    private int[] columnIndexes;
    private String[] columnNames;

    /*
      * PreparedStatement Constructors
      */

    public JdbcPreparedStatementHandle(String sql) {
        this.sql = sql;
    }

    public JdbcPreparedStatementHandle(String sql, int autoGeneratedKeys) {
        this.sql = sql;
        this.autoGeneratedKeys = new Integer(autoGeneratedKeys);
    }

    public JdbcPreparedStatementHandle(String sql, int resultSetType, int resultSetConcurrency) {
        this.sql = sql;
        this.resultSetType = resultSetType;
        this.resultSetConcurrency = resultSetConcurrency;
    }

    public JdbcPreparedStatementHandle(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) {
        this.sql = sql;
        this.resultSetType = resultSetType;
        this.resultSetConcurrency = resultSetConcurrency;
        this.resultSetHoldability = new Integer(resultSetHoldability);
    }

    public JdbcPreparedStatementHandle(String sql, int[] columnIndexes) {
        this.sql = sql;
        this.columnIndexes = new int[columnIndexes.length];
        System.arraycopy(columnIndexes, 0, this.columnIndexes, 0, columnIndexes.length);
    }

    public JdbcPreparedStatementHandle(String sql, String[] columnNames) {
        this.sql = sql;
        this.columnNames = new String[columnNames.length];
        System.arraycopy(columnNames, 0, this.columnNames, 0, columnNames.length);
    }

    /* java.sql.Wrapper implementation */

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

	public <T> T unwrap(Class<T> iface) throws SQLException {
        if (iface.isAssignableFrom(delegate.getClass())) {
            return (T) delegate;
	    }
	    throw new SQLException(getClass().getName() + " is not a wrapper for interface " + iface.getName());
	}

    /* Internal methods */

    /**
     * Set the parent connection that created this statement. We need this to
     * return the PreparedStatement to the pool.
     *
     * @param pooledConnection the parent JdbcPooledConnection
     */
    protected void setPooledConnection(JdbcPooledConnection pooledConnection) {
        this.parentConnection = pooledConnection;
    }

    protected JdbcPooledConnection getPooledConnection() {
        return parentConnection;
    }

    private PreparedStatement getDelegate() throws SQLException {
        if (pretendClosed)
            throw new SQLException("prepared statement closed");
        return delegate;
    }

    protected PreparedStatement getDelegateUnchecked() {
        return delegate;
    }

    protected void setDelegate(PreparedStatement delegate) {
        this.delegate = delegate;
    }

    public Object getProxiedDelegate() throws Exception {
        return getDelegate();
    }

    /* Overridden java.lang.Object methods */

    /**
     * Overridden equals() that takes all PreparedStatement attributes into
     * account.
     */
    public boolean equals(Object obj) {
        if (!(obj instanceof JdbcPreparedStatementHandle)) {
            return false;
        }

        JdbcPreparedStatementHandle otherStmt = (JdbcPreparedStatementHandle) obj;
        if (!sql.equals(otherStmt.sql)) {
            return false;
        } else if (resultSetType != otherStmt.resultSetType) {
            return false;
        } else if (resultSetConcurrency != otherStmt.resultSetConcurrency) {
            return false;
        } else if (!Arrays.equals(columnIndexes, otherStmt.columnIndexes)) {
            return false;
        } else if (!Arrays.equals(columnNames, otherStmt.columnNames)) {
            return false;
        } else if ((autoGeneratedKeys == null && otherStmt.autoGeneratedKeys != null) ||
                (autoGeneratedKeys != null && !autoGeneratedKeys.equals(otherStmt.autoGeneratedKeys))) {
            return false;
        } else if ((resultSetHoldability == null && otherStmt.resultSetHoldability != null) ||
                (resultSetHoldability != null && !resultSetHoldability.equals(otherStmt.resultSetHoldability))) {
            return false;
        }

        return true;
    }

    public int hashCode() {
        return sql != null ? sql.hashCode() : System.identityHashCode(this);
    }

    public String toString() {
        return "a JdbcPreparedStatementHandle with sql=[" + sql + "]";
    }

    /* Overridden methods of java.sql.PreparedStatement */

    public void close() throws SQLException {
        if (!pretendClosed) {
            // Clear the parameters so the next use of this cached statement
            // doesn't pick up unexpected values.
            delegate.clearParameters();
            // Return to cache so the usage count can be updated
            parentConnection.putCachedStatement(this);
        }

        pretendClosed = true;
    }

    public boolean isClosed() throws SQLException {
        return pretendClosed;
	}
}
