/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.painless;

public class DefOptimizationTests extends ScriptTestCase {
    public void testIntBraceArrayOptiLoad() {
        final String script = "int x = 0; def y = new int[1]; y[0] = 5; x = y[0]; return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayLoad(Ljava/lang/Object;I)I");
        assertEquals(5, exec(script));
    }

    public void testIntBraceArrayOptiStore() {
        final String script = "int x = 1; def y = new int[1]; y[0] = x; return y[0];";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayStore(Ljava/lang/Object;II)");
        assertEquals(1, exec(script));
    }

    public void testIntBraceListOptiLoad() {
        final String script = "int x = 0; def y = new ArrayList(); y.add(5); x = y[0]; return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayLoad(Ljava/lang/Object;I)I");
        assertEquals(5, exec(script));
    }

    public void testIntBraceListOptiStore() {
        final String script = "int x = 1; def y = new ArrayList(); y.add(0); y[0] = x; return y[0];";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayStore(Ljava/lang/Object;II)");
        assertEquals(1, exec(script));
    }

    public void testIntBraceMapOptiLoad() {
        final String script = "int x = 0; def y = new HashMap(); y.put(0, 5); x = y[0];";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayLoad(Ljava/lang/Object;I)I");
        assertEquals(5, exec(script));
    }

    public void testIntBraceMapOptiStore() {
        final String script = "int x = 1; def y = new HashMap(); y.put(0, 1); y[0] = x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayStore(Ljava/lang/Object;II)");
        assertEquals(1, exec(script));
    }

    public void testIntFieldListOptiLoad() {
        final String script = "int x = 0; def y = new ArrayList(); y.add(5); x = y.0;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;)I");
        assertEquals(5, exec(script));
    }

    public void testIntFieldListOptiStore() {
        final String script = "int x = 1; def y = new ArrayList(); y.add(0); y.0 = x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;I)");
        assertEquals(1, exec(script));
    }

    public void testIntFieldMapOptiLoad() {
        final String script = "int x = 0; def y = new HashMap(); y.put('0', 5); x = y.0; return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;)I");
        assertEquals(5, exec(script));
    }

    public void testIntFieldMapOptiStore() {
        final String script = "int x = 1; def y = new HashMap(); y.put('0', 1); y.0 = x; return y.0;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;I)");
        assertEquals(1, exec(script));
    }

    public void testIntCall0Opti() {
        final String script = "int x; def y = new HashMap(); y['int'] = 1; x = y.get('int'); return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC get(Ljava/lang/Object;Ljava/lang/String;)I");
        assertEquals(1, exec(script));
    }

    public void testIntCall1Opti() {
        final String script = "int x; def y = new HashMap(); y['int'] = 1; x = y.get('int');";
        assertBytecodeExists(script, "INVOKEDYNAMIC get(Ljava/lang/Object;Ljava/lang/String;)I");
        assertEquals(1, exec(script));
    }

    public void testDoubleBraceArrayOptiLoad() {
        final String script = "double x = 0; def y = new double[1]; y[0] = 5.0; x = y[0]; return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC arrayLoad(Ljava/lang/Object;I)D");
        assertEquals(5.0, exec(script));
    }

    public void testDoubleBraceArrayOptiStore() {
        final String script = "double x = 1; def y = new double[1]; y[0] = x; return y[0];";

        assertBytecodeExists(script, "INVOKEDYNAMIC arrayStore(Ljava/lang/Object;ID)");
        assertEquals(1.0, exec(script));
    }

    public void testDoubleBraceListOptiLoad() {
        final String script = "double x = 0.0; def y = new ArrayList(); y.add(5.0); x = y[0]; return x;";

        assertBytecodeExists(script, "INVOKEDYNAMIC arrayLoad(Ljava/lang/Object;I)D");
        assertEquals(5.0, exec(script));
    }

    public void testDoubleBraceListOptiStore() {
        final String script = "double x = 1.0; def y = new ArrayList(); y.add(0.0); y[0] = x; return y[0];";

        assertBytecodeExists(script, "INVOKEDYNAMIC arrayStore(Ljava/lang/Object;ID)");
        assertEquals(1.0, exec(script));
    }

    public void testDoubleBraceMapOptiLoad() {
        final String script = "double x = 0.0; def y = new HashMap(); y.put(0, 5.0); x = y[0];";

        assertBytecodeExists(script, "INVOKEDYNAMIC arrayLoad(Ljava/lang/Object;I)D");
        assertEquals(5.0, exec(script));
    }

    public void testDoubleBraceMapOptiStore() {
        final String script = "double x = 1.0; def y = new HashMap(); y.put(0, 2.0); y[0] = x;";

        assertBytecodeExists(script, "INVOKEDYNAMIC arrayStore(Ljava/lang/Object;ID)");
        assertEquals(1.0, exec(script));
    }

    public void testDoubleFieldListOptiLoad() {
        final String script = "double x = 0; def y = new ArrayList(); y.add(5.0); x = y.0;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;)D");
        assertEquals(5.0, exec(script));
    }

    public void testDoubleFieldListOptiStore() {
        final String script = "double x = 1.0; def y = new ArrayList(); y.add(0); y.0 = x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;D)");
        assertEquals(1.0, exec(script));
    }

    public void testDoubleFieldMapOptiLoad() {
        final String script = "double x = 0; def y = new HashMap(); y.put('0', 5.0); x = y.0; return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;)D");
        assertEquals(5.0, exec(script));
    }

    public void testDoubleFieldMapOptiStore() {
        final String script = "double x = 1.0; def y = new HashMap(); y.put('0', 1.0); y.0 = x; return y.0;";
        assertBytecodeExists(script, "INVOKEDYNAMIC 0(Ljava/lang/Object;D)");
        assertEquals(1.0, exec(script));
    }

    public void testDoubleCall0Opti() {
        final String script = "double x; def y = new HashMap(); y['double'] = 1.0; x = y.get('double'); return x;";
        assertBytecodeExists(script, "INVOKEDYNAMIC get(Ljava/lang/Object;Ljava/lang/String;)D");
        assertEquals(1.0, exec(script));
    }

    public void testDoubleCall1Opti() {
        final String script = "double x; def y = new HashMap(); y['double'] = 1.0; x = y.get('double');";
        assertBytecodeExists(script, "INVOKEDYNAMIC get(Ljava/lang/Object;Ljava/lang/String;)D");
        assertEquals(1.0, exec(script));
    }

    public void testIllegalCast() {
        final String script = "int x;\ndef y = new HashMap();\ny['double'] = 1.0;\nx = y.get('double');\n";
        assertBytecodeExists(script, "INVOKEDYNAMIC get(Ljava/lang/Object;Ljava/lang/String;)I");

        final Exception exception = expectScriptThrows(ClassCastException.class, () -> {
            exec(script);
        });
        assertTrue(exception.getMessage().contains("Cannot cast java.lang.Double to java.lang.Integer"));
    }
    
    public void testMulOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x * y", 
                             "INVOKEDYNAMIC mul(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testMulOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x * y", 
                             "INVOKEDYNAMIC mul(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testMulOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x * y", 
                             "INVOKEDYNAMIC mul(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testDivOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x / y", 
                             "INVOKEDYNAMIC div(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testDivOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x / y", 
                             "INVOKEDYNAMIC div(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testDivOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x / y", 
                             "INVOKEDYNAMIC div(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testRemOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x % y", 
                             "INVOKEDYNAMIC rem(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testRemOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x % y", 
                             "INVOKEDYNAMIC rem(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testRemOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x % y", 
                             "INVOKEDYNAMIC rem(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testAddOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x + y", 
                             "INVOKEDYNAMIC add(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testAddOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x + y", 
                             "INVOKEDYNAMIC add(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testAddOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x + y", 
                             "INVOKEDYNAMIC add(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    // horrible, sorry
    public void testAddOptNullGuards() {
        // needs null guard
        assertBytecodeHasPattern("def x = 1; def y = 2; return x + y", 
                "(?s).*INVOKEDYNAMIC add.*arguments:\\s+" + "\\d+"
                                                + ",\\s+" + DefBootstrap.BINARY_OPERATOR 
                                                + ",\\s+" + DefBootstrap.OPERATOR_ALLOWS_NULL + ".*");
        // still needs null guard, NPE is the wrong thing!
        assertBytecodeHasPattern("def x = 1; def y = 2; double z = x + y", 
                "(?s).*INVOKEDYNAMIC add.*arguments:\\s+" + "\\d+"
                                                + ",\\s+" + DefBootstrap.BINARY_OPERATOR
                                                + ",\\s+" + DefBootstrap.OPERATOR_ALLOWS_NULL + ".*");
        // a primitive argument is present: no null guard needed
        assertBytecodeHasPattern("def x = 1; int y = 2; return x + y", 
                "(?s).*INVOKEDYNAMIC add.*arguments:\\s+" + "\\d+"
                                                + ",\\s+" + DefBootstrap.BINARY_OPERATOR
                                                + ",\\s+" + 0 + ".*");
        assertBytecodeHasPattern("int x = 1; def y = 2; return x + y", 
                "(?s).*INVOKEDYNAMIC add.*arguments:\\s+" + "\\d+"
                                                + ",\\s+" + DefBootstrap.BINARY_OPERATOR
                                                + ",\\s+" + 0 + ".*");
    }
    
    public void testSubOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x - y", 
                             "INVOKEDYNAMIC sub(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testSubOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x - y", 
                             "INVOKEDYNAMIC sub(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testSubOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x - y", 
                             "INVOKEDYNAMIC sub(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testLshOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x << y", 
                             "INVOKEDYNAMIC lsh(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testLshOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x << y", 
                             "INVOKEDYNAMIC lsh(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testLshOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x << y", 
                             "INVOKEDYNAMIC lsh(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testRshOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x >> y", 
                             "INVOKEDYNAMIC rsh(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testRshOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x >> y", 
                             "INVOKEDYNAMIC rsh(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testRshOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x >> y", 
                             "INVOKEDYNAMIC rsh(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testUshOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x >>> y", 
                             "INVOKEDYNAMIC ush(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testUshOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x >>> y", 
                             "INVOKEDYNAMIC ush(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testUshOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x >>> y", 
                             "INVOKEDYNAMIC ush(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testAndOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x & y", 
                             "INVOKEDYNAMIC and(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testAndOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x & y", 
                             "INVOKEDYNAMIC and(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testAndOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x & y", 
                             "INVOKEDYNAMIC and(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testOrOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x | y", 
                             "INVOKEDYNAMIC or(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testOrOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x | y", 
                             "INVOKEDYNAMIC or(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testOrOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x | y", 
                             "INVOKEDYNAMIC or(Ljava/lang/Object;Ljava/lang/Object;)D");
    }
    
    public void testXorOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x ^ y", 
                             "INVOKEDYNAMIC xor(ILjava/lang/Object;)Ljava/lang/Object;");
    }
    
    public void testXorOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x ^ y", 
                             "INVOKEDYNAMIC xor(Ljava/lang/Object;I)Ljava/lang/Object;");
    }
    
    public void testXorOptRet() {
        assertBytecodeExists("def x = 1; def y = 2; double d = x ^ y", 
                             "INVOKEDYNAMIC xor(Ljava/lang/Object;Ljava/lang/Object;)D");
    }

    public void testBooleanXorOptLHS() {
        assertBytecodeExists("boolean x = true; def y = true; return x ^ y", 
                "INVOKEDYNAMIC xor(ZLjava/lang/Object;)Ljava/lang/Object;");
    }

    public void testBooleanXorOptRHS() {
        assertBytecodeExists("def x = true; boolean y = true; return x ^ y", 
                "INVOKEDYNAMIC xor(Ljava/lang/Object;Z)Ljava/lang/Object;");
    }
    
    public void testBooleanXorOptRet() {
        assertBytecodeExists("def x = true; def y = true; boolean v = x ^ y", 
                "INVOKEDYNAMIC xor(Ljava/lang/Object;Ljava/lang/Object;)Z");
    }

    public void testLtOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x < y", 
                             "INVOKEDYNAMIC lt(ILjava/lang/Object;)Z");
    }
    
    public void testLtOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x < y", 
                             "INVOKEDYNAMIC lt(Ljava/lang/Object;I)Z");
    }
    
    public void testLteOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x <= y", 
                             "INVOKEDYNAMIC lte(ILjava/lang/Object;)Z");
    }
    
    public void testLteOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x <= y", 
                             "INVOKEDYNAMIC lte(Ljava/lang/Object;I)Z");
    }
    
    public void testEqOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x == y", 
                             "INVOKEDYNAMIC eq(ILjava/lang/Object;)Z");
    }
    
    public void testEqOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x == y", 
                             "INVOKEDYNAMIC eq(Ljava/lang/Object;I)Z");
    }
    
    public void testNeqOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x != y", 
                             "INVOKEDYNAMIC eq(ILjava/lang/Object;)Z");
    }
    
    public void testNeqOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x != y", 
                             "INVOKEDYNAMIC eq(Ljava/lang/Object;I)Z");
    }
    
    public void testGteOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x >= y", 
                             "INVOKEDYNAMIC gte(ILjava/lang/Object;)Z");
    }
    
    public void testGteOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x >= y", 
                             "INVOKEDYNAMIC gte(Ljava/lang/Object;I)Z");
    }
    
    public void testGtOptLHS() {
        assertBytecodeExists("int x = 1; def y = 2; return x > y", 
                             "INVOKEDYNAMIC gt(ILjava/lang/Object;)Z");
    }
    
    public void testGtOptRHS() {
        assertBytecodeExists("def x = 1; int y = 2; return x > y", 
                             "INVOKEDYNAMIC gt(Ljava/lang/Object;I)Z");
    }
    
    public void testUnaryMinusOptRet() {
        assertBytecodeExists("def x = 1; double y = -x; return y", 
                             "INVOKEDYNAMIC neg(Ljava/lang/Object;)D");
    }
    
    public void testUnaryNotOptRet() {
        assertBytecodeExists("def x = 1; double y = ~x; return y", 
                             "INVOKEDYNAMIC not(Ljava/lang/Object;)D");
    }
    
    public void testUnaryPlusOptRet() {
        assertBytecodeExists("def x = 1; double y = +x; return y", 
                             "INVOKEDYNAMIC plus(Ljava/lang/Object;)D");
    }
    
    public void testLambdaReturnType() {
        assertBytecodeExists("List l = new ArrayList(); l.removeIf(x -> x < 10)",
                             "synthetic lambda$0(Ljava/lang/Object;)Z");
    }
    
    public void testLambdaArguments() {
        assertBytecodeExists("List l = new ArrayList(); l.stream().mapToDouble(Double::valueOf).map(x -> x + 1)",
                             "synthetic lambda$0(D)D");
    }
}
