/*
 * Copyright (c) 2014, 2015, 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 7101822
 * @summary Verify that the processing of classes in TypeEnter runs in the correct order.
 * @library /tools/lib
 * @modules jdk.compiler/com.sun.tools.javac.api
 *          jdk.compiler/com.sun.tools.javac.code
 *          jdk.compiler/com.sun.tools.javac.file
 *          jdk.compiler/com.sun.tools.javac.tree
 *          jdk.compiler/com.sun.tools.javac.util
 * @build annotations.TriggersComplete annotations.TriggersCompleteRepeat annotations.Phase
 * @build DependenciesTest
 * @run main DependenciesTest
 */

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Stream;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;

import annotations.*;
import com.sun.source.tree.AnnotationTree;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Context.Factory;
import com.sun.tools.javac.util.Dependencies;


public class DependenciesTest {
    public static void main(String... args) throws IOException {
        new DependenciesTest().run();
    }

    void run() throws IOException {
        Path src = Paths.get(System.getProperty("test.src"), "tests");

        try (Stream<Path> tests = Files.list(src)) {
            tests.map(p -> Files.isRegularFile(p) ? Stream.of(p) : silentWalk(p))
                 .forEach(this :: runTest);
        }
    }

    Stream<Path> silentWalk(Path src) {
        try {
            return Files.walk(src).filter(Files :: isRegularFile);
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    void runTest(Stream<Path> inputs) {
        JavacTool tool = JavacTool.create();
        try (JavacFileManager fm = tool.getStandardFileManager(null, null, null)) {
            Path classes = Paths.get(System.getProperty("test.classes"));
            Iterable<? extends JavaFileObject> reconFiles =
                    fm.getJavaFileObjectsFromFiles(inputs.sorted().map(p -> p.toFile()) :: iterator);
            List<String> options = Arrays.asList("-classpath", classes.toAbsolutePath().toString());
            JavacTask reconTask = tool.getTask(null, fm, null, options, null, reconFiles);
            Iterable<? extends CompilationUnitTree> reconUnits = reconTask.parse();
            JavacTrees reconTrees = JavacTrees.instance(reconTask);
            SearchAnnotations scanner = new SearchAnnotations(reconTrees,
                                                              reconTask.getElements());
            List<JavaFileObject> validateFiles = new ArrayList<>();

            reconTask.analyze();
            scanner.scan(reconUnits, null);

            for (CompilationUnitTree cut : reconUnits) {
                validateFiles.add(ClearAnnotations.clearAnnotations(reconTrees, cut));
            }

            Context validateContext = new Context();
            TestDependencies.preRegister(validateContext);
            JavacTask validateTask =
                    tool.getTask(null, fm, null, options, null, validateFiles, validateContext);

            validateTask.analyze();

            TestDependencies deps = (TestDependencies) Dependencies.instance(validateContext);

            if (!scanner.topLevel2Expected.equals(deps.topLevel2Completing)) {
                throw new IllegalStateException(  "expected=" + scanner.topLevel2Expected +
                                                "; actual=" + deps.topLevel2Completing);
            }
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        } finally {
            inputs.close();
        }
    }

    static final class TestDependencies extends Dependencies {

        public static void preRegister(Context context) {
            context.put(dependenciesKey, (Factory<Dependencies>) TestDependencies :: new);
        }

        public TestDependencies(Context context) {
            super(context);
        }

        final Stack<PhaseDescription> inProcess = new Stack<>();

        String topLevelMemberEnter;
        Map<String, Set<PhaseDescription>> topLevel2Completing = new HashMap<>();

        @Override
        public void push(ClassSymbol s, CompletionCause phase) {
            String flatname = s.flatName().toString();
            for (Phase p : Phase.values()) {
                if (phase == p.cause) {
                    inProcess.push(new PhaseDescription(flatname, p));
                    return ;
                }
            }
            if (phase == CompletionCause.MEMBER_ENTER) {
                if (inProcess.isEmpty()) {
                    topLevelMemberEnter = flatname;
                } else {
                    for (PhaseDescription running : inProcess) {
                        if (running == null)
                            continue;

                        Set<PhaseDescription> completing =
                                topLevel2Completing.computeIfAbsent(running.flatname, $ -> new HashSet<>());

                        completing.add(new PhaseDescription(flatname, running.phase));
                    }
                }
            }
            inProcess.push(null);
        }

        @Override
        public void pop() {
            inProcess.pop();
        }

    }

    static final class SearchAnnotations extends TreePathScanner<Void, Void> {
        final Trees trees;
        final Elements elements;
        final TypeElement triggersCompleteAnnotation;
        final TypeElement triggersCompleteRepeatAnnotation;
        final Map<String, Set<PhaseDescription>> topLevel2Expected =
                new HashMap<>();

        public SearchAnnotations(Trees trees, Elements elements) {
            this.trees = trees;
            this.elements = elements;
            this.triggersCompleteAnnotation =
                    elements.getTypeElement(TriggersComplete.class.getName());
            this.triggersCompleteRepeatAnnotation =
                    elements.getTypeElement(TriggersCompleteRepeat.class.getName());
        }

        @Override
        public Void visitClass(ClassTree node, Void p) {
            TypeElement te = (TypeElement) trees.getElement(getCurrentPath());
            Set<PhaseDescription> expected = new HashSet<>();

            for (AnnotationMirror am : getTriggersCompleteAnnotation(te)) {
                TypeMirror of = (TypeMirror) findAttribute(am, "of").getValue();
                Name ofName = elements.getBinaryName((TypeElement) ((DeclaredType) of).asElement());
                Element at = (Element) findAttribute(am, "at").getValue();
                Phase phase = Phase.valueOf(at.getSimpleName().toString());
                expected.add(new PhaseDescription(ofName.toString(), phase));
            }

            if (!expected.isEmpty())
                topLevel2Expected.put(elements.getBinaryName(te).toString(), expected);

            return super.visitClass(node, p);
        }

        Collection<AnnotationMirror> getTriggersCompleteAnnotation(TypeElement te) {
            for (AnnotationMirror am : te.getAnnotationMirrors()) {
                if (triggersCompleteAnnotation.equals(am.getAnnotationType().asElement())) {
                    return Collections.singletonList(am);
                }
                if (triggersCompleteRepeatAnnotation.equals(am.getAnnotationType().asElement())) {
                    return (Collection<AnnotationMirror>) findAttribute(am, "value").getValue();
                }
            }
            return Collections.emptyList();
        }

        AnnotationValue findAttribute(AnnotationMirror mirror, String name) {
            for (Entry<? extends ExecutableElement, ? extends AnnotationValue> e :
                    mirror.getElementValues().entrySet()) {
                if (e.getKey().getSimpleName().contentEquals(name)) {
                    return e.getValue();
                }
            }

            throw new IllegalStateException("Could not find " + name + " in " + mirror);
        }
    }

    static final class ClearAnnotations extends TreePathScanner<Void, Void> {
        final SourcePositions positions;
        final List<int[]> spans2Clear = new ArrayList<>();

        ClearAnnotations(Trees trees) {
            this.positions = trees.getSourcePositions();
        }

        @Override
        public Void visitAnnotation(AnnotationTree node, Void p) {
            removeCurrentNode();
            return null;
        }

        @Override
        public Void visitImport(ImportTree node, Void p) {
            if (node.getQualifiedIdentifier().toString().startsWith("annotations.")) {
                removeCurrentNode();
                return null;
            }
            return super.visitImport(node, p);
        }

        void removeCurrentNode() {
            CompilationUnitTree topLevel = getCurrentPath().getCompilationUnit();
            Tree node = getCurrentPath().getLeaf();
            spans2Clear.add(new int[] {(int) positions.getStartPosition(topLevel, node),
                                       (int) positions.getEndPosition(topLevel, node)});
        }

        static JavaFileObject clearAnnotations(Trees trees, CompilationUnitTree cut)
                throws IOException {
            ClearAnnotations a = new ClearAnnotations(trees);
            a.scan(cut, null);
            Collections.sort(a.spans2Clear, (s1, s2) -> s2[0] - s1[0]);
            StringBuilder result = new StringBuilder(cut.getSourceFile().getCharContent(true));
            for (int[] toClear : a.spans2Clear) {
                result.delete(toClear[0], toClear[1]);
            }
            return new TestJavaFileObject(cut.getSourceFile().toUri(), result.toString());
        }

    }

    static final class PhaseDescription {
        final String flatname;
        final Phase phase;

        public PhaseDescription(String flatname, Phase phase) {
            this.flatname = flatname;
            this.phase = phase;
        }

        @Override
        public String toString() {
            return "@annotations.TriggersComplete(of=" + flatname + ".class," +
                   "at=annotations.Phase." + phase + ')';
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 89 * hash + Objects.hashCode(this.flatname);
            hash = 89 * hash + Objects.hashCode(this.phase);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final PhaseDescription other = (PhaseDescription) obj;
            if (!Objects.equals(this.flatname, other.flatname)) {
                return false;
            }
            if (this.phase != other.phase) {
                return false;
            }
            return true;
        }

    }

    static final class TestJavaFileObject extends SimpleJavaFileObject {
        private final String content;

        public TestJavaFileObject(URI uri, String content) {
            super(uri, Kind.SOURCE);
            this.content = content;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return content;
        }

    }
}