/** Copyright 2018 Nick nickl- Lombard
 *
 * Licensed 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 bsh;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;

class Operators implements ParserConstants {

    private static final List<Integer> OVERFLOW_OPS
        = Arrays.asList(PLUS, MINUS, STAR, POWER);
    private static final List<Integer> COMPARABLE_OPS
        = Arrays.asList(LT, LTX, GT, GTX, EQ, LE, LEX, GE, GEX, NE);

    /** Constructor private no instance required. */
    private Operators() {}

    /** Binary operations on arbitrary objects.
     * @param lhs left hand side value
     * @param rhs right hand side value
     * @param kind operator type
     * @return operator applied value
     * @throws UtilEvalError evaluation error */
    @SuppressWarnings("unchecked")
    public static Object arbitraryObjectsBinaryOperation(
        Object lhs, Object rhs, int kind)
        throws UtilEvalError
    {
        if ( kind == EQ )
            return (lhs == rhs) ? Primitive.TRUE : Primitive.FALSE;
        if ( kind == NE )
            return (lhs != rhs) ? Primitive.TRUE : Primitive.FALSE;

        if ( lhs == Primitive.VOID || rhs == Primitive.VOID )
            throw new UtilEvalError(
                "illegal use of undefined variable, class, or"
                    + " 'void' literal");

        if ( kind == PLUS ) {
            // String concatenation operation
            if ( lhs instanceof String || rhs instanceof String )
                return String.valueOf((Object) lhs)
                     + String.valueOf((Object) rhs);
            // array concatenation operation
            if ( lhs.getClass().isArray() && rhs instanceof List )
                rhs = ((List<?>) rhs).toArray();
            if ( lhs.getClass().isArray()
                    && rhs.getClass().isArray() )
                return BshArray.concat(lhs, rhs);
            // list concatenation operation
            if ( lhs instanceof List && rhs.getClass().isArray() )
                rhs = Types.castObject(rhs, List.class, Types.CAST);
            if ( lhs instanceof List && rhs instanceof List )
                return BshArray.concat(
                        (List<?>) lhs, (List<?>) rhs);
        }
        if ( kind == STAR ) {
            // array repeat operation
            if ( lhs.getClass().isArray() )
                return BshArray.repeat(lhs,
                        (int) Primitive.unwrap(rhs));
            if ( rhs.getClass().isArray() )
                return BshArray.repeat(rhs,
                        (int) Primitive.unwrap(lhs));
            // List repeat operation
            if ( lhs instanceof List )
                return BshArray.repeat((List<Object>) lhs,
                        (int) Primitive.unwrap(rhs));
            if ( rhs instanceof List )
                return BshArray.repeat((List<Object>) rhs,
                        (int) Primitive.unwrap(lhs));
        }

        if ( lhs instanceof String || rhs instanceof String )
            throw new UtilEvalError(
                "Use of non + operator with String" );
        if ( lhs.getClass().isArray() || rhs.getClass().isArray()
               || lhs instanceof List || rhs instanceof List)
            throw new UtilEvalError(
                "Use of invalid operator " + tokenImage[kind]
                    + " with array or List type" );
        if ( lhs == Primitive.NULL || rhs == Primitive.NULL )
            throw new UtilEvalError(
                "illegal use of null value or 'null' literal");

        throw new UtilEvalError("Operator: " + tokenImage[kind]
                    + " inappropriate for objects");
    }
    /**
    Perform a binary operation on two Primitives or wrapper types.
    If both original args were Primitives return a Primitive result
    else it was mixed (wrapper/primitive) return the wrapper type.
    The exception is for boolean operations where we will return the
    primitive type either way.
    */
    public static Object binaryOperation(Object obj1, Object obj2, int kind)
            throws UtilEvalError {

        // Unwrap primitives
        Object lhs = Primitive.unwrap(obj1);
        Object rhs = Primitive.unwrap(obj2);

        if ( Types.isNumeric(lhs) && Types.isNumeric(rhs) ) {
            Object[] operands = promotePrimitives(lhs, rhs);
            lhs = operands[0];
            rhs = operands[1];
        }

        if ( lhs.getClass() != rhs.getClass() )
            throw new UtilEvalError("Type mismatch in operator.  "
                    + lhs.getClass() + " cannot be used with " + rhs.getClass());

        Object result;
        try {
            result = binaryOperationImpl( lhs, rhs, kind );
        } catch (ArithmeticException e) {
            throw new UtilTargetError("Arithemetic Exception in binary op", e);
        }

        if ( result instanceof Boolean )
            return ((Boolean) result).booleanValue() ? Primitive.TRUE : Primitive.FALSE;

        // If both original args were Primitives return a Primitive result
        // else it was mixed (wrapper/primitive) return the wrapper type
        // Exception is for boolean result, return the primitive
        if ( obj1 instanceof Primitive && obj2 instanceof Primitive )
            if ( Types.isFloatingpoint(result) && lhs.getClass() == BigDecimal.class )
                return Primitive.wrap(result, result.getClass());
            else
                return Primitive.shrinkWrap(result);

        return Primitive.shrinkWrap(result).getValue();
    }

    @SuppressWarnings("unchecked")
    static <T> Object binaryOperationImpl( T lhs, T rhs, int kind )
        throws UtilEvalError
    {
        if (lhs instanceof Boolean)
            return booleanBinaryOperation( (Boolean) lhs, (Boolean) rhs, kind );
        if (COMPARABLE_OPS.contains(kind))
            return comparableBinaryBooleanOperations((Comparable<T>) lhs, rhs, kind);
        if (lhs instanceof BigInteger)
            return bigIntegerBinaryOperation( (BigInteger) lhs, (BigInteger) rhs, kind );
        if (lhs instanceof BigDecimal)
            return bigDecimalBinaryOperation( (BigDecimal) lhs, (BigDecimal) rhs, kind );
        if (Types.isFloatingpoint(lhs))
            return doubleBinaryOperation( (Double) lhs, (Double) rhs, kind );
        if (lhs instanceof Number)
            return longBinaryOperation( (Long) lhs, (Long) rhs, kind );
        throw new UtilEvalError("Invalid types in binary operator" );
    }

    static Boolean booleanBinaryOperation(Boolean B1, Boolean B2, int kind)
    {
        boolean lhs = B1.booleanValue();
        boolean rhs = B2.booleanValue();

        switch(kind)
        {
            case EQ:
                return lhs == rhs;

            case NE:
                return lhs != rhs;

            case BOOL_OR:
            case BOOL_ORX:
                // already evaluated lhs TRUE
                // see BSHBinaryExpression
                return false || rhs;

            case BOOL_AND:
            case BOOL_ANDX:
                // already evaluated lhs FALSE
                // see BSHBinaryExpression
                return true && rhs;

            case BIT_AND:
            case BIT_ANDX:
                return lhs & rhs;

            case BIT_OR:
            case BIT_ORX:
                return lhs | rhs;

            case XOR:
            case XORX:
                return lhs ^ rhs;

        }
        throw new InterpreterError("unimplemented binary operator");
    }

    static <T> Boolean comparableBinaryBooleanOperations(Comparable<T> lhs, T rhs, int kind) {
        switch(kind)
        {
            // boolean
            case LT:
            case LTX:
                return lhs.compareTo(rhs) < 0;

            case GT:
            case GTX:
                return lhs.compareTo(rhs) > 0;

            case LE:
            case LEX:
                return lhs.compareTo(rhs) <= 0;

            case GE:
            case GEX:
                return lhs.compareTo(rhs) >= 0;

            case NE:
                return lhs.compareTo(rhs) != 0;

            case EQ:
            default:
                return lhs.compareTo(rhs) == 0;
        }
    }

    // returns Object covering both Long and Boolean return types
    static Object longBinaryOperation(long lhs, long rhs, int kind)
    {
        switch(kind)
        {
            // arithmetic
            case PLUS:
                if ( lhs > 0 && (Long.MAX_VALUE - lhs) < rhs )
                    break;
                return lhs + rhs;

            case MINUS:
                if ( lhs < 0 && (Long.MIN_VALUE - lhs) > -rhs )
                    break;
                return lhs - rhs;

            case STAR:
                if ( lhs != 0 && Long.MAX_VALUE / lhs < rhs )
                    break;
                return lhs * rhs;

            case SLASH:
                return lhs / rhs;

            case MOD:
            case MODX:
                return lhs % rhs;

            case POWER:
            case POWERX:
                double check = Math.pow(lhs, rhs);
                BigInteger bi = BigDecimal.valueOf(check).toBigInteger();
                if ( bi.compareTo(Primitive.LONG_MIN) >= 0 && bi.compareTo(Primitive.LONG_MAX) <= 0 )
                    return (long) check;
                break;

            // bitwise
            case LSHIFT:
            case LSHIFTX:
                return lhs << rhs;

            case RSIGNEDSHIFT:
            case RSIGNEDSHIFTX:
                return lhs >> rhs;

            case RUNSIGNEDSHIFT:
            case RUNSIGNEDSHIFTX:
                return lhs >>> rhs;

            case BIT_AND:
            case BIT_ANDX:
                return lhs & rhs;

            case BIT_OR:
            case BIT_ORX:
                return lhs | rhs;

            case XOR:
            case XORX:
                return lhs ^ rhs;

        }
        if ( OVERFLOW_OPS.contains(kind) )
            return bigIntegerBinaryOperation(BigInteger.valueOf(lhs), BigInteger.valueOf(rhs), kind);

        throw new InterpreterError(
                "Unimplemented binary long operator");
    }

    // returns Object covering both Integer and Boolean return types
    static Object bigIntegerBinaryOperation(BigInteger lhs, BigInteger rhs, int kind)
    {

        switch(kind)
        {
            // arithmetic
            case PLUS:
                return lhs.add(rhs);

            case MINUS:
                return lhs.subtract(rhs);

            case STAR:
                return lhs.multiply(rhs);

            case SLASH:
                return lhs.divide(rhs);

            case MOD:
            case MODX:
                return lhs.mod(rhs);

            case POWER:
            case POWERX:
                return lhs.pow(rhs.intValue());

            // bitwise
            case LSHIFT:
            case LSHIFTX:
                return lhs.shiftLeft(rhs.intValue());

            case RSIGNEDSHIFT:
            case RSIGNEDSHIFTX:
                return lhs.shiftRight(rhs.intValue());

            case RUNSIGNEDSHIFT:
            case RUNSIGNEDSHIFTX:
                if ( lhs.signum() >= 0 )
                    return lhs.shiftRight(rhs.intValue());
                BigInteger opener = BigInteger.ONE.shiftLeft(lhs.toString(2).length() + 1);
                BigInteger opened = lhs.subtract(opener);
                BigInteger mask = opener.subtract(BigInteger.ONE).shiftRight(rhs.intValue() + 1);
                return opened.shiftRight(rhs.intValue()).and(mask);


            case BIT_AND:
            case BIT_ANDX:
                return lhs.and(rhs);

            case BIT_OR:
            case BIT_ORX:
                return lhs.or(rhs);

            case XOR:
            case XORX:
                return lhs.xor(rhs);

        }
        throw new InterpreterError(
                "Unimplemented binary integer operator");
    }

    // returns Object covering both Double and Boolean return types
    static Object doubleBinaryOperation(double lhs, double rhs, int kind)
        throws UtilEvalError
    {
        switch(kind)
        {
            // arithmetic
            case PLUS:
                if ( lhs > 0d && (Double.MAX_VALUE - lhs) < rhs )
                    break;
                return lhs + rhs;

            case MINUS:
                if ( lhs < 0d && (-Double.MAX_VALUE - lhs) > -rhs )
                    break;
                return lhs - rhs;

            case STAR:
                if ( lhs != 0 && Double.MAX_VALUE / lhs < rhs )
                    break;
                return lhs * rhs;

            case SLASH:
                return lhs / rhs;

            case MOD:
            case MODX:
                return lhs % rhs;

            case POWER:
            case POWERX:
                double check = Math.pow(lhs, rhs);
                if ( Double.isInfinite(check) )
                    break;
                return check;

            // can't shift floating-point values
            case LSHIFT:
            case LSHIFTX:
            case RSIGNEDSHIFT:
            case RSIGNEDSHIFTX:
            case RUNSIGNEDSHIFT:
            case RUNSIGNEDSHIFTX:
                throw new UtilEvalError("Can't shift floatingpoint values");

        }
        if ( OVERFLOW_OPS.contains(kind) )
            return bigDecimalBinaryOperation(BigDecimal.valueOf(lhs), BigDecimal.valueOf(rhs), kind);

        throw new InterpreterError(
                "Unimplemented binary double operator");
    }

    // returns Object covering both Long and Boolean return types
    static Object bigDecimalBinaryOperation(BigDecimal lhs, BigDecimal rhs, int kind)
        throws UtilEvalError
    {
        switch(kind)
        {
            // arithmetic
            case PLUS:
                return lhs.add(rhs);

            case MINUS:
                return lhs.subtract(rhs);

            case STAR:
                return lhs.multiply(rhs);

            case SLASH:
                return lhs.divide(rhs);

            case MOD:
            case MODX:
                return lhs.remainder(rhs);

            case POWER:
            case POWERX:
                return lhs.pow(rhs.intValue());

            // can't shift floats
            case LSHIFT:
            case LSHIFTX:
            case RSIGNEDSHIFT:
            case RSIGNEDSHIFTX:
            case RUNSIGNEDSHIFT:
            case RUNSIGNEDSHIFTX:
                throw new UtilEvalError("Can't shift floatingpoint values");

        }
        throw new InterpreterError(
                "Unimplemented binary float operator");
    }

    /**
        Promote primitive wrapper type to Integer wrapper type
    */
    static Number promoteToInteger(Object wrapper )
    {
        if ( wrapper instanceof Character )
            return Integer.valueOf(((Character) wrapper).charValue());
        if ( wrapper instanceof Byte || wrapper instanceof Short )
            return Integer.valueOf(((Number) wrapper).intValue());

        return (Number) wrapper;
    }

    /**
        Promote the pair of primitives to the maximum type of the two.
        e.g. [int,long]->[long,long]
    */
    static Object[] promotePrimitives(Object lhs, Object rhs)
    {
        Number lnum = promoteToInteger(lhs);
        Number rnum = promoteToInteger(rhs);

        if ( lhs instanceof BigDecimal ) {
            if ( !(rhs instanceof BigDecimal) )
                rhs = Primitive.castNumber(BigDecimal.class, rnum);
        } else if ( rhs instanceof BigDecimal ) {
            lhs = Primitive.castNumber(BigDecimal.class, lnum);
        } else if ( Types.isFloatingpoint(lhs) || Types.isFloatingpoint(rhs)) {
            if ( !(lhs instanceof Double) )
                lhs = Double.valueOf(lnum.doubleValue());
            if ( !(rhs instanceof Double) )
                rhs = Double.valueOf(rnum.doubleValue());
        } else if ( lhs instanceof BigInteger ) {
            if ( !(rhs instanceof BigInteger) )
                rhs = Primitive.castNumber(BigInteger.class, rnum);
        } else if ( rhs instanceof BigInteger ) {
            lhs = Primitive.castNumber(BigInteger.class, lnum);
        } else {
            if ( !(lhs instanceof Long) )
                lhs = Long.valueOf(lnum.longValue());
            if ( !(rhs instanceof Long) )
                rhs = Long.valueOf(rnum.longValue());
        }

        return new Object[] { lhs, rhs };
    }

    public static Primitive unaryOperation(Primitive val, int kind)
        throws UtilEvalError
    {
        if (val == Primitive.NULL)
            throw new UtilEvalError(
                "illegal use of null object or 'null' literal");
        if (val == Primitive.VOID)
            throw new UtilEvalError(
                "illegal use of undefined object or 'void' literal");

        Class<?> operandType = val.getType();

        if ( operandType == Boolean.TYPE )
            return booleanUnaryOperation((Boolean) val.getValue(), kind)
                ? Primitive.TRUE : Primitive.FALSE;

        Number operand = promoteToInteger(val.getValue());
        if(operand instanceof Integer)
        {
            int result = intUnaryOperation((Integer) operand, kind);

            // ++ and -- must be cast back the original type
            if(kind == INCR || kind == DECR)
            {
                if(operandType == Byte.TYPE)
                    return new Primitive((byte) result);
                if(operandType == Short.TYPE)
                    return new Primitive((short) result);
                if(operandType == Character.TYPE)
                    return new Primitive((char) result);
            }

            return new Primitive(result);
        }
        if(operand instanceof Long)
            return new Primitive(longUnaryOperation(operand.longValue(), kind));
        if(operand instanceof Float)
            return new Primitive(floatUnaryOperation(operand.floatValue(), kind));
        if(operand instanceof Double)
            return new Primitive(doubleUnaryOperation(operand.doubleValue(), kind));
        if(operand instanceof BigInteger)
            return new Primitive(bigIntegerUnaryOperation((BigInteger) operand, kind));
        if(operand instanceof BigDecimal)
            return new Primitive(bigDecimalUnaryOperation((BigDecimal) operand, kind));

        throw new InterpreterError(
            "An error occurred.  Please call technical support.");
    }

    static boolean booleanUnaryOperation(Boolean B, int kind)
        throws UtilEvalError
    {
        boolean operand = B.booleanValue();
        switch(kind)
        {
            case BANG:
                return !operand;
        }
        throw new UtilEvalError("Operator inappropriate for boolean");
    }

    static int intUnaryOperation(Integer I, int kind)
    {
        int operand = I.intValue();

        switch(kind)
        {
            case PLUS:
                return operand;
            case MINUS:
                return -operand;
            case TILDE:
                return ~operand;
            case INCR:
                return operand + 1;
            case DECR:
                return operand - 1;
        }
        throw new InterpreterError("bad integer unaryOperation");
    }

    static BigInteger bigIntegerUnaryOperation(BigInteger operand, int kind)
    {
        switch(kind)
        {
            case PLUS:
                return operand;
            case MINUS:
                return operand.negate();
            case TILDE:
                return operand.not();
            case INCR:
                return operand.add(BigInteger.ONE);
            case DECR:
                return operand.subtract(BigInteger.ONE);
        }
        throw new InterpreterError("bad big integer unaryOperation");
    }

    static BigDecimal bigDecimalUnaryOperation(BigDecimal operand, int kind)
    {
        switch(kind)
        {
            case PLUS:
                return operand;
            case MINUS:
                return operand.negate();
            case TILDE:
                return operand.signum() == 1 ? operand.negate() : operand;
            case INCR:
                return operand.add(BigDecimal.ONE);
            case DECR:
                return operand.subtract(BigDecimal.ONE);
        }
        throw new InterpreterError("bad big decimal unaryOperation");
    }

    static long longUnaryOperation(Long L, int kind)
    {
        long operand = L.longValue();

        switch(kind)
        {
            case PLUS:
                return operand;
            case MINUS:
                return -operand;
            case TILDE:
                return ~operand;
            case INCR:
                return operand + 1;
            case DECR:
                return operand - 1;
        }
        throw new InterpreterError("bad long unaryOperation");
    }

    static float floatUnaryOperation(Float F, int kind)
    {
        float operand = F.floatValue();

        switch(kind)
        {
            case PLUS:
                return operand;
            case MINUS:
                return -operand;
            case INCR:
                return operand + 1;
            case DECR:
                return operand - 1;
        }
        throw new InterpreterError("bad float unaryOperation");
    }

    static double doubleUnaryOperation(Double D, int kind)
    {
        double operand = D.doubleValue();

        switch(kind)
        {
            case PLUS:
                return operand;
            case MINUS:
                return -operand;
            case INCR:
                return operand + 1;
            case DECR:
                return operand - 1;
        }
        throw new InterpreterError("bad double unaryOperation");
    }
}