/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

package org.codehaus.groovy.classgen.asm;

import org.apache.groovy.util.Maps;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.classgen.AsmClassGenerator;
import org.codehaus.groovy.runtime.BytecodeInterface8;

import java.util.Map;

import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.byte_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.char_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.double_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.float_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.long_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.short_TYPE;
import static org.codehaus.groovy.ast.tools.WideningCategories.isBigDecCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isDoubleCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isIntCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isLongCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isNumberCategory;
import static org.codehaus.groovy.syntax.TokenUtil.removeAssignment;
import static org.codehaus.groovy.syntax.Types.DIVIDE;
import static org.codehaus.groovy.syntax.Types.LEFT_SHIFT;
import static org.codehaus.groovy.syntax.Types.LEFT_SQUARE_BRACKET;
import static org.codehaus.groovy.syntax.Types.RIGHT_SHIFT;
import static org.codehaus.groovy.syntax.Types.RIGHT_SHIFT_UNSIGNED;

/**
 * This class is for internal use only!
 * This class will dispatch to the right type adapters according to the
 * kind of binary expression that is provided.
 */
public class BinaryExpressionMultiTypeDispatcher extends BinaryExpressionHelper {

    private static class BinaryCharExpressionHelper extends BinaryIntExpressionHelper {
        public BinaryCharExpressionHelper(final WriterController wc) {
            super(wc, charArraySet, charArrayGet);
        }
        private static final MethodCaller
            charArrayGet = MethodCaller.newStatic(BytecodeInterface8.class, "cArrayGet"),
            charArraySet = MethodCaller.newStatic(BytecodeInterface8.class, "cArraySet");
        @Override protected ClassNode getArrayGetResultType() { return char_TYPE; }
    }

    private static class BinaryByteExpressionHelper extends BinaryIntExpressionHelper {
        public BinaryByteExpressionHelper(final WriterController wc) {
            super(wc, byteArraySet, byteArrayGet);
        }
        private static final MethodCaller
            byteArrayGet = MethodCaller.newStatic(BytecodeInterface8.class, "bArrayGet"),
            byteArraySet = MethodCaller.newStatic(BytecodeInterface8.class, "bArraySet");
        @Override protected ClassNode getArrayGetResultType() { return byte_TYPE; }
    }

    private static class BinaryShortExpressionHelper extends BinaryIntExpressionHelper {
        public BinaryShortExpressionHelper(final WriterController wc) {
            super(wc, shortArraySet, shortArrayGet);
        }
        private static final MethodCaller
            shortArrayGet = MethodCaller.newStatic(BytecodeInterface8.class, "sArrayGet"),
            shortArraySet = MethodCaller.newStatic(BytecodeInterface8.class, "sArraySet");
        @Override protected ClassNode getArrayGetResultType() { return short_TYPE; }
    }

    protected BinaryExpressionWriter[] binExpWriter = initializeDelegateHelpers();

    protected BinaryExpressionWriter[] initializeDelegateHelpers() {
        return new BinaryExpressionWriter[]{
                /* 0: dummy  */ new BinaryObjectExpressionHelper(controller),
                /* 1: int    */ new BinaryIntExpressionHelper(controller),
                /* 2: long   */ new BinaryLongExpressionHelper(controller),
                /* 3: double */ new BinaryDoubleExpressionHelper(controller),
                /* 4: char   */ new BinaryCharExpressionHelper(controller),
                /* 5: byte   */ new BinaryByteExpressionHelper(controller),
                /* 6: short  */ new BinaryShortExpressionHelper(controller),
                /* 7: float  */ new BinaryFloatExpressionHelper(controller),
                /* 8: bool   */ new BinaryBooleanExpressionHelper(controller),
        };
    }

    public static final Map<ClassNode,Integer> typeMap = Maps.of(
        int_TYPE,     1,
        long_TYPE,    2,
        double_TYPE,  3,
        char_TYPE,    4,
        byte_TYPE,    5,
        short_TYPE,   6,
        float_TYPE,   7,
        boolean_TYPE, 8
    );
    public static final String[] typeMapKeyNames = {"dummy", "int", "long", "double", "char", "byte", "short", "float", "boolean"};

    public BinaryExpressionMultiTypeDispatcher(final WriterController wc) {
        super(wc);
    }

    private static int getOperandConversionType(final ClassNode leftType, final ClassNode rightType) {
        if (isIntCategory(leftType) && isIntCategory(rightType)) return 1;
        if (isLongCategory(leftType) && isLongCategory(rightType)) return 2;
        if (isBigDecCategory(leftType) && isBigDecCategory(rightType)) return 0;
        if (isDoubleCategory(leftType) && isDoubleCategory(rightType)) return 3;
        return 0;
    }

    protected int getOperandType(final ClassNode type) {
        Integer ret = typeMap.get(type);
        if (ret == null) return 0;
        return ret.intValue();
    }

    @Deprecated
    protected boolean doPrimtiveCompare(final ClassNode leftType, final ClassNode rightType, final BinaryExpression binExp) {
        return doPrimitiveCompare(leftType, rightType, binExp);
    }

    protected boolean doPrimitiveCompare(final ClassNode leftType, final ClassNode rightType, final BinaryExpression binExp) {
        Expression leftExp = binExp.getLeftExpression();
        Expression rightExp = binExp.getRightExpression();
        int operation = binExp.getOperation().getType();

        int operationType = getOperandConversionType(leftType,rightType);
        BinaryExpressionWriter bew = binExpWriter[operationType];

        if (!bew.write(operation, true)) return false;

        AsmClassGenerator acg = controller.getAcg();
        OperandStack os = controller.getOperandStack();
        leftExp.visit(acg);
        os.doGroovyCast(bew.getNormalOpResultType());
        rightExp.visit(acg);
        os.doGroovyCast(bew.getNormalOpResultType());
        bew.write(operation, false);

        return true;
    }

    @Override
    protected void evaluateCompareExpression(final MethodCaller compareMethod, final BinaryExpression binExp) {
        ClassNode current =  controller.getClassNode();
        TypeChooser typeChooser = controller.getTypeChooser();

        Expression leftExp = binExp.getLeftExpression();
        ClassNode leftType = typeChooser.resolveType(leftExp, current);
        Expression rightExp = binExp.getRightExpression();
        ClassNode rightType = typeChooser.resolveType(rightExp, current);

        if (!doPrimitiveCompare(leftType, rightType, binExp)) {
            super.evaluateCompareExpression(compareMethod, binExp);
        }
    }

    @Override
    protected void evaluateBinaryExpression(final String message, final BinaryExpression binExp) {
        final Expression lhsExp = binExp.getLeftExpression();
        final Expression rhsExp = binExp.getRightExpression();

        final ClassNode current = controller.getClassNode();
        final ClassNode lhsType = controller.getTypeChooser().resolveType(lhsExp, current);
        final ClassNode rhsType = controller.getTypeChooser().resolveType(rhsExp, current);

        var acg = controller.getAcg();
        var ops = controller.getOperandStack();
        int operation = removeAssignment(binExp.getOperation().getType());
        if (operation == LEFT_SQUARE_BRACKET) {
            var arrayItemType = lhsType.getComponentType();
            int operationType = getOperandType(arrayItemType);
            BinaryExpressionWriter bew = binExpWriter[operationType];
            if (lhsType.isArray() && !binExp.isSafe() && isNumberCategory(rhsType) && bew.arrayGet(operation, true)) {
                lhsExp.visit(acg);
                ops.doGroovyCast(lhsType);
                rhsExp.visit(acg);
                ops.doGroovyCast(int_TYPE);
                bew.arrayGet(operation, false);
                ops.replace(bew.getArrayGetResultType(), 2);
            } else {
                super.evaluateBinaryExpression(message, binExp);
            }
        } else if (operation == DIVIDE) {
            int operationType = getOperandType(controller.getTypeChooser().resolveType(binExp, current));
            BinaryExpressionWriter bew = binExpWriter[operationType];
            if (bew.writeDivision(true)) {
                lhsExp.visit(acg);
                ops.doGroovyCast(bew.getDevisionOpResultType());
                rhsExp.visit(acg);
                ops.doGroovyCast(bew.getDevisionOpResultType());
                bew.writeDivision(false);
            } else {
                super.evaluateBinaryExpression(message, binExp);
            }
        } else {
            int operationType = getOperandConversionType(lhsType, rhsType);
            BinaryExpressionWriter bew = binExpWriter[operationType];
            if (bew.write(operation, true)) {
                lhsExp.visit(acg);
                ops.doGroovyCast(bew.getNormalOpResultType());
                rhsExp.visit(acg);
                if (isShiftOperation(operation) && isNumberCategory(rhsType)) {
                    ops.doGroovyCast(int_TYPE);
                } else {
                    ops.doGroovyCast(bew.getNormalOpResultType());
                }
                bew.write(operation, false);
            } else {
                super.evaluateBinaryExpression(message, binExp);
            }
        }
    }

    private static boolean isShiftOperation(final int operation) {
        return operation == LEFT_SHIFT || operation == RIGHT_SHIFT || operation == RIGHT_SHIFT_UNSIGNED;
    }

    private static boolean isAssignmentToArray(final BinaryExpression binExp) {
        Expression leftExpression = binExp.getLeftExpression();
        if (!(leftExpression instanceof BinaryExpression)) return false;
        BinaryExpression leftBinExpr = (BinaryExpression) leftExpression;
        return leftBinExpr.getOperation().getType() == LEFT_SQUARE_BRACKET;
    }

    private boolean doAssignmentToArray(final BinaryExpression binExp) {
        if (!isAssignmentToArray(binExp)) return false;
        // we need to handle only assignment to arrays combined with an operation
        // special here. e.g x[a] += b

        int operation = removeAssignment(binExp.getOperation().getType());
        ClassNode current =  controller.getClassNode();

        Expression leftExp = binExp.getLeftExpression();
        ClassNode leftType = controller.getTypeChooser().resolveType(leftExp, current);
        Expression rightExp = binExp.getRightExpression();
        ClassNode rightType = controller.getTypeChooser().resolveType(rightExp, current);

        int operationType = getOperandType(leftType);
        BinaryExpressionWriter bew = binExpWriter[operationType];

        boolean simulationSuccess = bew.arrayGet(LEFT_SQUARE_BRACKET, true);
        simulationSuccess = simulationSuccess && bew.write(operation, true);
        simulationSuccess = simulationSuccess && bew.arraySet(true);
        if (!simulationSuccess) return false;

        AsmClassGenerator acg = controller.getAcg();
        OperandStack operandStack = controller.getOperandStack();
        CompileStack compileStack = controller.getCompileStack();

        // for x[a] += b we have the structure:
        //   x = left(left(binExp))), b = right(binExp), a = right(left(binExp)))
        // for array set we need these values on stack: array, index, right
        // for array get we need these values on stack: array, index
        // to eval the expression we need x[a] = x[a]+b
        // -> arraySet(x,a, x[a]+b)
        // -> arraySet(x,a, arrayGet(x,a,b))
        // --> x,a, x,a, b as operands
        // --> load x, load a, DUP2, call arrayGet, load b, call operation,call arraySet
        // since we cannot DUP2 here easily we will save the subscript and DUP x
        // --> sub=a, load x, DUP, load sub, call arrayGet, load b, call operation, load sub, call arraySet

        BinaryExpression arrayWithSubscript = (BinaryExpression) leftExp;
        Expression subscript = arrayWithSubscript.getRightExpression();

        // load array index: sub=a [load x, DUP, load sub, call arrayGet, load b, call operation, load sub, call arraySet]
        subscript.visit(acg);
        operandStack.doGroovyCast(int_TYPE);
        int subscriptValueId = compileStack.defineTemporaryVariable("$sub", int_TYPE, true);

        // load array: load x and DUP [load sub, call arrayGet, load b, call operation, load sub, call arraySet]
        arrayWithSubscript.getLeftExpression().visit(acg);
        operandStack.doGroovyCast(leftType.makeArray());
        operandStack.dup();

        // array get: load sub, call arrayGet [load b, call operation, load sub, call arraySet]
        operandStack.load(int_TYPE, subscriptValueId);
        bew.arrayGet(LEFT_SQUARE_BRACKET, false);
        operandStack.replace(leftType, 2);

        // complete rhs: load b, call operation [load sub, call arraySet]
        binExp.getRightExpression().visit(acg);
        if (! (bew instanceof BinaryObjectExpressionHelper)) {
            // in primopts we convert to the left type for supported binary operations
            operandStack.doGroovyCast(leftType);
        }
        bew.write(operation, false);

        // let us save that value for the return
        operandStack.dup();
        int resultValueId = compileStack.defineTemporaryVariable("$result", rightType, true);

        // array set: load sub, call arraySet []
        operandStack.load(int_TYPE, subscriptValueId);
        operandStack.swap();
        bew.arraySet(false);
        operandStack.remove(3); // 3 operands, the array, the index and the value!

        // load return value
        operandStack.load(rightType, resultValueId);

        // cleanup
        compileStack.removeVar(resultValueId);
        compileStack.removeVar(subscriptValueId);
        return true;
    }

    @Override
    protected void evaluateBinaryExpressionWithAssignment(final String method, final BinaryExpression binExp) {
        if (doAssignmentToArray(binExp)) return;
        if (doAssignmentToLocalVariable(method, binExp)) return;
        super.evaluateBinaryExpressionWithAssignment(method, binExp);
    }

    private boolean doAssignmentToLocalVariable(final String method, final BinaryExpression binExp) {
        Expression left = binExp.getLeftExpression();
        if (left instanceof VariableExpression) {
            VariableExpression ve = (VariableExpression) left;
            Variable v = ve.getAccessedVariable();
            if (v instanceof DynamicVariable) return false;
            if (v instanceof PropertyExpression) return false;
            /* field and declaration we don't return false */
        } else {
            return false;
        }

        evaluateBinaryExpression(method, binExp);
        controller.getOperandStack().dup();
        controller.getCompileStack().pushLHS(true);
        binExp.getLeftExpression().visit(controller.getAcg());
        controller.getCompileStack().popLHS();

        return true;
    }

    @Override
    protected void assignToArray(final Expression orig, final Expression receiver, final Expression index, final Expression rhsValueLoader, final boolean safe) {
        ClassNode current = controller.getClassNode();
        ClassNode arrayType = controller.getTypeChooser().resolveType(receiver, current);
        ClassNode arrayComponentType = arrayType.getComponentType();
        int operationType = getOperandType(arrayComponentType);
        BinaryExpressionWriter bew = binExpWriter[operationType];
        AsmClassGenerator acg = controller.getAcg();

        if (bew.arraySet(true) && arrayType.isArray() && !safe) {
            OperandStack operandStack   =   controller.getOperandStack();

            // load the array
            receiver.visit(acg);
            operandStack.doGroovyCast(arrayType);

            // load index
            index.visit(acg);
            operandStack.doGroovyCast(int_TYPE);

            // load rhs
            rhsValueLoader.visit(acg);
            operandStack.doGroovyCast(arrayComponentType);

            // store value in array
            bew.arraySet(false);

            // load return value && correct operand stack
            operandStack.remove(3);
            rhsValueLoader.visit(acg);
        } else {
            super.assignToArray(orig, receiver, index, rhsValueLoader, safe);
        }
    }

    @Override
    protected void writePostOrPrefixMethod(final int op, final String method, final Expression expression, final Expression orig) {
        ClassNode type = controller.getTypeChooser().resolveType(orig, controller.getClassNode());
        int operationType = getOperandType(type);
        BinaryExpressionWriter bew = binExpWriter[operationType];
        if (bew.writePostOrPrefixMethod(op, true)) {
            OperandStack operandStack = controller.getOperandStack();
            // at this point the receiver will be already on the stack
            operandStack.doGroovyCast(type);
            bew.writePostOrPrefixMethod(op, false);
            operandStack.replace(bew.getNormalOpResultType());
        } else {
            super.writePostOrPrefixMethod(op, method, expression, orig);
        }
    }
}
