/*
 * Copyright (c) 2011, 2024, 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.
 */

/*
 * @test
 * @bug 7003595
 * @summary IncompatibleClassChangeError with unreferenced local class with subclass
 * @modules jdk.compiler/com.sun.tools.javac.api
 *          jdk.compiler/com.sun.tools.javac.file
 */

import com.sun.source.util.JavacTask;
import java.lang.classfile.*;
import java.lang.classfile.attribute.*;
import com.sun.tools.javac.api.JavacTool;

import java.io.File;
import java.net.URI;
import java.util.Arrays;
import java.util.ArrayList;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;


public class T7003595 {

    /** global decls ***/

    //statistics
    static int checkCount = 0;

    enum ClassKind {
        NESTED("static class #N { #B }", "$", true),
        INNER("class #N { #B }", "$", false),
        LOCAL_REF("void test() { class #N { #B }; new #N(); }", "$1", false),
        LOCAL_NOREF("void test() { class #N { #B }; }", "$1", false),
        ANON("void test() { new Object() { #B }; }", "$1", false),
        NONE("", "", false);

        String memberInnerStr;
        String sep;
        boolean staticAllowed;

        private ClassKind(String memberInnerStr, String sep, boolean staticAllowed) {
            this.memberInnerStr = memberInnerStr;
            this.sep = sep;
            this.staticAllowed = staticAllowed;
        }

        String getSource(String className, String outerName, String nested) {
            return memberInnerStr.replaceAll("#O", outerName).
                    replaceAll("#N", className).replaceAll("#B", nested);
        }

        static String getClassfileName(String[] names, ClassKind[] outerKinds, int pos) {
            System.out.println(" pos = " + pos + " kind = " + outerKinds[pos] + " sep = " + outerKinds[pos].sep);
            String name = outerKinds[pos] != ANON ?
                    names[pos] : "";
            if (pos == 0) {
                return "Test" + outerKinds[pos].sep + name;
            } else {
                String outerStr = getClassfileName(names, outerKinds, pos - 1);
                return outerStr + outerKinds[pos].sep + name;
            }
        }

        boolean isAllowed(ClassKind nestedKind) {
            return nestedKind != NESTED ||
                    staticAllowed;
        }
    }

    enum LocalInnerClass {
        LOCAL_REF("class L {}; new L();", "Test$1L"),
        LOCAL_NOREF("class L {};", "Test$1L"),
        ANON("new Object() {};", "Test$1"),
        NONE("", "");

        String localInnerStr;
        String canonicalInnerStr;

        private LocalInnerClass(String localInnerStr, String canonicalInnerStr) {
            this.localInnerStr = localInnerStr;
            this.canonicalInnerStr = canonicalInnerStr;
        }
    }

    public static void main(String... args) throws Exception {
        // Create a single file manager and reuse it for each compile to save time.
        try (StandardJavaFileManager fm = JavacTool.create().getStandardFileManager(null, null, null)) {
            for (ClassKind ck1 : ClassKind.values()) {
                String cname1 = "C1";
                for (ClassKind ck2 : ClassKind.values()) {
                    if (!ck1.isAllowed(ck2)) continue;
                    String cname2 = "C2";
                    for (ClassKind ck3 : ClassKind.values()) {
                        if (!ck2.isAllowed(ck3)) continue;
                        String cname3 = "C3";
                        new T7003595(fm, new ClassKind[] {ck1, ck2, ck3}, new String[] { cname1, cname2, cname3 }).compileAndCheck();
                    }
                }
            }
        }

        System.out.println("Total checks made: " + checkCount);
    }

    /** instance decls **/

    ClassKind[] cks;
    String[] cnames;
    StandardJavaFileManager fm;

    T7003595(StandardJavaFileManager fm, ClassKind[] cks, String[] cnames) {
        this.fm = fm;
        this.cks = cks;
        this.cnames = cnames;
    }

    void compileAndCheck() throws Exception {
        final JavaCompiler tool = ToolProvider.getSystemJavaCompiler();
        JavaSource source = new JavaSource();
        JavacTask ct = (JavacTask)tool.getTask(null, fm, null,
                null, null, Arrays.asList(source));
        ct.call();
        verifyBytecode(source);
    }

    void verifyBytecode(JavaSource source) {
        for (int i = 0; i < 3 ; i ++) {
            if (cks[i] == ClassKind.NONE) break;
            checkCount++;
            String filename = cks[i].getClassfileName(cnames, cks, i);
            File compiledTest = new File(filename + ".class");
            try {
                ClassModel cf = ClassFile.of().parse(compiledTest.toPath());
                if (cf == null) {
                    throw new Error("Classfile not found: " + filename);
                }

                InnerClassesAttribute innerClasses = cf.findAttribute(Attributes.innerClasses()).orElse(null);

                ArrayList<String> foundInnerSig = new ArrayList<>();
                if (innerClasses != null) {
                    for (InnerClassInfo info : innerClasses.classes()) {
                        String foundSig = info.innerClass().asInternalName();
                        foundInnerSig.add(foundSig);
                    }
                }

                ArrayList<String> expectedInnerSig = new ArrayList<>();
                //add inner class (if any)
                if (i < 2 && cks[i + 1] != ClassKind.NONE) {
                    expectedInnerSig.add(cks[i + 1].getClassfileName(cnames, cks, i + 1));
                }
                //add inner classes
                for (int j = 0 ; j != i + 1 && j < 3; j++) {
                    expectedInnerSig.add(cks[j].getClassfileName(cnames, cks, j));
                }

                if (expectedInnerSig.size() != foundInnerSig.size()) {
                    throw new Error("InnerClasses attribute for " + cnames[i] + " has wrong size\n" +
                                    "expected " + expectedInnerSig.size() + "\n" +
                                    "found " + (innerClasses == null? 0: innerClasses.classes().size()) + "\n" +
                                    source);
                }

                for (String foundSig : foundInnerSig) {
                    if (!expectedInnerSig.contains(foundSig)) {
                        throw new Error("InnerClasses attribute for " + cnames[i] + " has unexpected signature: " +
                                foundSig + "\n" + source + "\n" + expectedInnerSig);
                    }
                }

                for (String expectedSig : expectedInnerSig) {
                    if (!foundInnerSig.contains(expectedSig)) {
                        throw new Error("InnerClasses attribute for " + cnames[i] + " does not contain expected signature: " +
                                    expectedSig + "\n" + source);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                throw new Error("error reading " + compiledTest +": " + e);
            }
        }
    }

    class JavaSource extends SimpleJavaFileObject {

        static final String source_template = "class Test { #C }";

        String source;

        public JavaSource() {
            super(URI.create("myfo:/Test.java"), JavaFileObject.Kind.SOURCE);
            String c3 = cks[2].getSource(cnames[2], cnames[1], "");
            String c2 = cks[1].getSource(cnames[1], cnames[0], c3);
            String c1 = cks[0].getSource(cnames[0], "Test", c2);
            source = source_template.replace("#C", c1);
        }

        @Override
        public String toString() {
            return source;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return source;
        }
    }
}
