/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import org.hsqldb.jdbc.jdbcResultSet;
import org.hsqldb.lib.HashMappedList;
import org.hsqldb.lib.HsqlArrayList;
import org.hsqldb.lib.java.JavaSystem;

// boucherb@users 200404xx - fixed broken CALL statement result set unwrapping;
//                           fixed broken support for prepared SELECT...INTO

/**
 * Provides execution of CompiledStatement objects. <p>
 *
 * If multiple threads access a CompiledStatementExecutor.execute()
 * concurrently, they must be synchronized externally, relative to both
 * this object's Session and the Session's Database object. Internally, this
 * is accomplished in Session.execute() by synchronizing on the Session
 * object's Database object.
 *
 * @author  boucherb@users
 * @version 1.7.2
 * @since 1.7.2
 */
final class CompiledStatementExecutor {

    private Session session;
    private Result  updateResult;
    private static Result emptyZeroResult =
        new Result(ResultConstants.UPDATECOUNT);
    private static Result updateOneResult =
        new Result(ResultConstants.UPDATECOUNT);

    static {
        updateOneResult.updateCount = 1;
    }

    /**
     * Creates a new instance of CompiledStatementExecutor.
     *
     * @param session the context in which to perform the execution
     */
    CompiledStatementExecutor(Session session) {
        this.session = session;
        updateResult = new Result(ResultConstants.UPDATECOUNT);
    }

    /**
     * Executes a generic CompiledStatement. Execution includes first building
     * any subquery result dependencies and clearing them after the main result
     * is built.
     *
     * @return the result of executing the statement
     * @param cs any valid CompiledStatement
     */
    Result execute(CompiledStatement cs, Object[] paramValues) {

        Result result = null;

        JavaSystem.gc();

        for (int i = 0; i < cs.parameters.length; i++) {
            cs.parameters[i].bind(paramValues[i]);
        }

        try {
            cs.materializeSubQueries(session);

            result = executeImpl(cs);
        } catch (Throwable t) {
            result = new Result(t, cs.sql);
        }

        // clear redundant data
        cs.dematerializeSubQueries(session);

        if (result == null) {
            result = emptyZeroResult;
        }

        return result;
    }

    /**
     * Executes a generic CompiledStatement. Execution excludes building
     * subquery result dependencies and clearing them after the main result
     * is built.
     *
     * @param cs any valid CompiledStatement
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeImpl(CompiledStatement cs) throws HsqlException {

        switch (cs.type) {

            case CompiledStatement.SELECT :
                return executeSelectStatement(cs);

            case CompiledStatement.INSERT_SELECT :
                return executeInsertSelectStatement(cs);

            case CompiledStatement.INSERT_VALUES :
                return executeInsertValuesStatement(cs);

            case CompiledStatement.UPDATE :
                return executeUpdateStatement(cs);

            case CompiledStatement.DELETE :
                return executeDeleteStatement(cs);

            case CompiledStatement.CALL :
                return executeCallStatement(cs);

            case CompiledStatement.DDL :
                return executeDDLStatement(cs);

            default :
                throw Trace.runtimeError(
                    Trace.UNSUPPORTED_INTERNAL_OPERATION,
                    "CompiledStatementExecutor.executeImpl()");
        }
    }

    /**
     * Executes a CALL statement.  It is assumed that the argument is
     * of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.CALL
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeCallStatement(CompiledStatement cs)
    throws HsqlException {

        Expression e = cs.expression;          // representing CALL
        Object     o = e.getValue(session);    // expression return value
        Result     r;

        if (o instanceof Result) {
            return (Result) o;
        } else if (o instanceof jdbcResultSet) {
            return ((jdbcResultSet) o).rResult;
        }

        r = Result.newSingleColumnResult(CompiledStatement.RETURN_COLUMN_NAME,
                                         e.getDataType());

        Object[] row = new Object[1];

        row[0]                   = o;
        r.metaData.classNames[0] = e.getValueClassName();

        r.add(row);

        return r;
    }

// fredt - currently deletes that fail due to referential constraints are caught prior to
// actual delete operation, so no nested transaction is required

    /**
     * Executes a DELETE statement.  It is assumed that the argument is
     * of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.DELETE
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeDeleteStatement(CompiledStatement cs)
    throws HsqlException {

        Table       table  = cs.targetTable;
        TableFilter filter = cs.targetFilter;
        int         count  = 0;

        if (filter.findFirst(session)) {
            Expression    c = cs.condition;
            HsqlArrayList del;

            del = new HsqlArrayList();

            do {
                if (c == null || c.testCondition(session)) {
                    del.add(filter.currentRow);
                }
            } while (filter.next(session));

            count = table.delete(session, del);
        }

        updateResult.updateCount = count;

        return updateResult;
    }

    /**
     * Executes an INSERT_SELECT statement.  It is assumed that the argument
     * is of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.INSERT_SELECT
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeInsertSelectStatement(CompiledStatement cs)
    throws HsqlException {

        Table     t   = cs.targetTable;
        Select    s   = cs.select;
        int[]     ct  = t.getColumnTypes();    // column types
        Result    r   = s.getResult(session, Integer.MAX_VALUE);
        Record    rc  = r.rRoot;
        int[]     cm  = cs.columnMap;          // column map
        boolean[] ccl = cs.checkColumns;       // column check list
        int       len = cm.length;
        Object[]  row;
        int       count;
        boolean   success = false;

        session.beginNestedTransaction();

        try {
            while (rc != null) {
                row = t.getNewRowData(session, ccl);

                for (int i = 0; i < len; i++) {
                    int j = cm[i];

                    if (ct[j] != r.metaData.colTypes[i]) {
                        row[j] = Column.convertObject(rc.data[i], ct[j]);
                    } else {
                        row[j] = rc.data[i];
                    }
                }

                rc.data = row;
                rc      = rc.next;
            }

            count   = t.insert(session, r);
            success = true;
        } finally {
            session.endNestedTransaction(!success);
        }

        updateResult.updateCount = count;

        return updateResult;
    }

    /**
     * Executes an INSERT_VALUES statement.  It is assumed that the argument
     * is of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.INSERT_VALUES
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeInsertValuesStatement(CompiledStatement cs)
    throws HsqlException {

        Table        t    = cs.targetTable;
        Object[]     row  = t.getNewRowData(session, cs.checkColumns);
        int[]        cm   = cs.columnMap;        // column map
        Expression[] acve = cs.columnValues;
        Expression   cve;
        int[]        ct = t.getColumnTypes();    // column types
        int          ci;                         // column index
        int          len = acve.length;

        for (int i = 0; i < len; i++) {
            cve     = acve[i];
            ci      = cm[i];
            row[ci] = cve.getValue(session, ct[ci]);
        }

        t.insert(session, row);

        return updateOneResult;
    }

    /**
     * Executes a SELECT statement.  It is assumed that the argument
     * is of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.SELECT
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeSelectStatement(CompiledStatement cs)
    throws HsqlException {

        Select select = cs.select;
        Result result;

        if (select.sIntoTable != null) {

            // session level user rights
            session.checkDDLWrite();

            boolean exists =
                session.database.schemaManager.findUserTable(
                    session, select.sIntoTable.name,
                    select.sIntoTable.schema.name) != null;

            if (exists) {
                throw Trace.error(Trace.TABLE_ALREADY_EXISTS,
                                  select.sIntoTable.name);
            }

            result = select.getResult(session, Integer.MAX_VALUE);
            result = session.dbCommandInterpreter.processSelectInto(result,
                    select.sIntoTable, select.intoType);

            session.getDatabase().setMetaDirty(false);
        } else {
            result = select.getResult(session, session.getMaxRows());
        }

        return result;
    }

    /**
     * Executes an UPDATE statement.  It is assumed that the argument
     * is of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.UPDATE
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeUpdateStatement(CompiledStatement cs)
    throws HsqlException {

        Table       table  = cs.targetTable;
        TableFilter filter = cs.targetFilter;
        int         count  = 0;

        if (filter.findFirst(session)) {
            int[]          colmap    = cs.columnMap;    // column map
            Expression[]   colvalues = cs.columnValues;
            Expression     condition = cs.condition;    // update condition
            int            len       = colvalues.length;
            HashMappedList rowset    = new HashMappedList();
            int            size      = table.getColumnCount();
            int[]          coltypes  = table.getColumnTypes();
            boolean        success   = false;

            do {
                if (condition == null || condition.testCondition(session)) {
                    try {
                        Row      row = filter.currentRow;
                        Object[] ni  = table.getEmptyRowData();

                        System.arraycopy(row.getData(), 0, ni, 0, size);

                        for (int i = 0; i < len; i++) {
                            int ci = colmap[i];

                            ni[ci] = colvalues[i].getValue(session,
                                                           coltypes[ci]);
                        }

                        rowset.add(row, ni);
                    } catch (HsqlInternalException e) {}
                }
            } while (filter.next(session));

            session.beginNestedTransaction();

            try {
                count   = table.update(session, rowset, colmap);
                success = true;
            } finally {

                // update failed (constraint violation) or succeeded
                session.endNestedTransaction(!success);
            }
        }

        updateResult.updateCount = count;

        return updateResult;
    }

    /**
     * Executes a DDL statement.  It is assumed that the argument
     * is of the correct type.
     *
     * @param cs a CompiledStatement of type CompiledStatement.DDL
     * @throws HsqlException if a database access error occurs
     * @return the result of executing the statement
     */
    private Result executeDDLStatement(CompiledStatement cs)
    throws HsqlException {
        return session.sqlExecuteDirectNoPreChecks(cs.sql);
    }
}
