/*
 * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */


package org.graalvm.compiler.core.test;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
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.Enumeration;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.graalvm.compiler.api.replacements.MethodSubstitution;
import org.graalvm.compiler.api.replacements.Snippet;
import org.graalvm.compiler.api.replacements.Snippet.ConstantParameter;
import org.graalvm.compiler.api.replacements.Snippet.NonNullParameter;
import org.graalvm.compiler.api.replacements.Snippet.VarargsParameter;
import org.graalvm.compiler.api.test.Graal;
import org.graalvm.compiler.bytecode.BridgeMethodUtils;
import org.graalvm.compiler.core.CompilerThreadFactory;
import org.graalvm.compiler.core.common.LIRKind;
import org.graalvm.compiler.core.common.type.ArithmeticOpTable;
import org.graalvm.compiler.debug.DebugCloseable;
import org.graalvm.compiler.debug.DebugContext;
import org.graalvm.compiler.debug.DebugHandlersFactory;
import org.graalvm.compiler.debug.GraalError;
import org.graalvm.compiler.graph.Node;
import org.graalvm.compiler.graph.NodeClass;
import org.graalvm.compiler.java.GraphBuilderPhase;
import org.graalvm.compiler.nodeinfo.NodeInfo;
import org.graalvm.compiler.nodes.PhiNode;
import org.graalvm.compiler.nodes.StructuredGraph;
import org.graalvm.compiler.nodes.StructuredGraph.AllowAssumptions;
import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration;
import org.graalvm.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration.Plugins;
import org.graalvm.compiler.nodes.graphbuilderconf.InvocationPlugins;
import org.graalvm.compiler.nodes.spi.CoreProviders;
import org.graalvm.compiler.options.OptionValues;
import org.graalvm.compiler.phases.OptimisticOptimizations;
import org.graalvm.compiler.phases.PhaseSuite;
import org.graalvm.compiler.phases.VerifyPhase;
import org.graalvm.compiler.phases.VerifyPhase.VerificationError;
import org.graalvm.compiler.phases.contract.VerifyNodeCosts;
import org.graalvm.compiler.phases.tiers.HighTierContext;
import org.graalvm.compiler.phases.util.Providers;
import org.graalvm.compiler.runtime.RuntimeProvider;
import org.graalvm.compiler.serviceprovider.JavaVersionUtil;
import org.graalvm.compiler.test.AddExports;
import org.graalvm.compiler.api.test.ModuleSupport;
import jdk.internal.vm.compiler.word.LocationIdentity;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;

import jdk.vm.ci.code.BailoutException;
import jdk.vm.ci.code.Register;
import jdk.vm.ci.code.Register.RegisterCategory;
import jdk.vm.ci.meta.JavaField;
import jdk.vm.ci.meta.JavaMethod;
import jdk.vm.ci.meta.JavaType;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import jdk.vm.ci.meta.ResolvedJavaType;
import jdk.vm.ci.meta.Value;

/**
 * Checks that all classes in *graal*.jar and *jvmci*.jar entries on the boot class path comply with
 * global invariants such as using {@link Object#equals(Object)} to compare certain types instead of
 * identity comparisons.
 */
@AddExports("jdk.internal.vm.ci/*=jdk.aot")
public class CheckGraalInvariants extends GraalCompilerTest {

    /**
     * Magic token to denote the classes in the Java runtime image (i.e. in the {@code jrt:/} file
     * system).
     */
    public static final String JRT_CLASS_PATH_ENTRY = "<jrt>";

    private static boolean shouldVerifyEquals(ResolvedJavaMethod m) {
        if (m.getName().equals("identityEquals")) {
            ResolvedJavaType c = m.getDeclaringClass();
            if (c.getName().equals("Ljdk/vm/ci/meta/AbstractValue;") || c.getName().equals("jdk/vm/ci/meta/Value")) {
                return false;
            }
        }

        return true;
    }

    public static String relativeFileName(String absolutePath) {
        int lastFileSeparatorIndex = absolutePath.lastIndexOf(File.separator);
        return absolutePath.substring(lastFileSeparatorIndex >= 0 ? lastFileSeparatorIndex : 0);
    }

    public static class InvariantsTool {

        protected boolean shouldProcess(String classpathEntry) {
            if (classpathEntry.equals(JRT_CLASS_PATH_ENTRY)) {
                return true;
            }
            if (classpathEntry.endsWith(".jar")) {
                String name = new File(classpathEntry).getName();
                return name.contains("jvmci") || name.contains("graal") || name.contains("jdk.internal.vm.compiler");
            }
            return false;
        }

        protected String getClassPath() {
            String bootclasspath;
            if (JavaVersionUtil.JAVA_SPEC <= 8) {
                bootclasspath = System.getProperty("sun.boot.class.path");
            } else {
                bootclasspath = JRT_CLASS_PATH_ENTRY;
            }
            return bootclasspath;
        }

        protected boolean shouldLoadClass(String className) {
            if (className.equals("module-info") || className.startsWith("META-INF.versions.")) {
                return false;
            }
            if (JavaVersionUtil.JAVA_SPEC > 8) {
                // @formatter:off
                /*
                 * Work around to prevent:
                 *
                 * org.graalvm.compiler.debug.GraalError: java.lang.IllegalAccessError: class org.graalvm.compiler.serviceprovider.GraalServices$Lazy (in module
                 * jdk.internal.vm.compiler) cannot access class java.lang.management.ManagementFactory (in module java.management) because module
                 * jdk.internal.vm.compiler does not read module java.management
                 *     at jdk.internal.vm.compiler/org.graalvm.compiler.debug.GraalError.shouldNotReachHere(GraalError.java:55)
                 *     at org.graalvm.compiler.core.test.CheckGraalInvariants$InvariantsTool.handleClassLoadingException(CheckGraalInvariants.java:149)
                 *     at org.graalvm.compiler.core.test.CheckGraalInvariants.initializeClasses(CheckGraalInvariants.java:321)
                 *     at org.graalvm.compiler.core.test.CheckGraalInvariants.runTest(CheckGraalInvariants.java:239)
                 *
                 * which occurs because JDK8 overlays are in modular jars. They are never used normally.
                 */
                // @formatter:on
                if (className.equals("org.graalvm.compiler.serviceprovider.GraalServices$Lazy")) {
                    return false;
                }
            } else {
                if (className.equals("jdk.vm.ci.services.JVMCIClassLoaderFactory")) {
                    return false;
                }
            }
            return true;
        }

        protected void handleClassLoadingException(Throwable t) {
            GraalError.shouldNotReachHere(t);
        }

        protected void handleParsingException(Throwable t) {
            GraalError.shouldNotReachHere(t);
        }

        public boolean shouldVerifyFoldableMethods() {
            return true;
        }

        /**
         * Makes edits to the list of verifiers to be run.
         */
        @SuppressWarnings("unused")
        protected void updateVerifiers(List<VerifyPhase<CoreProviders>> verifiers) {
        }
    }

    @Test
    @SuppressWarnings("try")
    public void test() {
        assumeManagementLibraryIsLoadable();
        runTest(new InvariantsTool());
    }

    @SuppressWarnings("try")
    public static void runTest(InvariantsTool tool) {
        RuntimeProvider rt = Graal.getRequiredCapability(RuntimeProvider.class);
        Providers providers = rt.getHostBackend().getProviders();
        MetaAccessProvider metaAccess = providers.getMetaAccess();

        PhaseSuite<HighTierContext> graphBuilderSuite = new PhaseSuite<>();
        Plugins plugins = new Plugins(new InvocationPlugins());
        GraphBuilderConfiguration config = GraphBuilderConfiguration.getDefault(plugins).withEagerResolving(true).withUnresolvedIsError(true);
        graphBuilderSuite.appendPhase(new GraphBuilderPhase(config));
        HighTierContext context = new HighTierContext(providers, graphBuilderSuite, OptimisticOptimizations.NONE);

        Assume.assumeTrue(VerifyPhase.class.desiredAssertionStatus());

        String bootclasspath = tool.getClassPath();
        Assert.assertNotNull("Cannot find boot class path", bootclasspath);

        final List<String> classNames = new ArrayList<>();
        for (String path : bootclasspath.split(File.pathSeparator)) {
            if (tool.shouldProcess(path)) {
                try {
                    if (path.equals(JRT_CLASS_PATH_ENTRY)) {
                        for (String className : ModuleSupport.getJRTGraalClassNames()) {
                            if (isGSON(className)) {
                                /*
                                 * GSON classes are compiled with old JDK
                                 */
                                continue;
                            }
                            classNames.add(className);
                        }
                    } else {
                        final ZipFile zipFile = new ZipFile(new File(path));
                        for (final Enumeration<? extends ZipEntry> entry = zipFile.entries(); entry.hasMoreElements();) {
                            final ZipEntry zipEntry = entry.nextElement();
                            String name = zipEntry.getName();
                            if (name.endsWith(".class") && !name.startsWith("META-INF/versions/")) {
                                String className = name.substring(0, name.length() - ".class".length()).replace('/', '.');
                                if (isInNativeImage(className)) {
                                    /*
                                     * Native Image is an external tool and does not need to follow
                                     * the Graal invariants.
                                     */
                                    continue;
                                }
                                if (isGSON(className)) {
                                    /*
                                     * GSON classes are compiled with old JDK
                                     */
                                    continue;
                                }
                                classNames.add(className);
                            }
                        }
                    }
                } catch (IOException ex) {
                    Assert.fail(ex.toString());
                }
            }
        }
        Assert.assertFalse("Could not find graal jars on boot class path: " + bootclasspath, classNames.isEmpty());

        // Allows a subset of methods to be checked through use of a system property
        String property = System.getProperty(CheckGraalInvariants.class.getName() + ".filters");
        String[] filters = property == null ? null : property.split(",");

        OptionValues options = getInitialOptions();
        CompilerThreadFactory factory = new CompilerThreadFactory("CheckInvariantsThread");
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(availableProcessors, availableProcessors, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), factory);

        List<String> errors = Collections.synchronizedList(new ArrayList<>());

        List<VerifyPhase<CoreProviders>> verifiers = new ArrayList<>();

        // If you add a new type to test here, be sure to add appropriate
        // methods to the BadUsageWithEquals class below
        verifiers.add(new VerifyUsageWithEquals(Value.class));
        verifiers.add(new VerifyUsageWithEquals(Register.class));
        verifiers.add(new VerifyUsageWithEquals(RegisterCategory.class));
        verifiers.add(new VerifyUsageWithEquals(JavaType.class));
        verifiers.add(new VerifyUsageWithEquals(JavaMethod.class));
        verifiers.add(new VerifyUsageWithEquals(JavaField.class));
        verifiers.add(new VerifyUsageWithEquals(LocationIdentity.class));
        verifiers.add(new VerifyUsageWithEquals(LIRKind.class));
        verifiers.add(new VerifyUsageWithEquals(ArithmeticOpTable.class));
        verifiers.add(new VerifyUsageWithEquals(ArithmeticOpTable.Op.class));

        verifiers.add(new VerifyDebugUsage());
        verifiers.add(new VerifyCallerSensitiveMethods());
        verifiers.add(new VerifyVirtualizableUsage());
        verifiers.add(new VerifyUpdateUsages());
        verifiers.add(new VerifyBailoutUsage());
        verifiers.add(new VerifySystemPropertyUsage());
        verifiers.add(new VerifyInstanceOfUsage());
        verifiers.add(new VerifyGraphAddUsage());
        verifiers.add(new VerifyBufferUsage());
        verifiers.add(new VerifyGetOptionsUsage());
        verifiers.add(new VerifyUnsafeAccess());

        VerifyFoldableMethods foldableMethodsVerifier = new VerifyFoldableMethods();
        if (tool.shouldVerifyFoldableMethods()) {
            verifiers.add(foldableMethodsVerifier);
        }

        tool.updateVerifiers(verifiers);

        for (Method m : BadUsageWithEquals.class.getDeclaredMethods()) {
            ResolvedJavaMethod method = metaAccess.lookupJavaMethod(m);
            try (DebugContext debug = DebugContext.create(options, DebugHandlersFactory.LOADER)) {
                StructuredGraph graph = new StructuredGraph.Builder(options, debug, AllowAssumptions.YES).method(method).build();
                try (DebugCloseable s = debug.disableIntercept(); DebugContext.Scope ds = debug.scope("CheckingGraph", graph, method)) {
                    graphBuilderSuite.apply(graph, context);
                    // update phi stamps
                    graph.getNodes().filter(PhiNode.class).forEach(PhiNode::inferStamp);
                    checkGraph(verifiers, context, graph);
                    errors.add(String.format("Expected error while checking %s", m));
                } catch (VerificationError e) {
                    // expected!
                } catch (Throwable e) {
                    errors.add(String.format("Error while checking %s:%n%s", m, printStackTraceToString(e)));
                }
            }
        }
        if (errors.isEmpty()) {
            // Order outer classes before the inner classes
            classNames.sort((String a, String b) -> a.compareTo(b));
            // Initialize classes in single thread to avoid deadlocking issues during initialization
            List<Class<?>> classes = initializeClasses(tool, classNames);
            for (Class<?> c : classes) {
                String className = c.getName();
                executor.execute(() -> {
                    try {
                        checkClass(c, metaAccess, verifiers);
                    } catch (Throwable e) {
                        errors.add(String.format("Error while checking %s:%n%s", className, printStackTraceToString(e)));
                    }
                });

                ResolvedJavaType type = metaAccess.lookupJavaType(c);
                List<ResolvedJavaMethod> methods = new ArrayList<>();
                try {
                    methods.addAll(Arrays.asList(type.getDeclaredMethods()));
                    methods.addAll(Arrays.asList(type.getDeclaredConstructors()));
                } catch (Throwable e) {
                    errors.add(String.format("Error while checking %s:%n%s", className, printStackTraceToString(e)));
                }
                ResolvedJavaMethod clinit = type.getClassInitializer();
                if (clinit != null) {
                    methods.add(clinit);
                }

                for (ResolvedJavaMethod method : methods) {
                    if (Modifier.isNative(method.getModifiers()) || Modifier.isAbstract(method.getModifiers())) {
                        // ignore
                    } else {
                        String methodName = className + "." + method.getName();
                        if (matches(filters, methodName)) {
                            executor.execute(() -> {
                                try (DebugContext debug = DebugContext.create(options, DebugHandlersFactory.LOADER)) {
                                    boolean isSubstitution = method.getAnnotation(Snippet.class) != null || method.getAnnotation(MethodSubstitution.class) != null;
                                    StructuredGraph graph = new StructuredGraph.Builder(options, debug).method(method).setIsSubstitution(isSubstitution).build();
                                    try (DebugCloseable s = debug.disableIntercept(); DebugContext.Scope ds = debug.scope("CheckingGraph", graph, method)) {
                                        checkMethod(method);
                                        graphBuilderSuite.apply(graph, context);
                                        // update phi stamps
                                        graph.getNodes().filter(PhiNode.class).forEach(PhiNode::inferStamp);
                                        checkGraph(verifiers, context, graph);
                                    } catch (VerificationError e) {
                                        errors.add(e.getMessage());
                                    } catch (LinkageError e) {
                                        // suppress linkages errors resulting from eager resolution
                                    } catch (BailoutException e) {
                                        // Graal bail outs on certain patterns in Java bytecode
                                        // (e.g.,
                                        // unbalanced monitors introduced by jacoco).
                                    } catch (Throwable e) {
                                        try {
                                            tool.handleParsingException(e);
                                        } catch (Throwable t) {
                                            errors.add(String.format("Error while checking %s:%n%s", methodName, printStackTraceToString(e)));
                                        }
                                    }
                                }
                            });
                        }
                    }
                }
            }

            executor.shutdown();
            try {
                executor.awaitTermination(1, TimeUnit.HOURS);
            } catch (InterruptedException e1) {
                throw new RuntimeException(e1);
            }

            if (tool.shouldVerifyFoldableMethods()) {
                try {
                    foldableMethodsVerifier.finish();
                } catch (Throwable e) {
                    errors.add(e.getMessage());
                }
            }
        }
        if (!errors.isEmpty()) {
            StringBuilder msg = new StringBuilder();
            String nl = String.format("%n");
            for (String e : errors) {
                if (msg.length() != 0) {
                    msg.append(nl);
                }
                msg.append(e);
            }
            Assert.fail(msg.toString());
        }
    }

    private static boolean isInNativeImage(String className) {
        return className.startsWith("org.graalvm.nativeimage");
    }

    private static boolean isGSON(String className) {
        return className.contains("com.google.gson");
    }

    private static List<Class<?>> initializeClasses(InvariantsTool tool, List<String> classNames) {
        List<Class<?>> classes = new ArrayList<>(classNames.size());
        for (String className : classNames) {
            if (!tool.shouldLoadClass(className)) {
                continue;
            }
            try {
                Class<?> c = Class.forName(className, true, CheckGraalInvariants.class.getClassLoader());
                classes.add(c);
            } catch (UnsupportedClassVersionError e) {
                // graal-test.jar can contain classes compiled for different Java versions
            } catch (NoClassDefFoundError e) {
                if (!e.getMessage().contains("Could not initialize class")) {
                    throw e;
                } else {
                    // A second or later attempt to initialize a class
                    // results in this confusing error where the
                    // original cause of initialization failure is lost
                }
            } catch (Throwable t) {
                tool.handleClassLoadingException(t);
            }
        }
        return classes;
    }

    /**
     * @param metaAccess
     * @param verifiers
     */
    private static void checkClass(Class<?> c, MetaAccessProvider metaAccess, List<VerifyPhase<CoreProviders>> verifiers) {
        if (Node.class.isAssignableFrom(c)) {
            if (c.getAnnotation(NodeInfo.class) == null) {
                throw new AssertionError(String.format("Node subclass %s requires %s annotation", c.getName(), NodeClass.class.getSimpleName()));
            }
            VerifyNodeCosts.verifyNodeClass(c);
        }
        for (VerifyPhase<CoreProviders> verifier : verifiers) {
            verifier.verifyClass(c, metaAccess);
        }
    }

    private static void checkMethod(ResolvedJavaMethod method) {
        if (method.getAnnotation(Snippet.class) == null) {
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                for (Annotation a : parameterAnnotations[i]) {
                    Class<? extends Annotation> annotationType = a.annotationType();
                    if (annotationType == ConstantParameter.class || annotationType == VarargsParameter.class || annotationType == NonNullParameter.class) {
                        VerificationError verificationError = new VerificationError("Parameter %d of %s is annotated with %s but the method is not annotated with %s", i, method,
                                        annotationType.getSimpleName(),
                                        Snippet.class.getSimpleName());
                        throw verificationError;
                    }
                }
            }
        }
    }

    /**
     * Checks the invariants for a single graph.
     */
    private static void checkGraph(List<VerifyPhase<CoreProviders>> verifiers, HighTierContext context, StructuredGraph graph) {
        for (VerifyPhase<CoreProviders> verifier : verifiers) {
            if (!(verifier instanceof VerifyUsageWithEquals) || shouldVerifyEquals(graph.method())) {
                verifier.apply(graph, context);
            } else {
                verifier.apply(graph, context);
            }
        }
        if (graph.method().isBridge()) {
            BridgeMethodUtils.getBridgedMethod(graph.method());
        }
    }

    private static boolean matches(String[] filters, String s) {
        if (filters == null || filters.length == 0) {
            return true;
        }
        for (String filter : filters) {
            if (s.contains(filter)) {
                return true;
            }
        }
        return false;
    }

    private static String printStackTraceToString(Throwable t) {
        StringWriter sw = new StringWriter();
        t.printStackTrace(new PrintWriter(sw));
        return sw.toString();
    }

    static class BadUsageWithEquals {
        Value aValue;
        Register aRegister;
        RegisterCategory aRegisterCategory;
        JavaType aJavaType;
        JavaField aJavaField;
        JavaMethod aJavaMethod;
        LocationIdentity aLocationIdentity;
        LIRKind aLIRKind;
        ArithmeticOpTable anArithmeticOpTable;
        ArithmeticOpTable.Op anArithmeticOpTableOp;

        static Value aStaticValue;
        static Register aStaticRegister;
        static RegisterCategory aStaticRegisterCategory;
        static JavaType aStaticJavaType;
        static JavaField aStaticJavaField;
        static JavaMethod aStaticJavaMethod;
        static LocationIdentity aStaticLocationIdentity;
        static LIRKind aStaticLIRKind;
        static ArithmeticOpTable aStaticArithmeticOpTable;
        static ArithmeticOpTable.Op aStaticArithmeticOpTableOp;

        boolean test01(Value f) {
            return aValue == f;
        }

        boolean test02(Register f) {
            return aRegister == f;
        }

        boolean test03(RegisterCategory f) {
            return aRegisterCategory == f;
        }

        boolean test04(JavaType f) {
            return aJavaType == f;
        }

        boolean test05(JavaField f) {
            return aJavaField == f;
        }

        boolean test06(JavaMethod f) {
            return aJavaMethod == f;
        }

        boolean test07(LocationIdentity f) {
            return aLocationIdentity == f;
        }

        boolean test08(LIRKind f) {
            return aLIRKind == f;
        }

        boolean test09(ArithmeticOpTable f) {
            return anArithmeticOpTable == f;
        }

        boolean test10(ArithmeticOpTable.Op f) {
            return anArithmeticOpTableOp == f;
        }

        boolean test12(Value f) {
            return aStaticValue == f;
        }

        boolean test13(Register f) {
            return aStaticRegister == f;
        }

        boolean test14(RegisterCategory f) {
            return aStaticRegisterCategory == f;
        }

        boolean test15(JavaType f) {
            return aStaticJavaType == f;
        }

        boolean test16(JavaField f) {
            return aStaticJavaField == f;
        }

        boolean test17(JavaMethod f) {
            return aStaticJavaMethod == f;
        }

        boolean test18(LocationIdentity f) {
            return aStaticLocationIdentity == f;
        }

        boolean test19(LIRKind f) {
            return aStaticLIRKind == f;
        }

        boolean test20(ArithmeticOpTable f) {
            return aStaticArithmeticOpTable == f;
        }

        boolean test21(ArithmeticOpTable.Op f) {
            return aStaticArithmeticOpTableOp == f;
        }
    }
}
