/* Copyright (c) 2001-2020, 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.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.HashMappedList;
import org.hsqldb.lib.HashSet;
import org.hsqldb.lib.OrderedHashSet;
import org.hsqldb.lib.OrderedIntHashSet;
import org.hsqldb.result.Result;
import org.hsqldb.result.ResultConstants;
import org.hsqldb.types.Type;

/**
 * Implementation of Statement for PSM compound statements.

 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.5.1
 * @since 1.9.0
 */
public class StatementCompound extends Statement implements RangeGroup {

    final boolean       isLoop;
    HsqlName            label;
    StatementHandler[]  handlers = StatementHandler.emptyExceptionHandlerArray;
    boolean             hasUndoHandler;
    StatementQuery      loopCursor;
    Statement[]         statements;
    StatementExpression condition;
    boolean             isAtomic;

    //
    ColumnSchema[]    variables      = ColumnSchema.emptyArray;
    StatementCursor[] cursors        = StatementCursor.emptyArray;
    HashMappedList    scopeVariables = new HashMappedList();
    RangeVariable[]   rangeVariables = RangeVariable.emptyArray;
    Table[]           tables         = Table.emptyArray;
    HashMappedList    scopeTables;

    //
    int variablesOffset;

    //
    public static final StatementCompound[] emptyStatementArray =
        new StatementCompound[]{};

    StatementCompound(int type, HsqlName label, StatementCompound parent) {

        super(type, StatementTypes.X_SQL_CONTROL);

        this.label             = label;
        isTransactionStatement = false;

        switch (type) {

            case StatementTypes.FOR :
            case StatementTypes.LOOP :
            case StatementTypes.WHILE :
            case StatementTypes.REPEAT :
                isLoop = true;
                break;

            case StatementTypes.BEGIN_END :
            case StatementTypes.IF :
                isLoop = false;
                break;

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

        this.parent = parent;
    }

    public String getSQL() {

/*
        StringBuilder sb = new StringBuilder();

        if (label != null) {
            sb.append(label.getStatementName()).append(':').append(' ');
        }

        switch (type) {
            case StatementTypes.FOR :
                // todo
                break;

            case StatementTypes.LOOP :
                sb.append(Tokens.T_LOOP).append(' ');

                for (int i = 0; i < statements.length; i++) {
                    sb.append(statements[i].getSQL()).append(';');
                }

                sb.append(Tokens.T_END).append(' ').append(Tokens.T_LOOP);
                break;

            case StatementTypes.WHILE :
                sb.append(Tokens.T_WHILE).append(' ');
                sb.append(condition.getSQL()).append(' ').append(Tokens.T_DO);
                sb.append(' ');

                for (int i = 0; i < statements.length; i++) {
                    sb.append(statements[i].getSQL()).append(';');
                }

                sb.append(Tokens.T_END).append(' ').append(Tokens.T_WHILE);
                break;

            case StatementTypes.REPEAT :
                sb.append(Tokens.T_REPEAT).append(' ');

                for (int i = 0; i < statements.length; i++) {
                    sb.append(statements[i].getSQL()).append(';');
                }

                sb.append(Tokens.T_UNTIL).append(' ');
                sb.append(condition.getSQL()).append(' ');
                sb.append(Tokens.T_END).append(' ').append(Tokens.T_REPEAT);
                break;

            case StatementTypes.BEGIN_END :
                sb.append(Tokens.T_BEGIN).append(' ').append(Tokens.T_ATOMIC);
                sb.append(' ');

                for (int i = 0; i < handlers.length; i++) {
                    sb.append(handlers[i].getSQL()).append(';');
                }

                for (int i = 0; i < variables.length; i++) {
                    sb.append(Tokens.T_DECLARE).append(' ');
                    sb.append(variables[i].getSQL());

                    if (variables[i].hasDefault()) {
                        sb.append(' ').append(Tokens.T_DEFAULT).append(' ');
                        sb.append(variables[i].getDefaultSQL());
                    }

                    sb.append(';');
                }

                for (int i = 0; i < statements.length; i++) {
                    sb.append(statements[i].getSQL()).append(';');
                }

                sb.append(Tokens.T_END);
                break;

            case StatementTypes.IF :
                for (int i = 0; i < statements.length; i++) {
                    if (statements[i].type == StatementTypes.CONDITION) {
                        if (i != 0) {
                            sb.append(Tokens.T_ELSE).append(' ');
                        }

                        sb.append(Tokens.T_IF).append(' ');
                        sb.append(statements[i].getSQL()).append(' ');
                        sb.append(Tokens.T_THEN).append(' ');
                    } else {
                        sb.append(statements[i].getSQL()).append(';');
                    }
                }

                sb.append(Tokens.T_END).append(' ').append(Tokens.T_IF);
                break;
        }

        return sb.toString();
*/
        return sql;
    }

    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();
    }

    boolean isLoop() {
        return isLoop;
    }

    void setLocalDeclarations(Object[] declarations) {

        int varCount     = 0;
        int handlerCount = 0;
        int cursorCount  = 0;
        int tableCount   = 0;

        for (int i = 0; i < declarations.length; i++) {
            if (declarations[i] instanceof ColumnSchema) {
                varCount++;
            } else if (declarations[i] instanceof StatementHandler) {
                handlerCount++;
            } else if (declarations[i] instanceof Table) {
                tableCount++;
            } else {
                cursorCount++;
            }
        }

        if (varCount > 0) {
            variables = new ColumnSchema[varCount];
        }

        if (handlerCount > 0) {
            handlers = new StatementHandler[handlerCount];
        }

        if (tableCount > 0) {
            tables = new Table[tableCount];
        }

        if (cursorCount > 0) {
            cursors = new StatementCursor[cursorCount];
        }

        varCount     = 0;
        handlerCount = 0;
        tableCount   = 0;
        cursorCount  = 0;

        for (int i = 0; i < declarations.length; i++) {
            if (declarations[i] instanceof StatementCursor) {
                StatementCursor cursor = (StatementCursor) declarations[i];

                cursors[cursorCount++] = cursor;
            } else if (declarations[i] instanceof ColumnSchema) {
                variables[varCount++] = (ColumnSchema) declarations[i];
            } else if (declarations[i] instanceof StatementHandler) {
                StatementHandler handler = (StatementHandler) declarations[i];

                handler.setParent(this);

                handlers[handlerCount++] = handler;

                if (handler.handlerType == StatementHandler.UNDO) {
                    hasUndoHandler = true;
                }
            } else if (declarations[i] instanceof Table) {
                Table table = (Table) declarations[i];

                tables[tableCount++] = table;
            }
        }

        setVariables();
        setHandlers();
        setTables();
        setCursors();
    }

    void setLoopStatement(HsqlName name, StatementQuery cursorStatement) {

        loopCursor = cursorStatement;

        HsqlName[] colNames =
            cursorStatement.queryExpression.getResultColumnNames();
        Type[] colTypes = cursorStatement.queryExpression.getColumnTypes();
        ColumnSchema[] columns = new ColumnSchema[colNames.length];

        for (int i = 0; i < colNames.length; i++) {
            columns[i] = new ColumnSchema(colNames[i], colTypes[i], false,
                                          false, null);

            columns[i].setParameterMode(SchemaObject.ParameterModes.PARAM_IN);
        }

        setLocalDeclarations(columns);
    }

    void setStatements(Statement[] statements) {

        for (int i = 0; i < statements.length; i++) {
            statements[i].setParent(this);
        }

        this.statements = statements;
    }

    void setCondition(StatementExpression condition) {
        this.condition = condition;
    }

    public Result execute(Session session) {

        Result result;

        switch (type) {

            case StatementTypes.BEGIN_END : {
                initialiseVariables(session);

                result = executeBlock(session);

                break;
            }
            case StatementTypes.FOR :
                result = executeForLoop(session);
                break;

            case StatementTypes.LOOP :
            case StatementTypes.WHILE :
            case StatementTypes.REPEAT : {
                result = executeLoop(session);

                break;
            }
            case StatementTypes.IF : {
                result = executeIf(session);

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

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

        return result;
    }

    private Result executeBlock(Session session) {

        Result  result = Result.updateZeroResult;
        boolean push   = !root.isTrigger();

        if (push) {
            session.sessionContext.push();

            if (hasUndoHandler) {
                String name = HsqlNameManager.getAutoSavepointNameString(
                    session.actionTimestamp, session.sessionContext.depth);

                session.savepoint(name);
            }
        }

        for (int i = 0; i < statements.length; i++) {
            result = executeProtected(session, statements[i]);
            result = handleCondition(session, result);

            if (result.isError()) {
                break;
            }

            if (result.getType() == ResultConstants.VALUE) {
                break;
            }

            if (result.getType() == ResultConstants.DATA) {
                break;
            }
        }

        if (result.getType() == ResultConstants.VALUE) {
            if (result.getErrorCode() == StatementTypes.LEAVE) {
                if (result.getMainString() == null) {
                    result = Result.updateZeroResult;
                } else if (label != null
                           && label.name.equals(result.getMainString())) {
                    result = Result.updateZeroResult;
                }
            }
        }

        if (push) {
            session.sessionContext.pop();
        }

        return result;
    }

    private Result handleCondition(Session session, Result result) {

        String sqlState = null;

        if (result.isError()) {
            sqlState = result.getSubString();
        } else if (session.getLastWarning() != null) {
            sqlState = session.getLastWarning().getSQLState();
        } else {
            return result;
        }

        if (sqlState != null) {
            for (int i = 0; i < handlers.length; i++) {
                StatementHandler handler = handlers[i];

                session.clearWarnings();

                /**
                 * @todo - if condition is "transaction rollback" promote to
                 * top call level without any further action
                 * if condition is system related promote to top level
                 * schema manipulation conditions are never handled
                 */
                if (handler.handlesCondition(sqlState)) {
                    String labelString = label == null ? null
                                                       : label.name;

                    switch (handler.handlerType) {

                        case StatementHandler.CONTINUE :
                            result = Result.updateZeroResult;
                            break;

                        case StatementHandler.UNDO :
                            if (session.sessionContext.savepoints.size() > 0) {
                                session.rollbackToSavepoint();
                            }

                            result = Result.newPSMResult(StatementTypes.LEAVE,
                                                         labelString, null);
                            break;

                        case StatementHandler.EXIT :
                            result = Result.newPSMResult(StatementTypes.LEAVE,
                                                         labelString, null);
                            break;
                    }

                    Result actionResult = executeProtected(session, handler);

                    if (actionResult.isError()) {
                        result = actionResult;

                        // parent should handle this
                    } else if (actionResult.getType()
                               == ResultConstants.VALUE) {
                        result = actionResult;
                    }
                }
            }

            if (result.isError() && parent != null) {

                // unhandled exception condition
                return parent.handleCondition(session, result);
            }
        }

        return result;
    }

    private Result executeForLoop(Session session) {

        Result queryResult = loopCursor.execute(session);

        if (queryResult.isError()) {
            return queryResult;
        }

        Result result = Result.updateZeroResult;

        while (queryResult.navigator.next()) {
            Object[] data = queryResult.navigator.getCurrent();

            initialiseVariables(session, data,
                                queryResult.metaData.getColumnCount());

            for (int i = 0; i < statements.length; i++) {
                result = executeProtected(session, statements[i]);
                result = handleCondition(session, result);

                if (result.isError()) {
                    break;
                }

                if (result.getType() == ResultConstants.VALUE) {
                    break;
                }

                if (result.getType() == ResultConstants.DATA) {
                    break;
                }
            }

            if (result.isError()) {
                break;
            }

            if (result.getType() == ResultConstants.VALUE) {
                if (result.getErrorCode() == StatementTypes.ITERATE) {
                    if (result.getMainString() == null) {
                        continue;
                    }

                    if (label != null
                            && label.name.equals(result.getMainString())) {
                        continue;
                    }

                    break;
                }

                if (result.getErrorCode() == StatementTypes.LEAVE) {
                    break;
                }

                // return
                break;
            }

            if (result.getType() == ResultConstants.DATA) {
                break;
            }
        }

        queryResult.navigator.release();

        return result;
    }

    private Result executeLoop(Session session) {

        Result result = Result.updateZeroResult;

        while (true) {
            if (type == StatementTypes.WHILE) {
                result = condition.execute(session);

                if (result.isError()) {
                    break;
                }

                if (!Boolean.TRUE.equals(result.getValueObject())) {
                    result = Result.updateZeroResult;

                    break;
                }
            }

            for (int i = 0; i < statements.length; i++) {
                result = executeProtected(session, statements[i]);
                result = handleCondition(session, result);

                if (result.getType() == ResultConstants.VALUE) {
                    break;
                }

                if (result.getType() == ResultConstants.DATA) {
                    break;
                }
            }

            if (result.isError()) {
                break;
            }

            if (result.getType() == ResultConstants.VALUE) {
                if (result.getErrorCode() == StatementTypes.ITERATE) {
                    if (result.getMainString() == null) {
                        continue;
                    }

                    if (label != null
                            && label.name.equals(result.getMainString())) {
                        continue;
                    }

                    break;
                }

                if (result.getErrorCode() == StatementTypes.LEAVE) {
                    if (result.getMainString() == null) {
                        result = Result.updateZeroResult;
                    }

                    if (label != null
                            && label.name.equals(result.getMainString())) {
                        result = Result.updateZeroResult;
                    }

                    break;
                }

                // return
                break;
            }

            if (result.getType() == ResultConstants.DATA) {
                break;
            }

            if (type == StatementTypes.REPEAT) {
                result = condition.execute(session);

                if (result.isError()) {
                    break;
                }

                if (Boolean.TRUE.equals(result.getValueObject())) {
                    result = Result.updateZeroResult;

                    break;
                }
            }
        }

        return result;
    }

    private Result executeIf(Session session) {

        Result  result  = Result.updateZeroResult;
        boolean execute = false;

        for (int i = 0; i < statements.length; i++) {
            if (statements[i].getType() == StatementTypes.CONDITION) {
                if (execute) {
                    break;
                }

                result = executeProtected(session, statements[i]);

                if (result.isError()) {
                    break;
                }

                Object value = result.getValueObject();

                execute = Boolean.TRUE.equals(value);

                i++;
            }

            result = Result.updateZeroResult;

            if (!execute) {
                continue;
            }

            result = executeProtected(session, statements[i]);
            result = handleCondition(session, result);

            if (result.isError()) {
                break;
            }

            if (result.getType() == ResultConstants.VALUE) {
                break;
            }
        }

        return result;
    }

    private Result executeProtected(Session session, Statement statement) {

        int actionIndex = session.rowActionList.size();
        long actionTimestamp =
            session.database.txManager.getNextGlobalChangeTimestamp();

        session.actionTimestamp = actionTimestamp;

        Result result = statement.execute(session);

        if (result.isError()) {
            session.rollbackAction(actionIndex, actionTimestamp);
        }

        return result;
    }

    public void resolve(Session session) {

        for (int i = 0; i < statements.length; i++) {
            if (statements[i].getType() == StatementTypes.LEAVE
                    || statements[i].getType() == StatementTypes.ITERATE) {
                if (!findLabel((StatementSimple) statements[i])) {
                    throw Error.error(
                        ErrorCode.X_42508,
                        ((StatementSimple) statements[i]).label.name);
                }

                continue;
            }

            if (statements[i].getType() == StatementTypes.RETURN) {
                if (!root.isFunction()) {
                    throw Error.error(ErrorCode.X_42602, Tokens.T_RETURN);
                }
            }
        }

        for (int i = 0; i < statements.length; i++) {
            statements[i].resolve(session);
        }

        for (int i = 0; i < handlers.length; i++) {
            handlers[i].resolve(session);
        }

        OrderedHashSet writeTableNamesSet = new OrderedHashSet();
        OrderedHashSet readTableNamesSet  = new OrderedHashSet();
        OrderedHashSet set                = new OrderedHashSet();

        for (int i = 0; i < variables.length; i++) {
            OrderedHashSet refs = variables[i].getReferences();

            if (refs != null) {
                set.addAll(refs);
            }
        }

        if (loopCursor != null) {
            set.addAll(loopCursor.getReferences());
            readTableNamesSet.addAll(loopCursor.getTableNamesForRead());
        }

        if (condition != null) {
            set.addAll(condition.getReferences());
            readTableNamesSet.addAll(condition.getTableNamesForRead());
        }

        for (int i = 0; i < statements.length; i++) {
            set.addAll(statements[i].getReferences());
            readTableNamesSet.addAll(statements[i].getTableNamesForRead());
            writeTableNamesSet.addAll(statements[i].getTableNamesForWrite());
        }

        for (int i = 0; i < handlers.length; i++) {
            set.addAll(handlers[i].getReferences());
            readTableNamesSet.addAll(handlers[i].getTableNamesForRead());
            writeTableNamesSet.addAll(handlers[i].getTableNamesForWrite());
        }

        readTableNamesSet.removeAll(writeTableNamesSet);

        readTableNames = new HsqlName[readTableNamesSet.size()];

        readTableNamesSet.toArray(readTableNames);

        writeTableNames = new HsqlName[writeTableNamesSet.size()];

        writeTableNamesSet.toArray(writeTableNames);

        references = set;
    }

    public void setRoot(Routine routine) {

        root = routine;
/*
        if (condition != null) {
            condition.setRoot(routine);
        }

        for (int i = 0; i < statements.length; i++) {
            statements[i].setRoot(routine);
        }
*/
    }

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

    public OrderedHashSet getReferences() {
        return references;
    }

    public void setAtomic(boolean atomic) {
        this.isAtomic = atomic;
    }

    //
    private void setVariables() {

        HashMappedList list = new HashMappedList();

        if (parent != null && parent.scopeVariables != null) {
            for (int i = 0; i < parent.scopeVariables.size(); i++) {
                list.add(parent.scopeVariables.getKey(i),
                         parent.scopeVariables.get(i));
            }
        }

        variablesOffset = list.size();

        for (int i = 0; i < variables.length; i++) {
            String  name  = variables[i].getName().name;
            boolean added = list.add(name, variables[i]);

            if (!added) {
                throw Error.error(ErrorCode.X_42606, name);
            }

            if (root.getParameterIndex(name) != -1) {
                throw Error.error(ErrorCode.X_42606, name);
            }
        }

        scopeVariables = list;

        RangeVariable[] parameterRangeVariables = root.getRangeVariables();
        RangeVariable range = new RangeVariable(list, null, true,
            RangeVariable.VARIALBE_RANGE);

        rangeVariables = new RangeVariable[parameterRangeVariables.length + 1];

        for (int i = 0; i < parameterRangeVariables.length; i++) {
            rangeVariables[i] = parameterRangeVariables[i];
        }

        rangeVariables[parameterRangeVariables.length] = range;

        if (list.size() > root.variableCount) {
            root.variableCount = list.size();
        }
    }

    private void setHandlers() {

        if (handlers.length == 0) {
            return;
        }

        HashSet           statesSet = new HashSet();
        OrderedIntHashSet typesSet  = new OrderedIntHashSet();

        for (int i = 0; i < handlers.length; i++) {
            int[] types = handlers[i].getConditionTypes();

            for (int j = 0; j < types.length; j++) {
                if (!typesSet.add(types[j])) {
                    throw Error.error(ErrorCode.X_42601);
                }
            }

            String[] states = handlers[i].getConditionStates();

            for (int j = 0; j < states.length; j++) {
                if (!statesSet.add(states[j])) {
                    throw Error.error(ErrorCode.X_42601);
                }
            }
        }
    }

    private void setTables() {

        if (tables.length == 0) {
            return;
        }

        HashMappedList list = new HashMappedList();

        if (parent != null && parent.scopeTables != null) {
            for (int i = 0; i < parent.scopeTables.size(); i++) {
                list.add(parent.scopeTables.getKey(i),
                         parent.scopeTables.get(i));
            }
        }

        for (int i = 0; i < tables.length; i++) {
            String  name  = tables[i].getName().name;
            boolean added = list.add(name, tables[i]);

            if (!added) {
                throw Error.error(ErrorCode.X_42606, name);
            }
        }

        scopeTables = list;
    }

    private void setCursors() {

        if (cursors.length == 0) {
            return;
        }

        HashSet list = new HashSet();

        for (int i = 0; i < cursors.length; i++) {
            StatementCursor cursor = cursors[i];
            boolean         added  = list.add(cursor.getCursorName().name);

            if (!added) {
                throw Error.error(ErrorCode.X_42606,
                                  cursor.getCursorName().name);
            }
        }
    }

    private boolean findLabel(StatementSimple statement) {

        if (label != null && statement.label.name.equals(label.name)) {
            if (!isLoop && statement.getType() == StatementTypes.ITERATE) {
                return false;
            }

            return true;
        }

        if (parent == null) {
            return false;
        }

        return parent.findLabel(statement);
    }

    private void initialiseVariables(Session session) {

        Object[] vars   = session.sessionContext.routineVariables;
        int      offset = parent == null ? 0
                                         : parent.scopeVariables.size();

        for (int i = 0; i < variables.length; i++) {
            try {
                vars[offset + i] = variables[i].getDefaultValue(session);
            } catch (HsqlException e) {}
        }
    }

    private void initialiseVariables(Session session, Object[] data,
                                     int count) {

        Object[] vars = session.sessionContext.routineVariables;

        for (int i = 0; i < count; i++) {
            vars[variablesOffset + i] = data[i];
        }
    }

    public RangeVariable[] getRangeVariables() {
        return rangeVariables;
    }

    public void setCorrelated() {

        //
    }

    public boolean isVariable() {
        return true;
    }
}
