/*
 *  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.junit.jupiter.api.Test

import static org.codehaus.groovy.control.CompilerConfiguration.DEFAULT as config
import static org.junit.jupiter.api.Assumptions.assumeFalse

final class MethodPatternsTest extends AbstractBytecodeTestCase {

    @Test
    void testUnoptimizedIfWithNestedOptimizedLoop() {
        assumeFalse(config.indyEnabled)
        // in this example the if block contains statements that will not be optimized
        // but we still want to optimize the for loops, which can.
        // The test will check there is an optimized bytecode sequence for the loops.
        assert compile('''

            long sum = 0;
            double m = 1;

            if( true ) {

                System.err.println( "START");
                long t0 = System.currentTimeMillis();

                for( int j=0; j<1000; j++ ) {
                    for( int i=0; i<100_000; i++ ) {
                       sum = sum + i;
                       m = m*i;
                    }
                }

                long t1 = System.currentTimeMillis();
                System.err.println( "END - " + (t1-t0)+"ms");
            }

            System.err.println( "Done: "+sum+" "+m );
        ''').hasSequence([
                // for (int j=0; j<1000; j++) start and condition
                'ICONST_0',
                'SIPUSH 1000',
                'IF_ICMPGE',
                'ICONST_1',
                // for (int i=0; i<100_000; i++) start and condition
                'ICONST_0',
                'LDC 100000',
                'IF_ICMPGE',
                'ICONST_1',
                'GOTO',
                'ICONST_0',
                'IFEQ',
                // sum = sum + i
                'LLOAD',
                'ILOAD',
                'I2L',
                'LADD',
                // m = m * i
                'DLOAD',
                'ILOAD',
                'I2D',
                'DMUL',
                // for (int i=0; i<100_000; i++) increment
                'ILOAD',
                'ICONST_1',
                'IADD',
                'ISTORE',
                // for (int j=0; j<1000; j++) increment
                'ILOAD',
                'ICONST_1',
                'IADD',
                'ISTORE'
        ])
    }

    // make a test for native compilation of the ackerman function
    // and ensure the nested call is optimized
    @Test
    void testAckerman() {
        assumeFalse(config.indyEnabled)
        assert compile(method: 'A', '''
            int A(int x, int y) {
                if (x == 0) return y+1
                if (y == 0) return A(x-1, 1)
                return A(x-1, A(x, y-1))
            }
        ''').hasSequence([
            // if (x==0) return y+1
            'ILOAD 1',
            'ICONST_0',
            'IF_ICMPNE',
            'ICONST_1',
            'ICONST_0',
            'IFEQ',     // x==0 and branching till here
            'ILOAD 2',
            'ICONST_1',
            'IADD',     // y+1
            'IRETURN',  // return
            // if (y==0) return A(x-1,1)
            'ILOAD 2',
            'ICONST_0',
            'IF_ICMPNE',
            'ICONST_1',
            'ICONST_0',
            'IFEQ',     // y==0 and branching till here
            'ALOAD 0',
            'ILOAD 1',
            'ICONST_1',
            'ISUB',     // x-1 argument
            'ICONST_1',
            'INVOKEVIRTUAL script.A (II)I', // A(x-1,1)
            'IRETURN',  //return
            // return A(x-1,A(x,y-1))
            'ALOAD 0',
            'ILOAD 1',
            'ICONST_1',
            'ISUB',     // outer A x-1 argument
            'ALOAD 0',
            'ILOAD 1',  // inner A x argument
            'ILOAD 2',
            'ICONST_1',
            'ISUB',     //inner A y-1 argument
            'INVOKEVIRTUAL script.A (II)I', // inner A
            'INVOKEVIRTUAL script.A (II)I', // outer A
            'IRETURN' //return
        ])
    }

    @Test
    void testForLoopSettingArray() {
        assumeFalse(config.indyEnabled)
        assert compile('''
            int n = 10
            int[] x = new int[n]
            for (int i = 0; i < n; i++) x[i] = i + 1
        ''').hasSequence([
            'ILOAD',
            'ILOAD',
            'IF_ICMPGE',
            'ICONST_1',
            'GOTO',
            'ICONST_0',
            'IFEQ',
            'ILOAD',
            'ICONST_1',
            'IADD',
            'ISTORE',
            'ALOAD',
            'ILOAD',
            'ILOAD',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.intArraySet ([III)V',
            'ILOAD',
            'POP',
            'ILOAD',
            'DUP',
            'ISTORE',
            'ICONST_1',
            'IADD',
            'DUP',
            'ISTORE',
            'POP',
            'ILOAD',
            'POP',
            'GOTO',
        ])
    }

    @Test
    void testArrayIncrement() {
        assumeFalse(config.indyEnabled)
        assert compile('''
            int n = 10
            int[] x = new int[n]
            for (int i = 0; i < n; i++) x[i]++
        ''').hasSequence([
            'ICONST_0',
            'ISTORE',
            'ILOAD',
            'POP',
            'ILOAD',
            'ILOAD',
            'IF_ICMPGE',
            'ICONST_1',
            'GOTO',
            'ICONST_0',
            'IFEQ',
            'ILOAD',
            'ISTORE',
            'ALOAD',
            'ILOAD',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.intArrayGet ([II)I',
            'DUP',
            'ISTORE',
            'ICONST_1',
            'IADD',
            'ISTORE',
            'ALOAD',
            'ILOAD',
            'ILOAD',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.intArraySet ([III)V',
            'ILOAD',
            'POP',
            'ILOAD',
            'POP',
            'ILOAD',
            'DUP',
            'ISTORE',
            'ICONST_1',
            'IADD',
            'DUP',
            'ISTORE',
            'POP',
            'ILOAD',
            'POP',
            'GOTO',
        ])
    }

    @Test
    void testForLoopSettingArrayWithOperatorUsedInAssignmentAndArrayRHS() {
        assumeFalse(config.indyEnabled)
        assert compile('''
            int n = 10
            int[] x = new int[n]
            int[] y = new int[n]
            for (int i = 0; i < n; i++) x[i] += y[i]
        ''').hasSequence ([
            'ILOAD',
            'ILOAD',
            'IF_ICMPGE',
            'ICONST_1',
            'GOTO',
            'ICONST_0',
            'IFEQ',
            'ILOAD',
            'ISTORE',
            'ALOAD',
            'DUP',
            'ILOAD',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.intArrayGet ([II)I',
            'ALOAD',
            'ILOAD',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.intArrayGet ([II)I',
            'IADD',
            'DUP',
            'ISTORE',
            'ILOAD',
            'SWAP',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.intArraySet ([III)V',
            'ILOAD',
            'DUP',
            'ISTORE',
            'ICONST_1',
            'IADD',
            'ISTORE',
            'GOTO'
        ])
    }

    @Test
    void testRightShiftUnsignedWithLongArgument() {
        assumeFalse(config.indyEnabled)
        assert compile(method: "hashCode", '''
            class X{
                long _tagReservationDate
                String userId, partnerItemId, trackingTag
                public int hashCode() {
                    final int prime = 31;
                    int result = 1;
                    result = prime * result + (partnerItemId?.hashCode() ?: 0)
                    result = prime * result + (int) (_tagReservationDate ^ (_tagReservationDate >>> 32))
                    result = prime * result + (trackingTag?.hashCode() ?: 0)
                    result = prime * result + (userId?.hashCode() ?: 0)
                    return result;
                }
            }
        ''').hasSequence ([
            'IMUL',
            'ALOAD 0',
            'GETFIELD X._tagReservationDate : J',
            'ALOAD 0',
            'GETFIELD X._tagReservationDate : J',
            'BIPUSH 32',
            'LUSHR',
            'LXOR',
            'L2I',
            'IADD',
        ])
    }

    @Test
    void testObjectArraySet() {
        assumeFalse(config.indyEnabled)
        assert compile(method: "foo", '''
            class X {
                void foo() {
                    X[] xa = new X[1]
                    xa[0] = new X()
                }
            }
        ''').hasSequence ([
            'ICONST_0',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.objectArraySet ([Ljava/lang/Object;ILjava/lang/Object;)V',
        ])
    }

    @Test
    void testBooleanArraySet() {
        assumeFalse(config.indyEnabled)
        assert compile(method: "foo", '''
            class X{
                void foo() {
                    boolean[] xa = new boolean[1]
                    xa[0] = false
                }
            }
        ''').hasSequence ([
            'ICONST_0',
            'INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.zArraySet ([ZIZ)V',
        ])
    }

    @Test
    void testArray() {
        assumeFalse(config.indyEnabled)
        def methods = [
            "short"     :   [1, "sArraySet ([SIS)V", "sArrayGet ([SI)S"],
            "int"       :   [1, "intArraySet ([III)V", "intArrayGet ([II)I"],
            "boolean"   :   [false, "zArraySet ([ZIZ)V", "zArrayGet ([ZI)Z"],
            "long"      :   [1l, "lArraySet ([JIJ)V","lArrayGet ([JI)J"],
            "float"     :   [1f, "fArraySet ([FIF)V", "fArrayGet ([FI)F"],
            "byte"      :   [1, "bArraySet ([BIB)V", "bArrayGet ([BI)B"],
            "char"      :   [1, "cArraySet ([CIC)V", "cArrayGet ([CI)C"],
            "double"    :   [1d, "dArraySet ([DID)V", "dArrayGet ([DI)D"]
        ]
        methods.each {
            assert compile(method: "foo", """
                class X{
                  void foo() {
                    ${it.key}[] xa = new ${it.key}[1]
                    xa[0] = ${it.value[0]}
                  }
                }
            """).hasSequence ([
                'ICONST_0',
                "INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8."+it.value[1],
            ])

            assert compile(method: "foo", """
                class X{
                  ${it.key} foo() {
                    ${it.key}[] xa = new ${it.key}[1]
                    xa[0]
                  }
                }
            """).hasSequence ([
                'ICONST_0',
                "INVOKESTATIC org/codehaus/groovy/runtime/BytecodeInterface8.${it.value[2]}",
            ])
        }

        assertScript """
            a=[3:5]
            class B {
                int v;
            }
            B b = new B();
            b.v = 3

            clos = {
                if (it!=null) {
                    a[it.v] += 3
                }
            }
            clos.call(b)
            assert b.v == 3
            assert a[3] == 8
        """
    }

    @Test
    void testFib() {
        assumeFalse(config.indyEnabled)
        assert compile(method: "fib", """
            int fib(int i) {
                i < 2 ? 1 : fib(i - 2) + fib(i - 1)
            }
        """).hasSequence ([
            'ILOAD 1',
            'ICONST_2',
            'IF_ICMPGE',
            'ICONST_1',
            'GOTO',
            'ICONST_0',
            'IFEQ',
            'ICONST_1',
            'GOTO',
            'ALOAD 0',
            'ILOAD 1',
            'ICONST_2',
            'ISUB',
            'INVOKEVIRTUAL script.fib (I)I',
            'ALOAD 0',
            'ILOAD 1',
            'ICONST_1',
            'ISUB',
            'INVOKEVIRTUAL script.fib (I)I',
            'IADD',
            'IRETURN'
        ])

        // check that there is no fastpath for this method, since n is Object
        def seq = compile(method: "fib", """
            def fib(n) {
                n<=2L?n:fib(n-1L)+fib(n-2L)
            }
        """).toSequence()
        // isOrigXY is used for the fastpath guards
        assert !seq.contains("isOrig")
    }

    @Test
    void testNoBoxUnbox() {
        assumeFalse(config.indyEnabled)
        assert compile(method: "someCode", """
            public boolean someCall() {
                return true;
            }

            public boolean someCode() {
                boolean val = someCall()
            }
        """).hasSequence([
            'ALOAD',
            'INVOKEVIRTUAL script.someCall ()Z',
            'ISTORE',
            'ILOAD',
            'IRETURN',
        ])
    }

    @Test
    void testDiv() {
        assumeFalse(config.indyEnabled)
        def types = [
            "byte", "short", "int", "long", "double", "float"]
        types.each {type ->
            assert compile(method: "someCode","""
                def someCode() {
                    $type v = 5/4
                }
            """).hasSequence(["IDIV"])
        }
        types.each {type ->
            assert compile(method: "someCode","""
                def someCode() {
                    $type v = 0
                    v = 5/4
                }
            """).hasSequence(["IDIV"])
        }

        assert compile(method: "someCode", """
            def someCode() {
                long l = 5l/4l
            }
        """).hasSequence(["LDIV"])
        assert compile(method: "someCode", """
            def someCode() {
                long l
                l = 5l/4l
            }
        """).hasSequence(["LDIV"])
    }
}
