/*
 * Copyright (c) 2017, 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.  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 build.tools.depend;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Documented;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.AnnotationValueVisitor;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.ModuleElement.DirectiveVisitor;
import javax.lang.model.element.ModuleElement.ExportsDirective;
import javax.lang.model.element.ModuleElement.OpensDirective;
import javax.lang.model.element.ModuleElement.ProvidesDirective;
import javax.lang.model.element.ModuleElement.RequiresDirective;
import javax.lang.model.element.ModuleElement.UsesDirective;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.QualifiedNameable;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.NullType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.type.UnionType;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;

import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.BasicJavacTask;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Context.Key;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Options;
import com.sun.tools.javac.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.StreamSupport;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

public class Depend implements Plugin {

    @Override
    public String getName() {
        return "depend";
    }

    @Override
    public void init(JavacTask jt, String... args) {
        addExports();

        Path internalAPIDigestFile;
        Map<String, String> internalAPI = new HashMap<>();
        AtomicBoolean noApiChange = new AtomicBoolean();
        Context context = ((BasicJavacTask) jt).getContext();
        JavaCompiler compiler = JavaCompiler.instance(context);
        try {
            Options options = Options.instance(context);
            String modifiedInputs = options.get("modifiedInputs");
            if (modifiedInputs == null) {
                throw new IllegalStateException("Expected modifiedInputs to be set using -XDmodifiedInputs=<list-of-files>");
            }
            String logLevel = options.get("LOG_LEVEL");
            boolean debug = "trace".equals(logLevel) || "debug".equals(logLevel);
            String internalAPIPath = options.get("internalAPIPath");
            if (internalAPIPath == null) {
                throw new IllegalStateException("Expected internalAPIPath to be set using -XDinternalAPIPath=<internal-API-path>");
            }
            Set<Path> modified = Files.readAllLines(Paths.get(modifiedInputs)).stream()
                                                                              .map(Paths::get)
                                                                              .collect(Collectors.toSet());
            internalAPIDigestFile = Paths.get(internalAPIPath);
            if (Files.isReadable(internalAPIDigestFile)) {
                try {
                    Files.readAllLines(internalAPIDigestFile, StandardCharsets.UTF_8)
                         .forEach(line -> {
                             String[] keyAndValue = line.split("=");
                             internalAPI.put(keyAndValue[0], keyAndValue[1]);
                         });
                } catch (IOException ex) {
                    throw new IllegalStateException(ex);
                }
            }
            Class<?> initialFileParserIntf = Class.forName("com.sun.tools.javac.main.JavaCompiler$InitialFileParserIntf");
            Class<?> initialFileParser = Class.forName("com.sun.tools.javac.main.JavaCompiler$InitialFileParser");
            Field initialParserKeyField = initialFileParser.getDeclaredField("initialParserKey");
            @SuppressWarnings("unchecked")
            Key<Object> key = (Key<Object>) initialParserKeyField.get(null);
            Object initialParserInstance =
                    Proxy.newProxyInstance(Depend.class.getClassLoader(),
                                           new Class<?>[] {initialFileParserIntf},
                                           new FilteredInitialFileParser(compiler,
                                                                         modified,
                                                                         internalAPI,
                                                                         noApiChange,
                                                                         debug));
            context.<Object>put(key, initialParserInstance);
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }

        jt.addTaskListener(new TaskListener() {
            private final Map<ModuleElement, Set<PackageElement>> apiPackages = new HashMap<>();
            private final MessageDigest apiHash;
            {
                try {
                    apiHash = MessageDigest.getInstance("SHA-256");
                } catch (NoSuchAlgorithmException ex) {
                    throw new IllegalStateException(ex);
                }
            }
            @Override
            public void started(TaskEvent te) {
            }

            @Override
            public void finished(TaskEvent te) {
                if (te.getKind() == Kind.ANALYZE) {
                    if (te.getSourceFile().isNameCompatible("module-info", JavaFileObject.Kind.SOURCE)) {
                        ModuleElement mod = (ModuleElement) Trees.instance(jt).getElement(new TreePath(te.getCompilationUnit()));
                        new APIVisitor(apiHash).visit(mod);
                    } else if (te.getSourceFile().isNameCompatible("package-info", JavaFileObject.Kind.SOURCE)) {
                        //ignore - cannot contain important changes (?)
                    } else {
                        TypeElement clazz = te.getTypeElement();
                        ModuleElement mod = jt.getElements().getModuleOf(clazz);
                        Set<PackageElement> thisModulePackages = apiPackages.computeIfAbsent(mod, m -> {
                            return ElementFilter.exportsIn(mod.getDirectives())
                                                .stream()
                                                .map(ed -> ed.getPackage())
                                                .collect(Collectors.toSet());
                        });
                        if (thisModulePackages.contains(jt.getElements().getPackageOf(clazz))) {
                            new APIVisitor(apiHash).visit(clazz);
                        }
                    }
                }
                if (te.getKind() == Kind.COMPILATION && !noApiChange.get() &&
                    compiler.errorCount() == 0) {
                    try (OutputStream out = Files.newOutputStream(internalAPIDigestFile)) {
                        String hashes = internalAPI.entrySet()
                                                   .stream()
                                                   .map(e -> e.getKey() + "=" + e.getValue())
                                                   .collect(Collectors.joining("\n"));
                        out.write(hashes.getBytes(StandardCharsets.UTF_8));
                    } catch (IOException ex) {
                        throw new IllegalStateException(ex);
                    }
                    String previousSignature = null;
                    File digestFile = new File(args[0]);
                    try (InputStream in = new FileInputStream(digestFile)) {
                        previousSignature = new String(in.readAllBytes(), "UTF-8");
                    } catch (IOException ex) {
                        //ignore
                    }
                    String currentSignature = Depend.this.toString(apiHash.digest());
                    if (!Objects.equals(previousSignature, currentSignature)) {
                        digestFile.getParentFile().mkdirs();
                        try (OutputStream out = new FileOutputStream(digestFile)) {
                            out.write(currentSignature.getBytes("UTF-8"));
                        } catch (IOException ex) {
                            throw new IllegalStateException(ex);
                        }
                    }
                }
            }
        });
    }

    private void addExports() {
        var systemCompiler = ToolProvider.getSystemJavaCompiler();
        try (JavaFileManager jfm = systemCompiler.getStandardFileManager(null, null, null)) {
            JavaFileManager fm = new ForwardingJavaFileManager<JavaFileManager>(jfm) {
                @Override
                public ClassLoader getClassLoader(JavaFileManager.Location location) {
                    if (location == StandardLocation.CLASS_PATH) {
                        return Depend.class.getClassLoader();
                    }
                    return super.getClassLoader(location);
                }
            };
            ((JavacTask) systemCompiler.getTask(null, fm, null,
                                                List.of("-proc:only", "-XDaccessInternalAPI=true"),
                                                List.of("java.lang.Object"), null))
                                       .analyze();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private com.sun.tools.javac.util.List<JCCompilationUnit> doFilteredParse(
            JavaCompiler compiler, Iterable<JavaFileObject> fileObjects, Set<Path> modified,
            Map<String, String> internalAPI, AtomicBoolean noApiChange,
            boolean debug) {
        Map<JavaFileObject, JCCompilationUnit> files2CUT = new IdentityHashMap<>();
        boolean fullRecompile = modified.stream()
                                        .map(Path::toString)
                                        .anyMatch(f -> !StringUtils.toLowerCase(f).endsWith(".java"));
        ListBuffer<JCCompilationUnit> result = new ListBuffer<>();
        for (JavaFileObject jfo : fileObjects) {
            if (modified.contains(Path.of(jfo.getName()))) {
                JCCompilationUnit parsed = compiler.parse(jfo);
                files2CUT.put(jfo, parsed);
                String currentSignature = treeDigest(parsed);
                if (!currentSignature.equals(internalAPI.get(jfo.getName()))) {
                    fullRecompile |= true;
                    internalAPI.put(jfo.getName(), currentSignature);
                }
                result.add(parsed);
            }
        }
        if (fullRecompile) {
            for (JavaFileObject jfo : fileObjects) {
                if (!modified.contains(Path.of(jfo.getName()))) {
                    JCCompilationUnit parsed = files2CUT.get(jfo);
                    if (parsed == null) {
                        parsed = compiler.parse(jfo);
                        internalAPI.put(jfo.getName(), treeDigest(parsed));
                    }
                    result.add(parsed);
                }
            }
        } else {
            noApiChange.set(true);
        }
        if (debug) {
            long allJavaInputs = StreamSupport.stream(fileObjects.spliterator(), false).count();
            String module = StreamSupport.stream(fileObjects.spliterator(), false)
                         .map(fo -> fo.toUri().toString())
                         .filter(path -> path.contains("/share/classes/"))
                         .map(path -> path.substring(0, path.indexOf("/share/classes/")))
                         .map(path -> path.substring(path.lastIndexOf("/") + 1))
                         .findAny()
                         .orElseGet(() -> "unknown");
            String nonJavaModifiedFiles = modified.stream()
                                                  .map(Path::toString)
                                                  .filter(f -> !StringUtils.toLowerCase(f)
                                                                           .endsWith(".java"))
                                                  .collect(Collectors.joining(", "));
            System.err.println("compiling module: " + module +
                               ", all Java inputs: " + allJavaInputs +
                               ", modified files (Java or non-Java): " + modified.size() +
                               ", full recompile: " + fullRecompile +
                               ", non-Java modified files: " + nonJavaModifiedFiles);
        }
        return result.toList();
    }

    private String treeDigest(JCCompilationUnit cu) {
        try {
            TreeVisitor v = new TreeVisitor(MessageDigest.getInstance("MD5"));
            v.scan(cu, null);
            return Depend.this.toString(v.apiHash.digest());
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException(ex);
        }
    }
    private String toString(byte[] digest) {
        return HexFormat.of().withUpperCase().formatHex(digest);
    }

    private static final class APIVisitor implements ElementVisitor<Void, Void>,
                                                     TypeVisitor<Void, Void>,
                                                     AnnotationValueVisitor<Void, Void>,
                                                     DirectiveVisitor<Void, Void> {

        private final MessageDigest apiHash;
        private final Charset utf8;

        public APIVisitor(MessageDigest apiHash) {
            this.apiHash = apiHash;
            utf8 = Charset.forName("UTF-8");
        }

        public Void visit(Iterable<? extends Element> list, Void p) {
            list.forEach(e -> visit(e, p));
            return null;
        }

        private void update(CharSequence data) {
            apiHash.update(data.toString().getBytes(utf8));
        }

        private void visit(Iterable<? extends TypeMirror> types) {
            for (TypeMirror type : types) {
                visit(type);
            }
        }

        private void updateAnnotation(AnnotationMirror am) {
            update("@");
            visit(am.getAnnotationType());
            am.getElementValues()
              .keySet()
              .stream()
              .sorted((ee1, ee2) -> ee1.getSimpleName().toString().compareTo(ee2.getSimpleName().toString()))
              .forEach(ee -> {
                  visit(ee);
                  visit(am.getElementValues().get(ee));
              });
        }

        private void updateAnnotations(Iterable<? extends AnnotationMirror> annotations) {
            for (AnnotationMirror am : annotations) {
                if (am.getAnnotationType().asElement().getAnnotation(Documented.class) == null)
                    continue;
                updateAnnotation(am);
            }
        }

        @Override
        public Void visit(Element e, Void p) {
            if (e.getKind() != ElementKind.MODULE &&
                !e.getModifiers().contains(Modifier.PUBLIC) &&
                !e.getModifiers().contains(Modifier.PROTECTED)) {
                return null;
            }
            update(e.getKind().name());
            update(e.getModifiers().stream()
                                   .map(mod -> mod.name())
                                   .collect(Collectors.joining(",", "[", "]")));
            update(e.getSimpleName());
            updateAnnotations(e.getAnnotationMirrors());
            return e.accept(this, p);
        }

        @Override
        public Void visit(Element e) {
            return visit(e, null);
        }

        @Override
        public Void visitModule(ModuleElement e, Void p) {
            update(String.valueOf(e.isOpen()));
            update(e.getQualifiedName());
            e.getDirectives()
             .stream()
             .forEach(d -> d.accept(this, null));
            return null;
        }

        @Override
        public Void visitPackage(PackageElement e, Void p) {
            throw new UnsupportedOperationException("Should not get here.");
        }

        @Override
        public Void visitType(TypeElement e, Void p) {
            visit(e.getTypeParameters(), p);
            visit(e.getSuperclass());
            visit(e.getInterfaces());
            visit(e.getEnclosedElements(), p);
            return null;
        }

        @Override
        public Void visitRecordComponent(RecordComponentElement e, Void p) {
            update(e.getSimpleName());
            visit(e.asType());
            return null;
        }

        @Override
        public Void visitVariable(VariableElement e, Void p) {
            visit(e.asType());
            update(String.valueOf(e.getConstantValue()));
            return null;
        }

        @Override
        public Void visitExecutable(ExecutableElement e, Void p) {
            update("<");
            visit(e.getTypeParameters(), p);
            update(">");
            visit(e.getReturnType());
            update("(");
            visit(e.getParameters(), p);
            update(")");
            visit(e.getThrownTypes());
            update(String.valueOf(e.getDefaultValue()));
            update(String.valueOf(e.isVarArgs()));
            return null;
        }

        @Override
        public Void visitTypeParameter(TypeParameterElement e, Void p) {
            visit(e.getBounds());
            return null;
        }

        @Override
        public Void visitUnknown(Element e, Void p) {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        public Void visit(TypeMirror t, Void p) {
            if (t == null) {
                update("null");
                return null;
            }
            update(t.getKind().name());
            updateAnnotations(t.getAnnotationMirrors());
            t.accept(this, p);
            return null;
        }

        @Override
        public Void visitPrimitive(PrimitiveType t, Void p) {
            return null; //done
        }

        @Override
        public Void visitNull(NullType t, Void p) {
            return null; //done
        }

        @Override
        public Void visitArray(ArrayType t, Void p) {
            visit(t.getComponentType());
            update("[]");
            return null;
        }

        @Override
        public Void visitDeclared(DeclaredType t, Void p) {
            update(((QualifiedNameable) t.asElement()).getQualifiedName());
            update("<");
            visit(t.getTypeArguments());
            update(">");
            return null;
        }

        @Override
        public Void visitError(ErrorType t, Void p) {
            return visitDeclared(t, p);
        }

        private final Set<Element> seenVariables = new HashSet<>();

        @Override
        public Void visitTypeVariable(TypeVariable t, Void p) {
            Element decl = t.asElement();
            if (!seenVariables.add(decl)) {
                return null;
            }
            visit(decl, null);
            visit(t.getLowerBound(), null);
            visit(t.getUpperBound(), null);
            seenVariables.remove(decl);
            return null;
        }

        @Override
        public Void visitWildcard(WildcardType t, Void p) {
            visit(t.getSuperBound());
            visit(t.getExtendsBound());
            return null;
        }

        @Override
        public Void visitExecutable(ExecutableType t, Void p) {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        public Void visitNoType(NoType t, Void p) {
            return null;//done
        }

        @Override
        public Void visitUnknown(TypeMirror t, Void p) {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        public Void visitUnion(UnionType t, Void p) {
            update("(");
            visit(t.getAlternatives());
            update(")");
            return null;
        }

        @Override
        public Void visitIntersection(IntersectionType t, Void p) {
            update("(");
            visit(t.getBounds());
            update(")");
            return null;
        }

        @Override
        public Void visit(AnnotationValue av, Void p) {
            return av.accept(this, p);
        }

        @Override
        public Void visitBoolean(boolean b, Void p) {
            update(String.valueOf(b));
            return null;
        }

        @Override
        public Void visitByte(byte b, Void p) {
            update(String.valueOf(b));
            return null;
        }

        @Override
        public Void visitChar(char c, Void p) {
            update(String.valueOf(c));
            return null;
        }

        @Override
        public Void visitDouble(double d, Void p) {
            update(String.valueOf(d));
            return null;
        }

        @Override
        public Void visitFloat(float f, Void p) {
            update(String.valueOf(f));
            return null;
        }

        @Override
        public Void visitInt(int i, Void p) {
            update(String.valueOf(i));
            return null;
        }

        @Override
        public Void visitLong(long i, Void p) {
            update(String.valueOf(i));
            return null;
        }

        @Override
        public Void visitShort(short s, Void p) {
            update(String.valueOf(s));
            return null;
        }

        @Override
        public Void visitString(String s, Void p) {
            update(s);
            return null;
        }

        @Override
        public Void visitType(TypeMirror t, Void p) {
            return visit(t);
        }

        @Override
        public Void visitEnumConstant(VariableElement c, Void p) {
            return visit(c);
        }

        @Override
        public Void visitAnnotation(AnnotationMirror a, Void p) {
            updateAnnotation(a);
            return null;
        }

        @Override
        public Void visitArray(List<? extends AnnotationValue> vals, Void p) {
            update("(");
            for (AnnotationValue av : vals) {
                visit(av);
            }
            update(")");
            return null;
        }

        @Override
        public Void visitUnknown(AnnotationValue av, Void p) {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        public Void visitRequires(RequiresDirective d, Void p) {
            update("RequiresDirective");
            update(String.valueOf(d.isStatic()));
            update(String.valueOf(d.isTransitive()));
            update(d.getDependency().getQualifiedName());
            return null;
        }

        @Override
        public Void visitExports(ExportsDirective d, Void p) {
            update("ExportsDirective");
            update(d.getPackage().getQualifiedName());
            if (d.getTargetModules() != null) {
                for (ModuleElement me : d.getTargetModules()) {
                    update(me.getQualifiedName());
                }
            } else {
                update("<none>");
            }
            return null;
        }

        @Override
        public Void visitOpens(OpensDirective d, Void p) {
            update("OpensDirective");
            update(d.getPackage().getQualifiedName());
            if (d.getTargetModules() != null) {
                for (ModuleElement me : d.getTargetModules()) {
                    update(me.getQualifiedName());
                }
            } else {
                update("<none>");
            }
            return null;
        }

        @Override
        public Void visitUses(UsesDirective d, Void p) {
            update("UsesDirective");
            update(d.getService().getQualifiedName());
            return null;
        }

        @Override
        public Void visitProvides(ProvidesDirective d, Void p) {
            update("ProvidesDirective");
            update(d.getService().getQualifiedName());
            update("(");
            for (TypeElement impl : d.getImplementations()) {
                update(impl.getQualifiedName());
            }
            update(")");
            return null;
        }

    }

    private static final class TreeVisitor extends TreeScanner<Void, Void> {

        private final Set<Name> seenIdentifiers = new HashSet<>();
        private final MessageDigest apiHash;
        private final Charset utf8;

        public TreeVisitor(MessageDigest apiHash) {
            this.apiHash = apiHash;
            utf8 = Charset.forName("UTF-8");
        }

        private void update(CharSequence data) {
            apiHash.update(data.toString().getBytes(utf8));
        }

        @Override
        public Void scan(Tree tree, Void p) {
            update("(");
            if (tree != null) {
                update(tree.getKind().name());
            };
            super.scan(tree, p);
            update(")");
            return null;
        }

        @Override
        public Void visitCompilationUnit(CompilationUnitTree node, Void p) {
            seenIdentifiers.clear();
            scan(node.getPackage(), p);
            scan(node.getTypeDecls(), p);
            scan(((JCCompilationUnit) node).getModuleDecl(), p);
            List<ImportTree> importantImports = new ArrayList<>();
            for (ImportTree imp : node.getImports()) {
                Tree t = imp.getQualifiedIdentifier();
                if (t.getKind() == Tree.Kind.MEMBER_SELECT) {
                    Name member = ((MemberSelectTree) t).getIdentifier();
                    if (member.contentEquals("*") || seenIdentifiers.contains(member)) {
                        importantImports.add(imp);
                    }
                } else {
                    //should not happen, possibly erroneous source?
                    importantImports.add(imp);
                }
            }
            importantImports.sort((imp1, imp2) -> {
                if (imp1.isStatic() ^ imp2.isStatic()) {
                    return imp1.isStatic() ? -1 : 1;
                } else {
                    return imp1.getQualifiedIdentifier().toString().compareTo(imp2.getQualifiedIdentifier().toString());
                }
            });
            scan(importantImports, p);
            return null;
        }

        @Override
        public Void visitIdentifier(IdentifierTree node, Void p) {
            update(node.getName());
            seenIdentifiers.add(node.getName());
            return super.visitIdentifier(node, p);
        }

        @Override
        public Void visitMemberSelect(MemberSelectTree node, Void p) {
            update(node.getIdentifier());
            return super.visitMemberSelect(node, p);
        }

        @Override
        public Void visitMemberReference(MemberReferenceTree node, Void p) {
            update(node.getName());
            return super.visitMemberReference(node, p);
        }

        @Override
        public Void scan(Iterable<? extends Tree> nodes, Void p) {
            update("(");
            super.scan(nodes, p);
            update(")");
            return null;
        }

        @Override
        public Void visitClass(ClassTree node, Void p) {
            update(node.getSimpleName());
            scan(node.getModifiers(), p);
            scan(node.getTypeParameters(), p);
            scan(node.getExtendsClause(), p);
            scan(node.getImplementsClause(), p);
            scan(node.getMembers()
                     .stream()
                     .filter(this::importantMember)
                     .collect(Collectors.toList()),
                 p);
            return null;
        }

        private boolean importantMember(Tree m) {
            return switch (m.getKind()) {
                case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD ->
                    !isPrivate(((ClassTree) m).getModifiers());
                case METHOD ->
                    !isPrivate(((MethodTree) m).getModifiers());
                case VARIABLE ->
                    !isPrivate(((VariableTree) m).getModifiers()) ||
                    isRecordComponent((VariableTree) m);
                case BLOCK -> false;
                default -> false;
            };
        }

        private boolean isPrivate(ModifiersTree mt) {
            return mt.getFlags().contains(Modifier.PRIVATE);
        }

        private boolean isRecordComponent(VariableTree vt) {
            return (((JCVariableDecl) vt).mods.flags & Flags.RECORD) != 0;
        }

        @Override
        public Void visitVariable(VariableTree node, Void p) {
            update(node.getName());
            return super.visitVariable(node, p);
        }

        @Override
        public Void visitMethod(MethodTree node, Void p) {
            update(node.getName());
            scan(node.getModifiers(), p);
            scan(node.getReturnType(), p);
            scan(node.getTypeParameters(), p);
            scan(node.getParameters(), p);
            scan(node.getReceiverParameter(), p);
            scan(node.getThrows(), p);
            scan(node.getDefaultValue(), p);
            return null;
        }

        @Override
        public Void visitLiteral(LiteralTree node, Void p) {
            update(String.valueOf(node.getValue()));
            return super.visitLiteral(node, p);
        }

        @Override
        public Void visitModifiers(ModifiersTree node, Void p) {
            update(node.getFlags().toString());
            return super.visitModifiers(node, p);
        }

        @Override
        public Void visitPrimitiveType(PrimitiveTypeTree node, Void p) {
            update(node.getPrimitiveTypeKind().name());
            return super.visitPrimitiveType(node, p);
        }

    }

    private class FilteredInitialFileParser implements InvocationHandler {

        private final JavaCompiler compiler;
        private final Set<Path> modified;
        private final Map<String, String> internalAPI;
        private final AtomicBoolean noApiChange;
        private final boolean debug;

        public FilteredInitialFileParser(JavaCompiler compiler,
                                         Set<Path> modified,
                                         Map<String, String> internalAPI,
                                         AtomicBoolean noApiChange,
                                         boolean debug) {
            this.compiler = compiler;
            this.modified = modified;
            this.internalAPI = internalAPI;
            this.noApiChange = noApiChange;
            this.debug = debug;
        }

        @Override
        @SuppressWarnings("unchecked")
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return switch (method.getName()) {
                case "parse" -> doFilteredParse(compiler,
                                                (Iterable<JavaFileObject>) args[0],
                                                modified,
                                                internalAPI,
                                                noApiChange,
                                                debug);
                default -> throw new UnsupportedOperationException();
            };
        }
    }
}
