/*
 * Copyright (c) 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 selectionresolution;

import java.io.File;
import java.io.FileWriter;
import java.util.HashMap;

/**
 * One individual test case.  This class also defines a builder, which
 * can be used to build up cases.
 */
public class SelectionResolutionTestCase {

    public enum InvokeInstruction {
        INVOKESTATIC,
        INVOKESPECIAL,
        INVOKEINTERFACE,
        INVOKEVIRTUAL;
    }

    /**
     * The class data (includes interface data).
     */
    public final HashMap<Integer, ClassData> classdata;
    /**
     * The hierarchy shape.
     */
    public final HierarchyShape hier;
    /**
     * The invoke instruction to use.
     */
    public final InvokeInstruction invoke;
    /**
     * Which class is the methodref (or interface methodref).
     */
    public final int methodref;
    /**
     * Which class is the objectref.
     */
    public final int objectref;
    /**
     * Which class is the callsite (this must be a class, not an interface.
     */
    public final int callsite;
    /**
     * The expected result.
     */
    public final Result result;

    private SelectionResolutionTestCase(final HashMap<Integer, ClassData> classdata,
                                        final HierarchyShape hier,
                                        final InvokeInstruction invoke,
                                        final int methodref,
                                        final int objectref,
                                        final int callsite,
                                        final int expected) {
        this.classdata = classdata;
        this.hier = hier;
        this.invoke = invoke;
        this.methodref = methodref;
        this.objectref = objectref;
        this.callsite = callsite;
        this.result = Result.is(expected);
    }

    private SelectionResolutionTestCase(final HashMap<Integer, ClassData> classdata,
                                        final HierarchyShape hier,
                                        final InvokeInstruction invoke,
                                        final int methodref,
                                        final int objectref,
                                        final int callsite,
                                        final Result result) {
        this.classdata = classdata;
        this.hier = hier;
        this.invoke = invoke;
        this.methodref = methodref;
        this.objectref = objectref;
        this.callsite = callsite;
        this.result = result;
    }

    private static int currError = 0;

    private String dumpClasses(final ClassConstruct[] classes)
        throws Exception {
        final String errorDirName = "error_" + currError++;
        final File errorDir = new File(errorDirName);
        errorDir.mkdirs();
        for (int i = 0; i < classes.length; i++) {
            classes[i].writeClass(errorDir);
        }
        try (final FileWriter fos =
             new FileWriter(new File(errorDir, "description.txt"))) {
            fos.write(this.toString());
        }
        return errorDirName;
    }

    /**
     * Run this case, return an error message, or null.
     *
     * @return An error message, or null if the case succeeded.
     */
    public String run() {
        /* Uncomment this line to print EVERY case */
        //System.err.println("Running\n" + this);
        final ClassBuilder builder =
            new ClassBuilder(this, ClassBuilder.ExecutionMode.DIRECT);
        try {
            final ByteCodeClassLoader bcl = new ByteCodeClassLoader();
            final ClassConstruct[] classes = builder.build();

            try {
                bcl.addClasses(classes);
                bcl.loadAll();

                // Grab the callsite class.
                final Class testclass =
                    bcl.findClass(builder.getCallsiteClass().getDottedName());

                // Get the 'test' method out of it and call it.  The
                // return value tess which class that got selected.
                final java.lang.reflect.Method method =
                    testclass.getDeclaredMethod("test");
                final int actual = (Integer) method.invoke(null);
                // Check the result.
                if (!result.complyWith(actual)) {
                    final String dump = dumpClasses(classes);
                    return "Failed:\n" + this + "\nExpected " + result + " got " + actual + "\nClasses written to " + dump;
                }
            } catch (Throwable t) {
                // This catch block is handling exceptions that we
                // might expect to see.
                final Throwable actual = t.getCause();
                if (actual == null) {
                    final String dump = dumpClasses(classes);
                    System.err.println("Unexpected exception in test\n" + this + "\nClasses written to " + dump);
                    throw t;
                } else if (result == null) {
                    final String dump = dumpClasses(classes);
                    return "Failed:\n" + this + "\nUnexpected exception " + actual + "\nClasses written to " + dump;
                } else if (!result.complyWith(actual)) {
                    final String dump = dumpClasses(classes);
                    return "Failed:\n" + this + "\nExpected " + this.result + " got " + actual + "\nClasses written to " + dump;
                }
            }
        } catch(Throwable e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    private static void addPackage(final StringBuilder sb,
                                  final ClassData cd) {
        switch (cd.packageId) {
        case SAME: sb.append("Same."); break;
        case DIFFERENT: sb.append("Different."); break;
        case OTHER: sb.append("Other."); break;
        case PLACEHOLDER: sb.append("_."); break;
        default: throw new RuntimeException("Impossible case");
        }
    }

    public String toString() {
        final StringBuilder sb = new StringBuilder();
        //sb.append("hierarchy:\n" + hier + "\n");
        sb.append("invoke:    " + invoke + "\n");
        if (methodref != -1) {
            if (hier.isClass(methodref)) {
                sb.append("methodref: C" + methodref + "\n");
            } else {
                sb.append("methodref: I" + methodref + "\n");
            }
        }
        if (objectref != -1) {
            if (hier.isClass(objectref)) {
                sb.append("objectref: C" + objectref + "\n");
            } else {
                sb.append("objectref: I" + objectref + "\n");
            }
        }
        if (callsite != -1) {
            if (hier.isClass(callsite)) {
                sb.append("callsite: C" + callsite + "\n");
            } else {
                sb.append("callsite: I" + callsite + "\n");
            }
        }
        sb.append("result: " + result + "\n");
        sb.append("classes:\n\n");

        for(int i = 0; classdata.containsKey(i); i++) {
            final ClassData cd = classdata.get(i);

            if (hier.isClass(i)) {
                sb.append("class ");
                addPackage(sb, cd);
                sb.append("C" + i);
            } else {
                sb.append("interface ");
                addPackage(sb, cd);
                sb.append("I" + i);
            }

            boolean first = true;
            for(final int j : hier.classes()) {
                if (hier.inherits(i, j)) {
                    if (first) {
                        sb.append(" extends C" + j);
                    } else {
                        sb.append(", C" + j);
                    }
                }
            }

            first = true;
            for(final int j : hier.interfaces()) {
                if (hier.inherits(i, j)) {
                    if (first) {
                        sb.append(" implements I" + j);
                    } else {
                        sb.append(", I" + j);
                    }
                }
            }

            sb.append(cd);
        }

        return sb.toString();
    }

    /**
     * A builder, facilitating building up test cases.
     */
    public static class Builder {
        /**
         * A map from class (or interface) id's to ClassDatas
         */
        public final HashMap<Integer, ClassData> classdata;
        /**
         * The hierarchy shape.
         */
        public final HierarchyShape hier;
        /**
         * Which invoke instruction to use.
         */
        public InvokeInstruction invoke;
        /**
         * The id of the methodref (or interface methodref).
         */
        public int methodref = -1;
        /**
         * The id of the object ref.  Note that for the generator
         * framework to work, this must be set to something.  If an
         * objectref isn't used, just set it to the methodref.
         */
        public int objectref = -1;
        /**
         * The id of the callsite.
         */
        public int callsite = -1;
        /**
         * The id of the expected result.  This is used to store the
         * expected resolution result.
         */
        public int expected;
        /**
         * The expected result.  This needs to be set before the final
         * test case is built.
         */
        public Result result;

        /**
         * Create an empty Builder object.
         */
        public Builder() {
            classdata = new HashMap<>();
            hier = new HierarchyShape();
        }

        private Builder(final HashMap<Integer, ClassData> classdata,
                        final HierarchyShape hier,
                        final InvokeInstruction invoke,
                        final int methodref,
                        final int objectref,
                        final int callsite,
                        final int expected,
                        final Result result) {
            this.classdata = classdata;
            this.hier = hier;
            this.invoke = invoke;
            this.methodref = methodref;
            this.objectref = objectref;
            this.callsite = callsite;
            this.expected = expected;
            this.result = result;
        }

        private Builder(final Builder other) {
            this((HashMap<Integer, ClassData>) other.classdata.clone(),
                 other.hier.copy(), other.invoke, other.methodref, other.objectref,
                 other.callsite, other.expected, other.result);
        }

        public SelectionResolutionTestCase build() {
            if (result != null) {
                return new SelectionResolutionTestCase(classdata, hier, invoke,
                                                       methodref, objectref,
                                                       callsite, result);
            } else {
                return new SelectionResolutionTestCase(classdata, hier, invoke,
                                                       methodref, objectref,
                                                       callsite, expected);
            }
        }

        /**
         * Set the expected result.
         */
        public void setResult(final Result result) {
            this.result = result;
        }

        /**
         * Add a class, and return its id.
         *
         * @return The new class' id.
         */
        public int addClass(final ClassData data) {
            final int id = hier.addClass();
            classdata.put(id, data);
            return id;
        }

        /**
         * Add an interface, and return its id.
         *
         * @return The new class' id.
         */
        public int addInterface(final ClassData data) {
            final int id = hier.addInterface();
            classdata.put(id, data);
            return id;
        }

        /**
         * Make a copy of this builder.
         */
        public Builder copy() {
            return new Builder(this);
        }

        public String toString() {
            final StringBuilder sb = new StringBuilder();
            //sb.append("hierarchy:\n" + hier + "\n");
            sb.append("invoke:    " + invoke + "\n");
            if (methodref != -1) {
                if (hier.isClass(methodref)) {
                    sb.append("methodref: C" + methodref + "\n");
                } else {
                    sb.append("methodref: I" + methodref + "\n");
                }
            }
            if (objectref != -1) {
                if (hier.isClass(objectref)) {
                    sb.append("objectref: C" + objectref + "\n");
                } else {
                    sb.append("objectref: I" + objectref + "\n");
                }
            }
            if (callsite != -1) {
                if (hier.isClass(callsite)) {
                    sb.append("callsite: C" + callsite + "\n");
                } else {
                    sb.append("callsite: I" + callsite + "\n");
                }
            }
            if (expected != -1) {
                if (hier.isClass(expected)) {
                    sb.append("expected: C" + expected + "\n");
                } else {
                    sb.append("expected: I" + expected + "\n");
                }
            }
            sb.append("result: " + result + "\n");
            sb.append("classes:\n\n");

            for(int i = 0; classdata.containsKey(i); i++) {
                final ClassData cd = classdata.get(i);

                if (hier.isClass(i)) {
                    sb.append("class ");
                    addPackage(sb, cd);
                    sb.append("C" + i);
                } else {
                    sb.append("interface ");
                    addPackage(sb, cd);
                    sb.append("I" + i);
                }

                boolean first = true;
                for(final int j : hier.classes()) {
                    if (hier.inherits(i, j)) {
                        if (first) {
                            sb.append(" extends C" + j);
                        } else {
                            sb.append(", C" + j);
                        }
                    }
                }

                first = true;
                for(final int j : hier.interfaces()) {
                    if (hier.inherits(i, j)) {
                        if (first) {
                            sb.append(" implements I" + j);
                        } else {
                            sb.append(", I" + j);
                        }
                    }
                }

                sb.append(cd);
            }

            return sb.toString();
        }
    }
}