/* Copyright (c) 2001-2021, 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.HsqlNameManager.HsqlName;
import org.hsqldb.ParserDQL.CompileContext;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.OrderedHashSet;
import org.hsqldb.map.ValuePool;
import org.hsqldb.result.Result;
import org.hsqldb.types.Type;

/**
 * Implementation of Statement for PSM and trigger assignment.
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.5.0
 * @since 1.9.0
 */
public class StatementSet extends StatementDMQL {

    Expression expression;

    //
    Expression[] targets;
    int[]        variableIndexes;
    Type[]       sourceTypes;

    //
    final int               operationType;
    public static final int TRIGGER_SET  = 1;
    public static final int SELECT_INTO  = 2;
    public static final int VARIABLE_SET = 3;

    /**
     * Trigger SET statement.
     */
    StatementSet(Session session, Expression[] targets, Table table,
                 RangeVariable[] rangeVars, int[] indexes,
                 Expression[] colExpressions, CompileContext compileContext) {

        super(StatementTypes.ASSIGNMENT, StatementTypes.X_SQL_DATA_CHANGE,
              session.getCurrentSchemaHsqlName());

        this.operationType        = TRIGGER_SET;
        this.targets              = targets;
        this.targetTable          = table;
        this.baseTable            = targetTable.getBaseTable();
        this.updateColumnMap      = indexes;
        this.updateExpressions    = colExpressions;
        this.updateCheckColumns   = targetTable.getColumnCheckList(indexes);
        this.targetRangeVariables = rangeVars;
        isTransactionStatement    = false;

        setDatabaseObjects(session, compileContext);
        checkAccessRights(session);
    }

    /**
     * PSM and session variable SET
     */
    StatementSet(Session session, Expression[] targets, Expression e,
                 int[] indexes, CompileContext compileContext) {

        super(StatementTypes.ASSIGNMENT, StatementTypes.X_SQL_CONTROL, null);

        this.operationType     = VARIABLE_SET;
        this.targets           = targets;
        this.expression        = e;
        variableIndexes        = indexes;
        sourceTypes            = expression.getNodeDataTypes();
        isTransactionStatement = false;

        setDatabaseObjects(session, compileContext);
        checkAccessRights(session);
    }

    /**
     * Single row SELECT INTO
     */
    StatementSet(Session session, Expression[] targets, QueryExpression query,
                 int[] indexes, CompileContext compileContext) {

        super(StatementTypes.ASSIGNMENT, StatementTypes.X_SQL_CONTROL, null);

        this.operationType     = SELECT_INTO;
        this.queryExpression   = query;
        this.targets           = targets;
        variableIndexes        = indexes;
        sourceTypes            = query.getColumnTypes();
        isTransactionStatement = false;

        setDatabaseObjects(session, compileContext);
        checkAccessRights(session);
    }

    TableDerived[] getSubqueries(Session session) {

        if (queryExpression != null) {
            return super.getSubqueries(session);
        }

        OrderedHashSet subQueries = null;

        if (expression != null) {
            subQueries = expression.collectAllSubqueries(subQueries);
        }

        if (subQueries == null || subQueries.size() == 0) {
            return TableDerived.emptyArray;
        }

        TableDerived[] subQueryArray = new TableDerived[subQueries.size()];

        subQueries.toArray(subQueryArray);

        for (int i = 0; i < subqueries.length; i++) {
            subQueryArray[i].prepareTable(session);
        }

        return subQueryArray;
    }

    Result getResult(Session session) {

        Result result = null;

        switch (operationType) {

            case StatementSet.TRIGGER_SET :
                result = executeTriggerSetStatement(session);
                break;

            case StatementSet.SELECT_INTO : {
                Object[] values = queryExpression.getSingleRowValues(session);

                if (values == null) {
                    session.addWarning(HsqlException.noDataCondition);

                    result = Result.updateZeroResult;

                    break;
                }

                result = performAssignment(session, variableIndexes, targets,
                                           values, sourceTypes);

                break;
            }
            case StatementSet.VARIABLE_SET : {
                Object[] values = getExpressionValues(session);

                if (values == null) {
                    result = Result.updateZeroResult;

                    break;
                }

                result = performAssignment(session, variableIndexes, targets,
                                           values, sourceTypes);

                break;
            }
            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "StatementSet");
        }

        return result;
    }

    /**
     * extra names may be added to references
     */
    public void resolve(Session session) {

        switch (operationType) {

            case StatementSet.TRIGGER_SET :
                for (int i = 0; i < updateExpressions.length; i++) {
                    updateExpressions[i].collectObjectNames(references);
                }
                break;

            case StatementSet.SELECT_INTO :
            case StatementSet.VARIABLE_SET : {
                if (expression != null) {
                    expression.collectObjectNames(references);
                }

                break;
            }
            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "StatementSet");
        }
    }

    public String getSQL() {

        StringBuilder sb = new StringBuilder();

        switch (operationType) {

            case StatementSet.TRIGGER_SET :
                return sql;

            case StatementSet.VARIABLE_SET : {

                /* @todo - cover row assignment */
                sb.append(Tokens.T_SET).append(' ');
                sb.append(targets[0].getColumn().getName().statementName);
                sb.append(' ').append('=').append(' ').append(
                    expression.getSQL());

                break;
            }
        }

        return sb.toString();
    }

    protected String describe(Session session, int blanks) {

        StringBuilder sb = new StringBuilder();

        sb.append('\n');

        for (int i = 0; i < blanks; i++) {
            sb.append(' ');
        }

        sb.append(Tokens.T_STATEMENT);

        return sb.toString();
    }

    public Result execute(Session session) {

        Result result;

        try {
            if (subqueries.length > 0) {
                materializeSubQueries(session);
            }

            result = getResult(session);
        } catch (Throwable t) {
            result = Result.newErrorResult(t);
        }

        if (result.isError()) {
            result.getException().setStatementType(group, type);
        }

        return result;
    }

    public String describe(Session session) {
        return "";
    }

    Result executeTriggerSetStatement(Session session) {

        Table        table          = targetTable;
        int[]        colMap         = updateColumnMap;    // column map
        Expression[] colExpressions = updateExpressions;
        Type[]       colTypes       = table.getColumnTypes();
        int index = targetRangeVariables[TriggerDef.NEW_ROW].rangePosition;
        Object[]     oldData = session.sessionContext.triggerArguments[index];
        Object[]     data = (Object[]) ArrayUtil.duplicateArray(oldData);

        StatementDML.getUpdatedData(session, targets, table, colMap,
                                    colExpressions, colTypes, data);
        ArrayUtil.copyArray(data, oldData, data.length);

        return Result.updateOneResult;
    }

    // this fk references -> other  :  other read lock
    void collectTableNamesForRead(OrderedHashSet set) {

        if (queryExpression != null) {
            queryExpression.getBaseTableNames(set);
        }

        for (int i = 0; i < rangeVariables.length; i++) {
            Table    rangeTable = rangeVariables[i].rangeTable;
            HsqlName name       = rangeTable.getName();

            if (rangeTable.isDataReadOnly() || rangeTable.isTemp()) {
                continue;
            }

            if (rangeTable.isView()) {
                continue;
            }

            if (name.schema == SqlInvariants.SYSTEM_SCHEMA_HSQLNAME) {
                continue;
            }

            set.add(name);
        }

        for (int i = 0; i < subqueries.length; i++) {
            if (subqueries[i].queryExpression != null) {
                subqueries[i].queryExpression.getBaseTableNames(set);
            }
        }

        for (int i = 0; i < routines.length; i++) {
            set.addAll(routines[i].getTableNamesForRead());
        }
    }

    void collectTableNamesForWrite(OrderedHashSet set) {}

    public void checkIsNotColumnTarget() {

        for (int i = 0; i < targets.length; i++) {
            ColumnSchema col = targets[i].getColumn();

            if (col.getType() == SchemaObject.COLUMN) {
                throw Error.error(ErrorCode.X_0U000,
                                  col.getName().statementName);
            }
        }
    }

    Object[] getExpressionValues(Session session) {

        Object[] values;

        if (expression.getType() == OpTypes.ROW) {
            values = expression.getRowValue(session);
        } else if (expression.getType() == OpTypes.ROW_SUBQUERY) {
            values =
                expression.table.queryExpression.getSingleRowValues(session);

            if (values == null) {

                // todo - verify semantics
                return null;
            }
        } else {
            values    = new Object[1];
            values[0] = expression.getValue(session);
        }

        return values;
    }

    static Result performAssignment(Session session, int[] variableIndexes,
                                    Expression[] targets, Object[] values,
                                    Type[] sourceTypes) {

        for (int j = 0; j < values.length; j++) {
            Object[] data = ValuePool.emptyObjectArray;

            switch (targets[j].getColumn().getType()) {

                case SchemaObject.PARAMETER :
                    data = session.sessionContext.routineArguments;
                    break;

                case SchemaObject.VARIABLE :
                    data = session.sessionContext.routineVariables;
                    break;

                case SchemaObject.COLUMN :
                    data = session.sessionContext
                        .triggerArguments[TriggerDef.NEW_ROW];
                    break;
            }

            int    colIndex = variableIndexes[j];
            Object value    = values[j];
            Type   targetType;

            if (targets[j].getType() == OpTypes.ARRAY_ACCESS) {
                targetType =
                    targets[j].getLeftNode().getColumn().getDataType()
                        .collectionBaseType();
                value = targetType.convertToType(session, value,
                                                 sourceTypes[j]);
                data[colIndex] =
                    ((ExpressionAccessor) targets[j]).getUpdatedArray(session,
                        (Object[]) data[colIndex], value, true);
            } else {
                targetType = targets[j].getColumn().getDataType();
                value = targetType.convertToType(session, value,
                                                 sourceTypes[j]);
                data[colIndex] = value;
            }
        }

        return Result.updateZeroResult;
    }
}
