/*
 * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.jfr.event.compiler;

import jdk.internal.org.objectweb.asm.*;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordedMethod;
import jdk.jfr.consumer.RecordedObject;
import jdk.test.lib.Asserts;
import jdk.test.lib.Platform;
import jdk.test.lib.jfr.EventNames;
import jdk.test.lib.jfr.Events;
import sun.hotspot.WhiteBox;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.IntStream;

/**
 * @test CompilerInliningTest
 * @bug 8073607
 * @key jfr
 * @summary Verifies that corresponding JFR events are emitted in case of inlining.
 *
 *
 *
 * @library /test/lib /
 *

 *
 * @build sun.hotspot.WhiteBox
 * @run main ClassFileInstaller sun.hotspot.WhiteBox
 *     sun.hotspot.WhiteBox$WhiteBoxPermission
 * @run main/othervm -Xbootclasspath/a:.
 *     -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
 *     -Xbatch jdk.jfr.event.compiler.TestCompilerInlining
 */
public class TestCompilerInlining {
    private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox();
    private static final int LEVEL_SIMPLE = 1;
    private static final int LEVEL_FULL_OPTIMIZATION = 4;
    private static final Executable ENTRY_POINT = getConstructor(TestCase.class);
    private static final String TEST_CASE_CLASS_NAME = TestCase.class.getName().replace('.', '/');

    public static void main(String[] args) throws Exception {
        InlineCalls inlineCalls = new InlineCalls(TestCase.class);
        inlineCalls.disableInline(getConstructor(Object.class));
        inlineCalls.disableInline(getMethod(TestCase.class, "qux", boolean.class));
        inlineCalls.forceInline(getMethod(TestCase.class, "foo"));
        inlineCalls.forceInline(getMethod(TestCase.class, "foo", int.class));
        inlineCalls.forceInline(getMethod(TestCase.class, "bar"));
        inlineCalls.forceInline(getMethod(TestCase.class, "baz"));

        Map<Call, Boolean> result = inlineCalls.getExpected(ENTRY_POINT);
        for (int level : determineAvailableLevels()) {
            testLevel(result, level);
        }
    }

    private static void testLevel(Map<Call, Boolean> expectedResult, int level) throws IOException {
        System.out.println("****** Testing level " + level + " *******");
        Recording r = new Recording();
        r.enable(EventNames.CompilerInlining);
        r.start();
        WHITE_BOX.enqueueMethodForCompilation(ENTRY_POINT, level);
        WHITE_BOX.deoptimizeMethod(ENTRY_POINT);
        r.stop();
        System.out.println("Expected:");

        List<RecordedEvent> events = Events.fromRecording(r);
        Set<Call> foundEvents = new HashSet<>();
        int foundRelevantEvent = 0;
        for (RecordedEvent event : events) {
            RecordedMethod callerObject = event.getValue("caller");
            RecordedObject calleeObject = event.getValue("callee");
            MethodDesc caller = methodToMethodDesc(callerObject);
            MethodDesc callee = ciMethodToMethodDesc(calleeObject);
            // only TestCase.* -> TestCase.* OR TestCase.* -> Object.<init> are tested/filtered
            if (caller.className.equals(TEST_CASE_CLASS_NAME) && (callee.className.equals(TEST_CASE_CLASS_NAME)
                    || (callee.className.equals("java/lang/Object") && callee.methodName.equals("<init>")))) {
                System.out.println(event);
                boolean succeeded = (boolean) event.getValue("succeeded");
                int bci = Events.assertField(event, "bci").atLeast(0).getValue();
                Call call = new Call(caller, callee, bci);
                foundRelevantEvent++;
                Boolean expected = expectedResult.get(call);
                Asserts.assertNotNull(expected, "Unexpected inlined call : " + call);
                Asserts.assertEquals(expected, succeeded, "Incorrect result for " + call);
                Asserts.assertTrue(foundEvents.add(call), "repeated event for " + call);
            }
        }
        Asserts.assertEquals(foundRelevantEvent, expectedResult.size(), String.format("not all events found at lavel %d. " + "found = '%s'. expected = '%s'", level, events, expectedResult.keySet()));
        System.out.println();
        System.out.println();
    }

    private static int[] determineAvailableLevels() {
        if (WHITE_BOX.getBooleanVMFlag("TieredCompilation")) {
            return IntStream.rangeClosed(LEVEL_SIMPLE, WHITE_BOX.getIntxVMFlag("TieredStopAtLevel").intValue()).toArray();
        }
        if (Platform.isServer() && !Platform.isEmulatedClient()) {
            return new int[] { LEVEL_FULL_OPTIMIZATION };
        }
        if (Platform.isClient() || Platform.isEmulatedClient()) {
            return new int[] { LEVEL_SIMPLE };
        }
        throw new Error("TESTBUG: unknown VM");
    }

    private static MethodDesc methodToMethodDesc(RecordedMethod method) {
        String internalClassName = method.getType().getName().replace('.', '/');
        String methodName = method.getValue("name");
        String methodDescriptor = method.getValue("descriptor");
        return new MethodDesc(internalClassName, methodName, methodDescriptor);
    }

    private static MethodDesc ciMethodToMethodDesc(RecordedObject ciMethod) {
        String internalClassName = ciMethod.getValue("type");
        String methodName = ciMethod.getValue("name");
        String methodDescriptor = ciMethod.getValue("descriptor");
        return new MethodDesc(internalClassName, methodName, methodDescriptor);
    }

    private static Method getMethod(Class<?> aClass, String name, Class<?>... params) {
        try {
            return aClass.getDeclaredMethod(name, params);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new Error("TESTBUG : cannot get method " + name + Arrays.toString(params), e);
        }
    }

    private static Constructor<?> getConstructor(Class<?> aClass, Class<?>... params) {
        try {
            return aClass.getDeclaredConstructor(params);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new Error("TESTBUG : cannot get constructor" + Arrays.toString(params), e);
        }
    }
}

class TestCase {
    public TestCase() {
        foo();
    }

    public void foo() {
        qux(true);
        bar();
        foo(2);
    }

    private void foo(int i) {
    }

    private void bar() {
        baz();
        qux(false);
        qux(true);
    }

    protected static double baz() {
        qux(false);
        return .0;
    }

    private static int qux(boolean b) {
        qux(b);
        return 0;
    }
}

/**
 * data structure for method call
 */
class Call {
    public final MethodDesc caller;
    public final MethodDesc callee;
    public final int bci;

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || !(o instanceof Call))
            return false;

        Call call = (Call) o;

        if (bci != call.bci)
            return false;
        if (!callee.equals(call.callee))
            return false;
        if (!caller.equals(call.caller))
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = caller.hashCode();
        result = 31 * result + callee.hashCode();
        result = 47 * result + bci;
        return result;
    }

    public Call(MethodDesc caller, MethodDesc callee, int bci) {
        Objects.requireNonNull(caller);
        Objects.requireNonNull(callee);
        this.caller = caller;
        this.callee = callee;
        this.bci = bci;
    }

    @Override
    public String toString() {
        return String.format("Call{caller='%s', callee='%s', bci=%d}", caller, callee, bci);
    }
}

/**
 * data structure for method description
 */
class MethodDesc {
    public final String className;
    public final String methodName;
    public final String descriptor;

    public MethodDesc(Class<?> aClass, String methodName, String descriptor) {
        this(aClass.getName().replace('.', '/'), methodName, descriptor);
    }

    public MethodDesc(String className, String methodName, String descriptor) {
        Objects.requireNonNull(className);
        Objects.requireNonNull(methodName);
        Objects.requireNonNull(descriptor);
        this.className = className.replace('.', '/');
        this.methodName = methodName;
        this.descriptor = descriptor;
    }

    public MethodDesc(Executable executable) {
        Class<?> aClass = executable.getDeclaringClass();
        className = Type.getInternalName(aClass).replace('.', '/');

        if (executable instanceof Constructor<?>) {
            methodName = "<init>";
            descriptor = Type.getConstructorDescriptor((Constructor<?>) executable);
        } else {
            methodName = executable.getName();
            descriptor = Type.getMethodDescriptor((Method) executable);
        }

    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        MethodDesc that = (MethodDesc) o;

        if (!className.equals(that.className))
            return false;
        if (!methodName.equals(that.methodName))
            return false;
        if (!descriptor.equals(that.descriptor))
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = className.hashCode();
        result = 31 * result + methodName.hashCode();
        result = 47 * result + descriptor.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return String.format("MethodDesc{className='%s', methodName='%s', descriptor='%s'}", className, methodName, descriptor);
    }
}

/**
 * Aux class to get all calls in an arbitrary class.
 */
class InlineCalls {
    private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox();

    private final Collection<Call> calls;
    private final Map<Call, Boolean> inline;

    public InlineCalls(Class<?> aClass) {
        calls = getCalls(aClass);
        inline = new HashMap<>();
    }

    /**
     * @return expected inline events
     */
    public Map<Call, Boolean> getExpected(Executable entry) {
        Map<Call, Boolean> result = new HashMap<>();
        Queue<MethodDesc> methods = new ArrayDeque<>();
        Set<MethodDesc> finished = new HashSet<>();
        methods.add(new MethodDesc(entry));
        while (!methods.isEmpty()) {
            MethodDesc method = methods.poll();
            if (finished.add(method)) {
                inline.entrySet().stream().filter(k -> k.getKey().caller.equals(method)).forEach(k -> {
                    result.put(k.getKey(), k.getValue());
                    if (k.getValue()) {
                        methods.add(k.getKey().callee);
                    }
                });
            }
        }

        return result;
    }

    public void disableInline(Executable executable) {
        WHITE_BOX.testSetDontInlineMethod(executable, true);
        MethodDesc md = new MethodDesc(executable);
        calls.stream().filter(c -> c.callee.equals(md)).forEach(c -> inline.put(c, false));
    }

    public void forceInline(Executable executable) {
        WHITE_BOX.testSetForceInlineMethod(executable, true);
        MethodDesc md = new MethodDesc(executable);
        calls.stream().filter(c -> c.callee.equals(md)).forEach(c -> inline.putIfAbsent(c, true));
    }

    private static Collection<Call> getCalls(Class<?> aClass) {
        List<Call> calls = new ArrayList<>();
        ClassWriter cw;
        ClassReader cr;
        try {
            cr = new ClassReader(aClass.getName());
        } catch (IOException e) {
            throw new Error("TESTBUG : unexpected IOE during class reading", e);
        }
        cw = new ClassWriter(cr, 0);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String descriptor, String[] exceptions) {
                System.out.println("Method: " +name);
                MethodVisitor mv = super.visitMethod(access, name, desc, descriptor, exceptions);
                return new CallTracer(aClass, name, desc, mv, calls);
            }
        };
        cr.accept(cv, 0);

        return calls;
    }

    private static class CallTracer extends MethodVisitor {
        private final MethodDesc caller;
        private Collection<Call> calls;

        public CallTracer(Class<?> aClass, String name, String desc, MethodVisitor mv, Collection<Call> calls) {
            super(Opcodes.ASM5, mv);
            caller = new MethodDesc(aClass.getName(), name, desc);
            this.calls = calls;
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
            Label label = new Label();
            visitLabel(label);
            super.visitMethodInsn(opcode, owner, name, desc, itf);
            calls.add(new Call(caller, new MethodDesc(owner, name, desc), label.getOffset()));
        }
    }
}
