/*
 * 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.java.j2seprofiles;

import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.prefs.Preferences;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.fileinfo.NonRecursiveFolder;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.java.queries.SourceForBinaryQuery;
import org.netbeans.api.java.queries.SourceLevelQuery;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.java.source.support.ProfileSupport;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.modules.analysis.spi.Analyzer;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.netbeans.modules.refactoring.api.Scope;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.ErrorDescriptionFactory;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.editor.hints.Severity;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.Pair;
import org.openide.util.lookup.ServiceProvider;

/**
 *
 * @author Tomas Zezula
 */
public class ProfilesAnalyzer implements Analyzer {

    private static final String ICON = "org/netbeans/modules/java/j2seprofiles/resources/profile.png"; //NOI18N

    private final Context context;
    private final Result result;
    private final AtomicBoolean canceled = new AtomicBoolean();

    private ProfilesAnalyzer(
            @NonNull final Context context,
            @NonNull final Result result) {
        Parameters.notNull("context", context); //NOI18N
        Parameters.notNull("result", result);   //NOI18N
        this.context = context;
        this.result = result;
    }

    @Override
    @NonNull
    @NbBundle.Messages ({
        "MSG_BuildingClasses=Building Classes..."
    })
    public Iterable<? extends ErrorDescription> analyze() {
        context.progress(Bundle.MSG_BuildingClasses());
        try {
            final JavaSource js = JavaSource.create(
            ClasspathInfo.create(
                JavaPlatform.getDefault().getBootstrapLibraries() ,
                ClassPath.EMPTY,
                ClassPath.EMPTY));
            final Future<Void> f = js.runWhenScanFinished(
                    new Task<CompilationController>() {
                        @Override
                        public void run(CompilationController parameter) throws Exception {
                            analyzeImpl();
                        }
                    }, true);
            while (!f.isDone()) {
                try {
                    f.get(2500, TimeUnit.MILLISECONDS);
                } catch (InterruptedException ex) {
                    break;
                } catch (ExecutionException ex) {
                    throw new IOException(ex);
                } catch (TimeoutException ex) {
                    if (canceled.get()) {
                        break;
                    }
                }
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return Collections.<ErrorDescription>emptySet();
    }

    @NbBundle.Messages ({
        "MSG_AnalyzingRoot=Analyzing root {0}"
    })
    private void  analyzeImpl() {
        final Scope scope = context.getScope();
        final Set<FileObject> roots = new HashSet<>();
        final Set<FileObject> completeRoots = scope.getSourceRoots();
        final Map<FileObject, Filter.Builder> filters = new HashMap<>();
        roots.addAll(completeRoots);
        for (NonRecursiveFolder nrf : scope.getFolders()) {
            final FileObject ownerRoot = findOwnerRoot(nrf.getFolder());
            if (ownerRoot != null && !completeRoots.contains(ownerRoot)) {
                roots.add(ownerRoot);
                Filter.Builder filterForRoot = filters.get(ownerRoot);
                if (filterForRoot == null) {
                    filterForRoot = new Filter.Builder();
                    filters.put(ownerRoot, filterForRoot);
                }
                filterForRoot.addNonRecursiveFolder(nrf);
            }
        }
        for (FileObject f : scope.getFiles()) {
            Collection<FileObject> ownerRoots = asCollection(findOwnerRoot(f));
            if (ownerRoots.isEmpty()) {
                ownerRoots = findOwnedRoots(f);
            }
            for (FileObject ownerRoot : ownerRoots) {
                if (!completeRoots.contains(ownerRoot)) {
                    roots.add(ownerRoot);
                    Filter.Builder filterForRoot = filters.get(ownerRoot);
                    if (filterForRoot == null) {
                        filterForRoot = new Filter.Builder();
                        filters.put(ownerRoot, filterForRoot);
                    }
                    if (f.isFolder()) {
                        filterForRoot.addFolder(f);
                    } else if (f.isData()) {
                        filterForRoot.addFile(f);
                    }
                }
            }
        }
        final ProfileProvider pp = new ProfileProvider(context);
        final HashMap<Pair<URI,SourceLevelQuery.Profile>,Set<Project>> submittedBinaries = new HashMap<>();
        final Set<URI> submittedSources = new HashSet<>();
        final CollectorFactory.ViolationsProvider vp = new CollectorFactory.ViolationsProvider();
        for (FileObject root : roots) {
            if (canceled.get()) {
                break;
            }
            final SourceLevelQuery.Profile profile = pp.findProfile(root);
            if (profile != SourceLevelQuery.Profile.DEFAULT) {
                final ClassPath boot = ClassPath.getClassPath(root, ClassPath.BOOT);
                final ClassPath compile = ClassPath.getClassPath(root, ClassPath.COMPILE);
                if (boot == null || compile == null) {
                    continue;
                }
                final Project owner = FileOwnerQuery.getOwner(root);
                if (owner == null) {
                    continue;
                }
                submittedSources.add(root.toURI());
                final Set<Project> projectRefs = new HashSet<>();
                ProfileSupport.findProfileViolations(
                    profile,
                    cpToRootUrls(boot, profile, null, null, null),
                    cpToRootUrls(compile, profile, owner, submittedBinaries, projectRefs),
                    Collections.singleton(root.toURL()),
                    EnumSet.of(
                        ProfileSupport.Validation.BINARIES_BY_MANIFEST,
                        ProfileSupport.Validation.BINARIES_BY_CLASS_FILES,
                        ProfileSupport.Validation.SOURCES),
                    new CollectorFactory(vp, profile, canceled));
                verifySubProjects(projectRefs, owner, profile, result);
            }
        }
        if (!canceled.get()) {
            context.start(submittedBinaries.size() + submittedSources.size());
            int count = 0;
            while (!submittedBinaries.isEmpty() || !submittedSources.isEmpty()) {
                try {
                    final Pair<Pair<URI,SourceLevelQuery.Profile>,Collection<? extends ProfileSupport.Violation>> violationsPair = vp.poll(2500);
                    if (violationsPair == null) {
                        continue;
                    }
                    if (canceled.get()) {
                        break;
                    }
                    final URI rootURI = violationsPair.first().first();
                    final Set<Project> projects = submittedBinaries.remove(violationsPair.first());
                    final boolean binary = projects != null;
                    if (!binary) {
                        submittedSources.remove(rootURI);
                    }
                    final Collection<? extends ProfileSupport.Violation> violations = violationsPair.second();
                    if (violations.isEmpty()) {
                        continue;
                    }
                    final FileObject root = URLMapper.findFileObject(rootURI.toURL());
                    if (root == null) {
                        continue;
                    }
                    context.progress(Bundle.MSG_AnalyzingRoot(FileUtil.getFileDisplayName(archiveFileOrFolder(root))), count);
                    if (binary) {
                        //Binary roots
                        verifyBinaryRoot(root, violations, projects, result);
                    } else {
                        //Source roots
                        final Filter.Builder filter = filters.get(root);
                        verifySourceRoot(root, filter == null ? null : filter.build(), violations, result);
                    }
                    context.progress(++count);
                } catch (InterruptedException ex) {
                    break;
                } catch (MalformedURLException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
            context.finish();
        }
    }

    @Override
    public boolean cancel() {
        canceled.set(true);
        return true;
    }

    @NbBundle.Messages ({
        "MSG_ProjectHigherProfile=Project requires profile: {0}",
        "DESC_ProjectHigherProfile=The project {0} located in {1} requires profile: {2}",
    })
    private static void verifySubProjects(
        @NonNull final Collection<? extends Project> projectRefs,
        @NonNull final Project owner,
        @NonNull final SourceLevelQuery.Profile profile,
        @NonNull final Result result) {
        for (Project p : projectRefs) {
            final FileObject pHome = p.getProjectDirectory();
            final SourceLevelQuery.Profile pProfile = SourceLevelQuery.getSourceLevel2(pHome).getProfile();
            if (pProfile.compareTo(profile) > 0) {
                result.reportError(owner, ErrorDescriptionFactory.createErrorDescription(
                        null,
                        Severity.ERROR,
                        Bundle.MSG_ProjectHigherProfile(pProfile.getDisplayName()),
                        Bundle.DESC_ProjectHigherProfile(
                            ProjectUtils.getInformation(p).getDisplayName(),
                            FileUtil.getFileDisplayName(pHome),
                            profile.getDisplayName()),
                        ErrorDescriptionFactory.lazyListForFixes(Collections.<Fix>emptyList()),
                        p.getProjectDirectory(),
                        null));
            }
        }
    }

    @NbBundle.Messages ({
        "MSG_LibraryHigherProfile=Library requires profile: {0}",
        "DESC_LibraryHigherProfile=The Profile attribute in the manifest of the library {0} requires profile: {1}",
        "MSG_LibraryInvalidProfile=Library has invalid profile",
        "DESC_LibraryInvalidProfile=The library Manifest of the library {0} has invalid value of the Profile attribute",
        "MSG_ClassFileHigherProfile={0} requires profile: {1}",
        "DESC_ClassFileHigherProfile=The {0} used in class {1} of library {2} requires profile: {3}"
    })
    private static void verifyBinaryRoot(
            @NonNull final FileObject root,
            @NonNull final Collection<? extends ProfileSupport.Violation> violations,
            @NonNull final Set<Project> projects,
            @NonNull final Result result) {
        for (ProfileSupport.Violation violation : violations) {
            final URL fileURL = violation.getFile();
            FileObject target;
            String message;
            String description;
            final SourceLevelQuery.Profile requiredProfile = violation.getRequiredProfile();
            if (fileURL == null) {
                target = root;
                if (requiredProfile != null) {
                    message = Bundle.MSG_LibraryHigherProfile(requiredProfile.getDisplayName());
                    description = Bundle.DESC_LibraryHigherProfile(
                            FileUtil.getFileDisplayName(archiveFileOrFolder(target)),
                            requiredProfile.getDisplayName());
                } else {
                    message = Bundle.MSG_LibraryInvalidProfile();
                    description = Bundle.DESC_LibraryInvalidProfile(FileUtil.getFileDisplayName(archiveFileOrFolder(target)));
                }
            } else {
                final ElementHandle<TypeElement> usedType = violation.getUsedType();
                assert usedType != null;
                assert requiredProfile != null;
                target = URLMapper.findFileObject(fileURL);
                message = Bundle.MSG_ClassFileHigherProfile(
                        simpleName(usedType),
                        requiredProfile.getDisplayName());
                description = Bundle.DESC_ClassFileHigherProfile(
                        usedType.getQualifiedName(),
                        stripExtension(FileUtil.getRelativePath(root, target)),
                        FileUtil.getFileDisplayName(archiveFileOrFolder(root)),
                        requiredProfile.getDisplayName());
            }
            for (Project p : projects) {
                result.reportError(p, ErrorDescriptionFactory.createErrorDescription(
                    null,
                    Severity.ERROR,
                    message,
                    description,
                    ErrorDescriptionFactory.lazyListForFixes(Collections.<Fix>emptyList()),
                    target,
                    null));
            }
        }
    }

    private static void verifySourceRoot(
            @NonNull final FileObject root,
            @NullAllowed final Filter filter,
            @NonNull final Collection<? extends ProfileSupport.Violation> violations,
            @NonNull final Result result) {
        try {
            final ClasspathInfo cpInfo = ClasspathInfo.create(root);
            final Map<FileObject,Collection<ProfileSupport.Violation>> violationsByFiles =
                new HashMap<>();
            final JavaSource js = JavaSource.create(
                cpInfo,
                violationsToFileObjects(violations, filter, violationsByFiles));
            if (js != null) {
                js.runUserActionTask(
                    new Task<CompilationController>(){
                        @Override
                        public void run(@NonNull final CompilationController cc) throws Exception {
                            cc.toPhase(JavaSource.Phase.RESOLVED);
                            final FileObject currentFile = cc.getFileObject();
                            if (currentFile != null) {
                                final FindPosScanner fps = new FindPosScanner(
                                        currentFile,
                                        cc.getTrees(),
                                        cc.getElements(),
                                        cc.getTreeUtilities(),
                                        violationsByFiles.get(currentFile),
                                        result);
                                fps.scan(cc.getCompilationUnit(), null);
                            }
                        }
                    },
                    true);
            }
        } catch (IOException ioe) {
            Exceptions.printStackTrace(ioe);
        }
    }

    @NonNull
    private Collection<FileObject> asCollection(@NullAllowed final FileObject file) {
        return file == null ?
            Collections.<FileObject>emptySet() :
            Collections.singleton(file);
    }


    @CheckForNull
    private static FileObject findOwnerRoot(@NonNull final FileObject file) {
        final ClassPath sourcePath = ClassPath.getClassPath(file, ClassPath.SOURCE);
        return sourcePath == null ? null : sourcePath.findOwnerRoot(file);
    }

    @NonNull
    private static Collection<FileObject> findOwnedRoots(@NonNull final FileObject file) {
        final Project p = FileOwnerQuery.getOwner(file);
        if (p == null || !file.equals(p.getProjectDirectory())) {
            return Collections.emptySet();
        }
        final Sources sources = ProjectUtils.getSources(p);
        final Collection<FileObject> res = new ArrayList<>();
        for (SourceGroup grp : sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)) {
            res.add(grp.getRootFolder());
        }
        return res;
    }

    @NonNull
    private static FileObject archiveFileOrFolder(@NonNull final FileObject root) {
        final FileObject archiveFile = FileUtil.getArchiveFile(root);
        return archiveFile != null ? archiveFile : root;
    }

    @NonNull
    private static String stripExtension(@NonNull final String path) {
        final int index = path.lastIndexOf('.');    //NOI18N
        return index <= 0 ? path : path.substring(0, index);
    }

    @NonNull
    private static String simpleName(@NullAllowed final ElementHandle<TypeElement> eh) {
        if (eh == null) {
            return "";  //NOI18N
        }
        final String qn = eh.getQualifiedName();
        int index = qn.lastIndexOf('.');    //NOI18N
        return index < 0 ? qn : qn.substring(index+1);
    }

    @NonNull
    private static Iterable<URL> cpToRootUrls(
            @NonNull final ClassPath cp,
            @NonNull final SourceLevelQuery.Profile requiredProfile,
            @NullAllowed final Project owner,
            @NullAllowed final Map<Pair<URI,SourceLevelQuery.Profile>,Set<Project>> alreadyProcessed,
            @NullAllowed final Set<? super Project> projectRefs) {
        assert (owner == null && alreadyProcessed == null && projectRefs == null) ||
               (owner != null && alreadyProcessed != null && projectRefs != null);
        final Queue<URL> res = new ArrayDeque<>();
nextCpE:for (ClassPath.Entry e : cp.entries()) {
            final URL url = e.getURL();
            try {
                if (projectRefs != null) {
                    final SourceForBinaryQuery.Result2 sfbqRes = SourceForBinaryQuery.findSourceRoots2(url);
                    if (sfbqRes.preferSources()) {
                        for (FileObject src : sfbqRes.getRoots()) {
                            final Project prj = FileOwnerQuery.getOwner(src);
                            if (prj != null) {
                                for (SourceGroup sg : ProjectUtils.getSources(prj).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)) {
                                    if (src.equals(sg.getRootFolder())) {
                                        if (!prj.equals(owner)) {
                                            projectRefs.add(prj);
                                        }
                                        continue nextCpE;
                                    }
                                }
                            }
                        }
                    }
                }

                if (alreadyProcessed == null) {
                    res.offer(url);
                } else {
                    final URI uri = url.toURI();
                    final Pair<URI,SourceLevelQuery.Profile> key = Pair.of(uri,requiredProfile);
                    Set<Project> projects = alreadyProcessed.get(key);
                    if (projects == null) {
                        projects = new HashSet<>();
                        alreadyProcessed.put(key, projects);
                        res.offer(url);
                    }
                    projects.add(owner);
                }
            } catch (URISyntaxException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return res;
    }

    private static FileObject[] violationsToFileObjects(
            @NonNull final Collection<? extends ProfileSupport.Violation> violations,
            @NullAllowed final Filter filter,
            @NullAllowed final Map<FileObject,Collection<ProfileSupport.Violation>> violationsByFiles) {
        final Collection<FileObject> fos = new HashSet<>(violations.size());
        for (ProfileSupport.Violation v : violations) {
            final URL fileURL = v.getFile();
            if (fileURL != null) {
                final FileObject fo = URLMapper.findFileObject(fileURL);
                if (shouldProcessViolationsInSource(fo, filter)) {
                    fos.add(fo);
                    if (violationsByFiles != null) {
                        Collection<ProfileSupport.Violation> violationsInFile = violationsByFiles.get(fo);
                        if (violationsInFile == null) {
                            violationsInFile = new ArrayList<>();
                            violationsByFiles.put(fo, violationsInFile);
                        }
                        violationsInFile.add(v);
                    }
                }
            }
        }
        return fos.toArray(new FileObject[fos.size()]);
    }

    private static boolean shouldProcessViolationsInSource(
            @NullAllowed final FileObject source,
            @NullAllowed final Filter filter) {
        return filter == null ?
                true :
                filter.accept(source);
    }

    private static final class ProfileProvider {

        private final SourceLevelQuery.Profile profile;

        ProfileProvider (@NonNull Context ctx) {
            final Preferences prefs = ctx.getSettings();
            final String profileName = prefs == null ?
                    null :
                    prefs.get(ProfilesCustomizerProvider.PROP_PROFILE_TO_CHECK, null);
            profile = profileName == null ?
                    null :
                    SourceLevelQuery.Profile.forName(profileName);
        }

        @NonNull
        public SourceLevelQuery.Profile findProfile (@NonNull final FileObject root) {
            return profile != null ?
                    profile :
                    SourceLevelQuery.getSourceLevel2(root).getProfile();
        }

    }

    //@ThreadSafe
    private static final class CollectorFactory implements ProfileSupport.ViolationCollectorFactory {

        private final ViolationsProvider provider;
        private final SourceLevelQuery.Profile profile;
        private final AtomicBoolean canceled;


        CollectorFactory(
                @NonNull final ViolationsProvider provider,
                @NonNull final SourceLevelQuery.Profile profile,
                @NonNull final AtomicBoolean canceled) {
            assert provider != null;
            assert profile != null;
            assert canceled != null;
            this.provider = provider;
            this.profile = profile;
            this.canceled = canceled;
        }

        @Override
        public ProfileSupport.ViolationCollector create(@NonNull final URL root) {
            try {
                return new Collector(root.toURI());
            } catch (URISyntaxException ex) {
                throw new IllegalArgumentException(ex);
            }
        }

        @Override
        public boolean isCancelled() {
            return canceled.get();
        }

        private void addViolations(
                @NonNull final URI root,
                @NonNull final Collection<? extends ProfileSupport.Violation> violations) {
            provider.allViolations.offer(Pair.<Pair<URI,SourceLevelQuery.Profile>,Collection<? extends ProfileSupport.Violation>>of(
                    Pair.of(root,profile),violations));
        }

        //@ThreadSafe
        static final  class ViolationsProvider {

            private final BlockingQueue<Pair<Pair<URI,SourceLevelQuery.Profile>,Collection<? extends ProfileSupport.Violation>>>
                    allViolations = new LinkedBlockingQueue<>();

            @CheckForNull
            Pair<Pair<URI,SourceLevelQuery.Profile>,Collection<? extends ProfileSupport.Violation>> poll(long timeOut) throws InterruptedException {
                return allViolations.poll(timeOut, TimeUnit.MILLISECONDS);
            }
        }

        private final class Collector implements ProfileSupport.ViolationCollector {

            private final URI root;

            Collector(@NonNull final URI root) {
                Parameters.notNull("root", root);   //NOI18N
                this.root = root;
            }

            private final Queue<ProfileSupport.Violation> violations = new ArrayDeque<>();

            @Override
            public void reportProfileViolation(@NonNull final ProfileSupport.Violation violation) {
                violations.offer(violation);
            }

            @Override
            public void finished() {
                addViolations(root, violations);
            }
        }
    }

    //@NonThreadSafe
    private static final class FindPosScanner extends ErrorAwareTreePathScanner<Void, Void> {

        private final FileObject target;
        private final Elements elements;
        private final TreeUtilities treeUtilities;
        private final Trees trees;
        private final Result errors;
        private final Map<String,ProfileSupport.Violation> violationsByBinNames =
                new HashMap<>();

        FindPosScanner(
                @NonNull final FileObject target,
                @NonNull final Trees trees,
                @NonNull final Elements elements,
                @NonNull final TreeUtilities treeUtilities,
                @NonNull final Collection<? extends ProfileSupport.Violation> violations,
                @NonNull final Result errors) {
            assert target != null;
            assert trees != null;
            assert elements != null;
            assert treeUtilities != null;
            assert violations != null;
            assert errors != null;
            this.target = target;
            this.trees = trees;
            this.elements = elements;
            this.treeUtilities = treeUtilities;
            this.errors = errors;
            for (ProfileSupport.Violation v : violations) {
                final ElementHandle<TypeElement> eh = v.getUsedType();
                if (eh != null) {
                    violationsByBinNames.put(eh.getBinaryName(), v);
                }
            }
        }

        @Override
        public Void visitIdentifier(IdentifierTree node, Void p) {
            handleIdentSelect();
            return super.visitIdentifier(node, p);
        }

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

        @NbBundle.Messages({
            "MSG_SourceFileHigherProfile={0} requires profile: {1}",
            "DESC_SourceFileHigherProfile=The {0} requires profile: {1}"
        })
        private void handleIdentSelect() {
            final TreePath tp = getCurrentPath();
            Element e = trees.getElement(tp);
            if (e != null) {
                final ElementKind ek = e.getKind();
                if (ek == ElementKind.OTHER ||
                    ek.isField() ||
                    ek == ElementKind.CONSTRUCTOR ||
                    ek == ElementKind.METHOD) {
                        e = e.getEnclosingElement();
                }
                if ((e.getKind().isClass() || e.getKind().isInterface()) && !treeUtilities.isSynthetic(tp)) {
                    final Name binName = elements.getBinaryName((TypeElement)e);
                    final ProfileSupport.Violation v = violationsByBinNames.get(binName.toString());
                    if (v != null) {
                        final SourcePositions sp = trees.getSourcePositions();
                        final int start = (int) sp.getStartPosition(tp.getCompilationUnit(), tp.getLeaf());
                        final int end = (int) sp.getEndPosition(tp.getCompilationUnit(), tp.getLeaf());
                        final SourceLevelQuery.Profile requiredProfile = v.getRequiredProfile();
                        assert requiredProfile != null;
                        errors.reportError(ErrorDescriptionFactory.createErrorDescription(
                            null,
                            Severity.ERROR,
                            Bundle.MSG_SourceFileHigherProfile(e.getSimpleName(), requiredProfile.getDisplayName()),
                            Bundle.DESC_SourceFileHigherProfile(((TypeElement)e).getQualifiedName(), requiredProfile.getDisplayName()),
                            ErrorDescriptionFactory.lazyListForFixes(Collections.<Fix>emptyList()),
                            target,
                            start,
                            end));
                    }
                }
            }
        }
    }

    @NbBundle.Messages({
        "NAME_JdkProfiles=JRE 8 Profiles Conformance"
    })
    @ServiceProvider(service=AnalyzerFactory.class)
    public static final class Factory extends Analyzer.AnalyzerFactory {

        public Factory() {
            super("jdk-profiles", Bundle.NAME_JdkProfiles(), ICON);
        }

        @Override
        public Analyzer createAnalyzer(
                @NonNull final Context context,
                @NonNull final Result result) {
            return new ProfilesAnalyzer(context, result);
        }

        @Override
        public Analyzer createAnalyzer(@NonNull final Context context) {
            throw new IllegalStateException();
        }

        @Override
        public Iterable<? extends WarningDescription> getWarnings() {
            return Collections.emptySet();
        }

        @CheckForNull
        @Override
        public CustomizerProvider<Void, ProfilesCustomizer> getCustomizerProvider() {
            return new ProfilesCustomizerProvider();
        }
    }

    private static interface Filter {

        public boolean accept(@NullAllowed FileObject fo);

        static final class Builder {

            private final Set<FileObject> folders;
            private final Set<FileObject> nonRecursiveFolders;
            private final Set<FileObject> files;

            Builder() {
                this.folders = new HashSet<>();
                this.nonRecursiveFolders = new HashSet<>();
                this.files = new HashSet<>();
            }

            @NonNull
            Builder addFolder(@NonNull final FileObject folder) {
                assert folder.isFolder();
                this.folders.add(folder);
                return this;
            }

            @NonNull
            Builder addNonRecursiveFolder(@NonNull final NonRecursiveFolder nonRecursiveFolder) {
                final FileObject folder = nonRecursiveFolder.getFolder();
                assert folder.isFolder();
                this.nonRecursiveFolders.add(folder);
                return this;
            }

            @NonNull
            Builder addFile(@NonNull final FileObject file) {
                assert file.isData();
                this.files.add(file);
                return this;
            }

            @NonNull
            public Filter build() {
                return new Filter() {
                    @Override
                    public boolean accept(@NullAllowed final FileObject fo) {
                        if (fo == null) {
                            return false;
                        }
                        if (files.contains(fo)) {
                            return true;
                        }
                        for (FileObject folder : nonRecursiveFolders) {
                            if (folder.equals(fo.getParent())) {
                                return true;
                            }
                        }
                        for (FileObject folder : folders) {
                            if (FileUtil.isParentOf(folder, fo)) {
                                return true;
                            }
                        }
                        return false;
                    }
                };
            }
        }
    }
}
