/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.junit;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.ClasspathInfo.PathKind;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.modules.junit.TestabilityResult.SkippedClass;
import org.openide.filesystems.FileObject;

/**
 * Finds all non-annotation top-level classes in a compilation unit.
 * 
 * @author  Marian Petras
 */
public final class TopClassFinder {

    interface Filter {
        boolean passes(TypeElement topClass,
                       CompilationInfo compInfo);
    }

    static class BasicTestabilityFilter implements Filter {
        public boolean passes(TypeElement topClass,
                              CompilationInfo compInfo) {
            ElementKind elemKind = topClass.getKind();
            return (elemKind != ElementKind.ANNOTATION_TYPE)
                   && (elemKind.isClass()|| elemKind.isInterface());
        }
    }

    static final class MainClassOnly extends BasicTestabilityFilter {
        @Override
        public boolean passes(TypeElement topClass,
                              CompilationInfo compInfo) {
            if (!super.passes(topClass, compInfo)) {
                return false;
            }

            FileObject javaFileObj = compInfo.getFileObject();
            ClassPath sourceCP = compInfo.getClasspathInfo().getClassPath(PathKind.SOURCE);
            String qualifiedClassName = topClass.getQualifiedName().toString();
            return qualifiedClassName.equals(sourceCP.getResourceName(javaFileObj, '.', false));
        }
    }

    static final class ExtendedTestabilityFilter implements Filter {

        private final TestabilityJudge testabilityJudge;
        private final Collection<SkippedClass> nonTestable;
        private final long skipTestabilityResultMask;

        ExtendedTestabilityFilter(TestabilityJudge testabilityJudge,
                                  Collection<SkippedClass> nonTestable,
                                  long skipTestabilityResultMask) {
            this.testabilityJudge = testabilityJudge;
            this.nonTestable = nonTestable;
            this.skipTestabilityResultMask = skipTestabilityResultMask;
        }
        public boolean passes(TypeElement topClass,
                              CompilationInfo compInfo) {
            TestabilityResult testabilityStatus
                    = testabilityJudge.isClassTestable(compInfo,
                                                       topClass, skipTestabilityResultMask);
            if (testabilityStatus.isTestable()) {
                return true;
            } else {
                nonTestable.add(
                        new SkippedClass(topClass.getQualifiedName().toString(),
                                         testabilityStatus));
                return false;
            }
        }

    }
    
    private TopClassFinder() { }

    /**
     */
    private static class TopClassFinderTask
                            implements CancellableTask<CompilationController> {
        private final Filter filter;
        private List<ElementHandle<TypeElement>> topClassElems;
        private volatile boolean cancelled;
        private TopClassFinderTask() {
            this(new BasicTestabilityFilter());
        }
        private TopClassFinderTask(Filter filter) {
            this.filter = filter;
        }
        public void run(CompilationController controller) throws IOException {
            controller.toPhase(Phase.ELEMENTS_RESOLVED);
            if (cancelled) {
                return;
            }
            
            if (topClassElems == null) {
                topClassElems = new ArrayList<ElementHandle<TypeElement>>(10);
            }
            topClassElems.addAll(findTopClassElemHandles(
                                                controller,
                                                controller.getCompilationUnit(),
                                                filter));
        }
        public void cancel() {
            cancelled = true;
        }
    }
    
    /**
     */
    static List<ElementHandle<TypeElement>> findTopClasses(
                                                    JavaSource javaSource)
                                                        throws IOException {
        TopClassFinderTask analyzer = new TopClassFinderTask();
        javaSource.runUserActionTask(analyzer, true);
        return analyzer.topClassElems;
    }
    
    /**
     * Finds main top classes, i.e. those whose name matches with the name
     * of the file they reside in.
     */
    public static List<ElementHandle<TypeElement>> findMainTopClasses(
                                                    JavaSource javaSource)
                                                        throws IOException {
        TopClassFinderTask analyzer = new TopClassFinderTask(new MainClassOnly());
        javaSource.runUserActionTask(analyzer, true);
        return analyzer.topClassElems;
    }
    
    /**
     * Finds testable top-level classes, interfaces and enums in a given
     * Java source.
     * 
     * @param  javaSource  source in which testable classes should be found
     * @param  judge  {@code TestCreator} that will select testable
     *                top-level classes
     *                (see {@link TestCreator#isClassTestable})
     * @param  nonTestable  container where names of found non-testable classes
     *                      should be stored
     * @return  handles to testable top-level classes, interfaces and enums
     * @exception  java.lang.IllegalArgumentException
     *             if any of the parameters is {@code null}
     */
    static List<ElementHandle<TypeElement>> findTestableTopClasses(
                                                JavaSource javaSource,
                                                TestabilityJudge testabilityJudge,
                                                Collection<SkippedClass> nonTestable,
                                                long skipTestabilityResultMask)
                                                        throws IOException {
        TopClassFinderTask analyzer = new TopClassFinderTask(new ExtendedTestabilityFilter(testabilityJudge, nonTestable, skipTestabilityResultMask));
        javaSource.runUserActionTask(analyzer, true);
        return analyzer.topClassElems;
    }

    /**
     * 
     * @return  list of top classes, or an empty list of none were found
     */
    static List<ClassTree> findTopClasses(
                                        CompilationUnitTree compilationUnit,
                                        TreeUtilities treeUtils) {
        List<? extends Tree> typeDecls = compilationUnit.getTypeDecls();
        if ((typeDecls == null) || typeDecls.isEmpty()) {
            return Collections.<ClassTree>emptyList();
        }

        List<ClassTree> result = new ArrayList<ClassTree>(typeDecls.size());
        
        for (Tree typeDecl : typeDecls) {
            if (TreeUtilities.CLASS_TREE_KINDS.contains(typeDecl.getKind())) {
                ClassTree clsTree = (ClassTree) typeDecl;
                if (isTestable(clsTree, treeUtils)) {
                    result.add(clsTree);
                }
            }
        }

        return result;
    }

    /**
     * 
     * @return  list of {@code Element}s representing top classes,
     *          or an empty list of none were found
     */
    private static List<TypeElement> findTopClassElems(
                                        CompilationInfo compInfo,
                                        CompilationUnitTree compilationUnit,
                                        Filter filter) {
        List<? extends Tree> typeDecls = compilationUnit.getTypeDecls();
        if ((typeDecls == null) || typeDecls.isEmpty()) {
            return Collections.<TypeElement>emptyList();
        }
        
        List<TypeElement> result = new ArrayList<TypeElement>(typeDecls.size());

        Trees trees = compInfo.getTrees();
        for (Tree typeDecl : typeDecls) {
            if (TreeUtilities.CLASS_TREE_KINDS.contains(typeDecl.getKind())) {
                Element element = trees.getElement(
                        new TreePath(new TreePath(compilationUnit), typeDecl));
                TypeElement typeElement = (TypeElement) element;
                if (filter.passes(typeElement, compInfo)) {
                    result.add(typeElement);
                }
            }
        }

        return result;
    }

    /**
     * 
     * @return  list of handles to {@code Element}s representing top classes,
     *          or an empty list of none were found
     */
    private static List<ElementHandle<TypeElement>> findTopClassElemHandles(
                                        CompilationInfo compInfo,
                                        CompilationUnitTree compilationUnit,
                                        Filter filter) {
        return getElemHandles(
                    findTopClassElems(compInfo, compilationUnit, filter));
    }

    private static <T extends Element> List<ElementHandle<T>> getElemHandles(List<T> elements) {
        if (elements == null) {
            return null;
        }
        if (elements.isEmpty()) {
            return Collections.<ElementHandle<T>>emptyList();
        }

        List<ElementHandle<T>> handles = new ArrayList<ElementHandle<T>>(elements.size());
        for (T element : elements) {
            handles.add(ElementHandle.<T>create(element));
        }
        return handles;
    }

    private static boolean isTestable(ClassTree typeDecl,
                                      TreeUtilities treeUtils) {
        return !treeUtils.isAnnotation(typeDecl);
    }

    static boolean isTestable(TypeElement typeDeclElement) {
        ElementKind elemKind = typeDeclElement.getKind();
        return (elemKind != ElementKind.ANNOTATION_TYPE)
               && (elemKind.isClass()|| elemKind.isInterface());
    }
    
}
