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

import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.*;
import javax.tools.*;
import java.util.*;

/**
 * Check for public classes that are candidates to be re-declared as
 * sealed.
 *
 * As written, this processor can be run in two modes. The first mode
 * is part of a standard compile:
 *
 * $ javac SealedCandidate.java
 * $ javac -processor SealedCandidate Foo.java
 * 
 * (The --processor-path may need to be set too.)
 * 
 * The second mode analyzes modules instead:
 *
 * $ javac SealedCandidate.java
 * $ javac -proc:only -processor SealedCandidate -Amodule_list=java.base,java.desktop  Foo.java
 * 
 * In the latter case, as the source code is not available, warning
 * messages will provide less precise location information
 */
@SupportedAnnotationTypes("*")
@SupportedOptions({"module_list", "verbose"})
@SupportedSourceVersion(SourceVersion.RELEASE_19)
public class SealedCandidate extends AbstractProcessor {
    private List<String> moduleNames = null;
    private static boolean verbosePrinting = false;

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // Could do this in an overridden init method
        verbosePrinting = "true".equals(processingEnv.getOptions().get("verbose"));
        setModuleList();

        SealedCheckScanner scanner = new SealedCheckScanner(roundEnv, processingEnv);
        if (moduleNames.isEmpty()) { // Examine files being compiled
            if (!roundEnv.processingOver()) {
                for (var rootElement : roundEnv.getRootElements() ) {
                    scanner.scan(rootElement);
                }
            }
        } else {
            if (roundEnv.processingOver()) { // Once everything else is done...
                for(String name : moduleNames) {
                    ModuleElement mod = processingEnv.getElementUtils().getModuleElement(name);
                    if (mod == null) {
                        processingEnv.getMessager().printWarning("Module not found for " + name);
                        continue;
                    }
                    scanner.scan(mod);
                }
            }
        }
        return false;
    }

    // Could do this in an overridden init method...
    void setModuleList() {
        if (moduleNames != null) {
            return;
        } else {
            String moduleListValue = processingEnv.getOptions().get("module_list");
            System.out.println("moduleListValue = " + moduleListValue); 
            if (moduleListValue == null) {
                moduleNames = List.of();
            } else {
                moduleNames = List.of(moduleListValue.split(","));
            }
            return;
        }
    }

    private static class SealedCheckScanner extends ElementScanner14<Void, Void> {
        private final RoundEnvironment roundEnv;
        private final ProcessingEnvironment procEnv;
        private final Messager messager;
        private final ElementVisitor<Void, Void> sealedTypeVisitor;

        SealedCheckScanner(RoundEnvironment roundEnv, ProcessingEnvironment procEnv) {
            this.roundEnv = roundEnv;
            this.procEnv = procEnv;
            this.messager = procEnv.getMessager();
            this.sealedTypeVisitor = new SealedTypeVisitor(messager);
        }

        @Override
        public Void visitType(TypeElement e, Void p) {
            if (verbosePrinting) {
                System.out.println("Visiting " + e.getQualifiedName());
            }
            super.visitType(e, p);
            sealedTypeVisitor.visit(e);
            return null;
        }

        private static class SealedTypeVisitor extends SimpleElementVisitor14<Void, Void> {
            private Messager messager;
            public SealedTypeVisitor(Messager messager){
                super();
                this.messager = messager;
            }

            @Override
            public Void visitType(TypeElement e, Void p) {
                // Interfaces and annotation types are not considered
                // by this analysis. Neither records nor enums can be
                // used-extended.
                if (e.getKind() != ElementKind.CLASS) {
                    return null;
                }
                Set<Modifier> typeModifiers = e.getModifiers();
                if (typeModifiers.contains(Modifier.FINAL)  ||
                    typeModifiers.contains(Modifier.SEALED) ||
                    typeModifiers.contains(Modifier.NON_SEALED) ) {
                    // Not a candidate to be re-declared as sealed
                    return null;
                }

                if (typeModifiers.contains(Modifier.PUBLIC)) {
                    // If any public or protected ctor, exit as the
                    // class can be arbitrarily subclassed and sealing
                    // would break that property.

                    // If at least one package-level ctor, report as a
                    // candidate
                    
                    boolean anyPackageAccess = false;
                    for (var ctor : ElementFilter.constructorsIn(e.getEnclosedElements())) {
                        var modifiers = ctor.getModifiers();
                        if (modifiers.contains(Modifier.PUBLIC) ||
                            modifiers.contains(Modifier.PROTECTED)) {
                            return null;
                        }
                        if (modifiers.contains(Modifier.PRIVATE)) {
                            continue;
                        }
                        anyPackageAccess = true;
                    }
                    if (anyPackageAccess) {
                        messager.printNote("sealing candidate " + e.getQualifiedName() , e);
                    }
                }
                return null;
            }
        }
    }
}
