/*
 * Copyright (c) 2015, 2016, 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.
 *
 * 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 compiler.jvmci.compilerToVM;

import compiler.jvmci.common.CTVMUtilities;
import compiler.testlibrary.CompilerUtils;
import jdk.test.lib.util.Pair;
import jdk.test.lib.Utils;
import jdk.vm.ci.code.InstalledCode;
import sun.hotspot.WhiteBox;
import sun.hotspot.code.NMethod;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A test case for tests which require compiled code.
 */
public class CompileCodeTestCase {
    private static final WhiteBox WB = WhiteBox.getWhiteBox();
    private static final int COMP_LEVEL;
    static {
        int[] levels = CompilerUtils.getAvailableCompilationLevels();
        if (levels.length == 0) {
            throw new Error("TESTBUG: no compilers available");
        }
        COMP_LEVEL = levels[levels.length - 1];
    }
    private static final Class<?>[] CLASSES = {
            Interface.class,
            Dummy.class,
            DummyEx.class};
    private static final Map<Class<?>, Object> RECEIVERS;

    public final Object receiver;
    public final Executable executable;
    public final int bci;
    private final boolean isOsr;

    public CompileCodeTestCase(Object receiver, Executable executable,
            int bci) {
        this.receiver = receiver;
        this.executable = executable;
        this.bci = bci;
        isOsr = (bci >= 0);
    }

    public NMethod compile() {
        return compile(COMP_LEVEL);
    }

    public Pair<Object, ? extends Throwable> invoke(Object[] args) {
        boolean old = executable.isAccessible();
        executable.setAccessible(true);
        try {
            try {
                if (executable instanceof Method) {
                    Method m = (Method) executable;
                    return new Pair<>(m.invoke(receiver, args), null);
                }

                if (executable instanceof Constructor) {
                    Constructor c = (Constructor) executable;
                    return new Pair<>(c.newInstance(args), null);
                }
            } catch (InvocationTargetException e) {
                return new Pair<>(null, e.getCause());
            } catch (Throwable e) {
                return new Pair<>(null, e);
            }
        } finally {
            executable.setAccessible(old);
        }
        throw new Error(executable + " has unsupported type "
                + executable.getClass());
    }

    public NMethod compile(int level) {
        String directive = "[{ match: \"" + executable.getDeclaringClass().getName().replace('.', '/')
                + "." + (executable instanceof Constructor ? "<init>" : executable.getName())
                + "\", " + "BackgroundCompilation: false }]";
        if (WB.addCompilerDirective(directive) != 1) {
            throw new Error("Failed to add compiler directive: " + directive);
        }
        boolean enqueued = WB.enqueueMethodForCompilation(executable,
                level, bci);
        if (!enqueued) {
            throw new Error(String.format(
                    "%s can't be enqueued for %scompilation on level %d",
                    executable, bci >= 0 ? "osr-" : "", level));
        }
        Utils.waitForCondition(() -> WB.isMethodCompiled(executable, isOsr));
        return NMethod.get(executable, isOsr);
    }

    public static List<CompileCodeTestCase> generate(int bci) {
        ArrayList<CompileCodeTestCase> result = new ArrayList<>();
        for (Class<?> aClass : CLASSES) {
            Object receiver = RECEIVERS.get(aClass);
            if (receiver == null) {
                throw new Error("TESTBUG : no receiver for class " + aClass);
            }
            for (Executable m : aClass.getDeclaredConstructors()) {
                result.add(new CompileCodeTestCase(receiver, m, bci));
            }
            Arrays.stream(aClass.getDeclaredMethods())
                    .filter(m -> !Modifier.isAbstract(m.getModifiers()))
                    .filter(m -> !Modifier.isNative(m.getModifiers()))
                    .map(m -> new CompileCodeTestCase(receiver, m, bci))
                    .forEach(result::add);
        }
        return result;
    }

    public NMethod toNMethod() {
        return NMethod.get(executable, isOsr);
    }

    public InstalledCode toInstalledCode() {
        NMethod nmethod = toNMethod();
        long address = nmethod == null ? 0L : nmethod.address;
        long entryPoint = nmethod == null ? 0L : nmethod.entry_point;
        return CTVMUtilities.getInstalledCode(
                executable.getName(), address, entryPoint);
    }

    @Override
    public String toString() {
        return "CompileCodeTestCase{" +
                "executable=" + executable +
                ", bci=" + bci +
                '}';
    }

    public void deoptimize() {
        WB.deoptimizeMethod(executable, isOsr);
    }

    public NMethod deoptimizeAndCompile() {
        deoptimize();
        return compile();
    }

    // classes which are used as "input" data in test cases
    private static interface Interface {
        Interface interfaceMethod();
        default Long defaultOverriddenMethod(Interface[] array) {
            return array == null ? 0L : array.length;
        }
        default int defaultMethod(Object o) {
            return o != null ? o.hashCode() : 0;
        }
    }

    private static abstract class Dummy implements Interface {
        protected Dummy() {
        }

        private static void staticMethod() {
        }

        Dummy instanceMethod(int i) {
            return null;
        }

        abstract Object abstractMethod(double d);

        @Override
        public Long defaultOverriddenMethod(Interface[] array) {
            return 0L;
        }
    }

    public static class DummyEx extends Dummy {
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            return 0;
        }

        public DummyEx() {
        }

        protected Dummy instanceMethod(int i) {
            if (i == 0) {
                return this;
            }
            return null;
        }

        @Override
        Object abstractMethod(double d) {
            return this;
        }

        @Override
        public Interface interfaceMethod() {
            return null;
        }
    }

    static {
        Map<Class<?>, Object> map = new HashMap<>();;
        map.put(CompileCodeTestCase.DummyEx.class,
                new CompileCodeTestCase.DummyEx());
        map.put(CompileCodeTestCase.Dummy.class,
                new CompileCodeTestCase.Dummy() {
                    @Override
                    public CompileCodeTestCase.Interface interfaceMethod() {
                        throw new AbstractMethodError();
                    }

                    @Override
                    Object abstractMethod(double d) {
                        throw new AbstractMethodError();
                    }
                });
        map.put(CompileCodeTestCase.Interface.class,
                new CompileCodeTestCase.Interface() {
                    @Override
                    public CompileCodeTestCase.Interface interfaceMethod() {
                        throw new AbstractMethodError();
                    }
                });
        RECEIVERS = Collections.unmodifiableMap(map);
    }

}
