/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2013 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 * Contributor(s):
 *
 * Portions Copyrighted 2013 Sun Microsystems, Inc.
 */
package org.netbeans.modules.java.project.ui;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.logging.Logger;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
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.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.api.java.queries.SourceLevelQuery;
import org.netbeans.api.java.queries.SourceLevelQuery.Profile;
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.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.ant.AntArtifact;
import org.netbeans.api.project.libraries.Library;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.netbeans.spi.project.support.ant.ReferenceHelper;
import org.netbeans.spi.project.ui.ProjectProblemsProvider;
import org.netbeans.spi.project.ui.support.ProjectProblemsProviderSupport;
import org.openide.filesystems.FileUtil;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.ui.ProjectProblemResolver;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.MutexException;

import static org.netbeans.modules.java.project.ui.Bundle.*;
import org.netbeans.spi.java.classpath.ClassPathFactory;
import org.netbeans.spi.java.project.classpath.support.ProjectClassPathSupport;
import org.netbeans.spi.project.support.ant.PropertyUtils;
import org.openide.filesystems.FileObject;
import org.openide.util.ImageUtilities;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
/**
 *
 * @author Tomas Zezula
 */
final class ProfileProblemsProviderImpl implements ProjectProblemsProvider, PropertyChangeListener, ChangeListener, Runnable {

    private static final String LIB_PREFIX = "${libs."; // NOI18N
    private static final String PRJ_PREFIX = "${reference."; // NOI18N
    private static final String FILE_PREFIX = "${file.reference."; //NOI18N
    private static final String REF_PREFIX = "${"; //NOI18N
    private static final String VOL_CLASSPATH = "classpath";    //NOI18N
    private static final char PATH_SEPARATOR_CHAR = ':';                          //NOI18N
    private static final String ICON_LIBRARIES = "org/netbeans/modules/java/project/ui/resources/libraries.gif"; //NOI18N
    private static final String ICON_FILE = "org/netbeans/modules/java/project/ui/resources/jar.gif";//NOI18N
    private static final int SLIDING_DELAY = 1000;  //1s
    private static final Logger LOG = Logger.getLogger(ProfileProblemsProviderImpl.class.getName());
    private static final RequestProcessor RP = new RequestProcessor(ProjectProblemsProviders.class);

    private final AntProjectHelper antProjectHelper;
    private final ReferenceHelper referenceHelper;
    private final PropertyEvaluator evaluator;
    private final String profileProperty;
    private final Set<String> classPathProperties;
    private final ProjectProblemsProviderSupport problemsProviderSupport;
    //@GuardedBy("foreignSlResults")
    private final Collection<SourceLevelQuery.Result> foreignSlResults;
    private final RequestProcessor.Task firer;
    private final Object listenersInitLock = new Object();
    //@GuardedBy("listenersInitLock")
    private ClassPath classPath;
    //@GuardedBy("listenersInitLock")
    private SourceLevelQuery.Result slRes;

    ProfileProblemsProviderImpl(
            @NonNull final AntProjectHelper antProjectHelper,
            @NonNull final ReferenceHelper referenceHelper,
            @NonNull final PropertyEvaluator evaluator,
            @NonNull final String profileProperty,
            @NonNull final String... classPathProperties) {
        assert antProjectHelper != null;
        assert referenceHelper != null;
        assert evaluator != null;
        assert profileProperty != null;
        assert classPathProperties != null;
        this.antProjectHelper = antProjectHelper;
        this.referenceHelper = referenceHelper;
        this.evaluator = evaluator;
        this.profileProperty = profileProperty;
        this.classPathProperties = new HashSet<String>(
                Arrays.asList(classPathProperties));
        this.problemsProviderSupport = new ProjectProblemsProviderSupport(this);
        this.foreignSlResults = Collections.synchronizedCollection(new ArrayList<SourceLevelQuery.Result>());
        this.firer = RP.create(this);
    }

    @Override
    public void addPropertyChangeListener(@NonNull final PropertyChangeListener listener) {
        Parameters.notNull("listener", listener);   //NOI18N
        problemsProviderSupport.addPropertyChangeListener(listener);
    }

    @Override
    public void removePropertyChangeListener(@NonNull final PropertyChangeListener listener) {
        Parameters.notNull("listener", listener);   //NOI18N
        problemsProviderSupport.removePropertyChangeListener(listener);
    }

    @Override
    @NbBundle.Messages({
        "LBL_InvalidProfile=Invalid JRE 8 Profile",
        "DESC_InvalidProfile=The project profile ({0}) is lower than the profile of used libraries ({1}).",
        "DESC_IllegalProfile=The project libraries have illegal value of profile."
    })
    public Collection<? extends ProjectProblem> getProblems() {        
        return problemsProviderSupport.getProblems(new ProjectProblemsProviderSupport.ProblemsCollector() {
            @Override
            public Collection<? extends ProjectProblem> collectProblems() {
                return ProjectManager.mutex().readAccess(new Mutex.Action<Collection<? extends ProjectProblem>>() {
                    @Override
                    public Collection<? extends ProjectProblem> run() {
                        final SourceLevelQuery.Result mySL = listenenOnProjectMetadata();
                        final Profile profile = mySL.getProfile();
                        if (profile == Profile.DEFAULT) {
                            return Collections.<ProjectProblem>emptySet();
                        }
                        final Set<Reference> problems = collectReferencesWithWrongProfile(profile);
                        if (problems.isEmpty()) {
                            return Collections.<ProjectProblem>emptySet();
                        }
                        Profile minProfile = null;
                        for (Reference problem : problems) {
                            final Profile problemProfile = problem.getRequiredProfile();
                            minProfile = max(minProfile, problemProfile);
                        }
                        return Collections.<ProjectProblem>singleton(
                                ProjectProblem.createError(
                                LBL_InvalidProfile(),
                                minProfile != null
                                ? DESC_InvalidProfile(profile.getDisplayName(), minProfile.getDisplayName())
                                : DESC_IllegalProfile(),
                                new ProfileResolver(
                                    antProjectHelper,
                                    profileProperty,
                                    profile,
                                    problems)));
                    }
                });
            }
        });
    }

    @Override
    public void propertyChange(@NonNull final PropertyChangeEvent event) {
        final String propName = event.getPropertyName();
        if (ClassPath.PROP_ROOTS.equals(propName) ||
            JavaPlatformManager.PROP_INSTALLED_PLATFORMS.equals(propName)) {
            firer.schedule(SLIDING_DELAY);
        }
    }

    @Override
    public void stateChanged(ChangeEvent ce) {        
        firer.schedule(SLIDING_DELAY);
    }

    @Override
    public void run() {        
        problemsProviderSupport.fireProblemsChange();
    }

    private SourceLevelQuery.Result listenenOnProjectMetadata() {
        synchronized (listenersInitLock) {
            if (slRes == null) {
                assert classPath == null;
                final JavaPlatformManager jpm = JavaPlatformManager.getDefault();
                jpm.addPropertyChangeListener(WeakListeners.propertyChange(this, jpm));
                slRes = SourceLevelQuery.getSourceLevel2(antProjectHelper.getProjectDirectory());
                slRes.addChangeListener(this);
                final File baseFolder = FileUtil.toFile(antProjectHelper.getProjectDirectory());
                if (baseFolder != null) {
                    final ClassPath cp = ClassPathFactory.createClassPath(ProjectClassPathSupport.createPropertyBasedClassPathImplementation(
                        baseFolder,
                        evaluator,
                        classPathProperties.toArray(new String[classPathProperties.size()])));
                    cp.addPropertyChangeListener(this);
                    cp.getRoots();
                    classPath = cp;
                }
            }
            return slRes;
        }
    }

    @NonNull
    private Set<Reference> collectReferencesWithWrongProfile(@NonNull final Profile currentProfile) {
        assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
        final Set<Reference> res = new HashSet<Reference>();
        final EditableProperties projectProps = antProjectHelper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
        final EditableProperties privateProps = antProjectHelper.getProperties(AntProjectHelper.PRIVATE_PROPERTIES_PATH);
        final EditableProperties globalProps = PropertyUtils.getGlobalProperties();
        final Collection<SourceLevelQuery.Result> newSlResults = new ArrayDeque<SourceLevelQuery.Result>();
        for (String cpId : classPathProperties) {
            collectReferencesWithWrongProfileImpl(
                    cpId,
                    currentProfile,
                    antProjectHelper,
                    evaluator,
                    referenceHelper,
                    projectProps,
                    privateProps,
                    globalProps,
                    res,
                    newSlResults);
        }
        synchronized (foreignSlResults) {
            for (final Iterator<? extends SourceLevelQuery.Result> it = foreignSlResults.iterator(); it.hasNext();) {
                final SourceLevelQuery.Result cslr = it.next();
                it.remove();
                cslr.removeChangeListener(this);
            }
            assert  foreignSlResults.isEmpty();
            for (SourceLevelQuery.Result cslr : newSlResults) {
                cslr.addChangeListener(this);
                foreignSlResults.add(cslr);
            }
        }
        return res;
    }

    private static void collectReferencesWithWrongProfileImpl(
            @NonNull final String classPathId,
            @NonNull final Profile requiredProfile,
            @NonNull final AntProjectHelper antProjectHelper,
            @NonNull final PropertyEvaluator eval,
            @NonNull final ReferenceHelper refHelper,
            @NonNull final EditableProperties projectProps,
            @NonNull final EditableProperties privateProps,
            @NonNull final EditableProperties globalProps,
            @NonNull final Set<? super Reference> collector,
            @NonNull final Collection<? super SourceLevelQuery.Result> slResCollector) {
        String cp = projectProps.getProperty(classPathId);
        if (cp == null) {
            cp = privateProps.getProperty(classPathId);
        }
        if (cp == null) {
            cp = globalProps.getProperty(classPathId);
        }
        if (cp != null) {
            final Queue<String> todo = new ArrayDeque<String>(Arrays.asList(PropertyUtils.tokenizePath(cp)));
            while (!todo.isEmpty()) {
                final String rawEntry = todo.remove();
                final String propName = getAntPropertyName(rawEntry);
                if (propName == null) {
                    continue;
                }
                if (rawEntry.startsWith(LIB_PREFIX)) {
                    final String libName = rawEntry.substring(LIB_PREFIX.length(),rawEntry.lastIndexOf('.'));
                    final Library lib = refHelper.findLibrary(libName);
                    if (lib != null) {
                        final Collection<? extends ProfileSupport.Violation> res =
                            ProfileSupport.findProfileViolations(
                                requiredProfile,
                                Collections.<URL>emptySet(),
                                lib.getContent(VOL_CLASSPATH),
                                Collections.<URL>emptySet(),
                                EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST));
                            if (!res.isEmpty()) {
                                final Profile maxProfile = findMaxProfile(res);
                                collector.add(new LibraryReference(classPathId, rawEntry, maxProfile, lib));
                            }
                    }
                } else if (rawEntry.startsWith(PRJ_PREFIX)) {
                    final Object[] ref = refHelper.findArtifactAndLocation(rawEntry);
                    if (ref[0] != null) {
                        AntArtifact artifact = (AntArtifact)ref[0];
                        final SourceLevelQuery.Result slRes = SourceLevelQuery.getSourceLevel2(artifact.getProject().getProjectDirectory());
                        slResCollector.add(slRes);
                        final Profile minProfile = slRes.getProfile();
                        if (isBroken(requiredProfile, minProfile)) {
                            collector.add(new ProjectReference(classPathId, rawEntry, minProfile, artifact.getProject()));
                        }
                    }

                } else if (rawEntry.startsWith(FILE_PREFIX)) {
                    final String path = eval.getProperty(propName);
                    if (path != null) {
                        final File file = antProjectHelper.resolveFile(path);
                        final URL root = FileUtil.urlForArchiveOrDir(file);
                        if (root != null) {
                            final Collection<? extends ProfileSupport.Violation> res =
                                ProfileSupport.findProfileViolations(
                                    requiredProfile,
                                    Collections.<URL>emptySet(),
                                    Collections.singleton(root),
                                    Collections.<URL>emptySet(),
                                    EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST));
                            if (!res.isEmpty()) {
                                final Profile maxProfile = findMaxProfile(res);
                                collector.add(new FileReference(classPathId, rawEntry, maxProfile, file));
                            }
                        }
                    }
                } else if (rawEntry.startsWith(REF_PREFIX)) {
                    collectReferencesWithWrongProfileImpl(
                            propName,
                            requiredProfile,
                            antProjectHelper,
                            eval,
                            refHelper,
                            projectProps,
                            privateProps,
                            globalProps,
                            collector,
                            slResCollector);
                } else {
                    final File file = antProjectHelper.resolveFile(propName);
                    final URL root = FileUtil.urlForArchiveOrDir(file);
                    if (root != null) {
                        final Collection<? extends ProfileSupport.Violation> res =
                                ProfileSupport.findProfileViolations(
                                    requiredProfile,
                                    Collections.<URL>emptySet(),
                                    Collections.singleton(root),
                                    Collections.<URL>emptySet(),
                                    EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST));
                        if (!res.isEmpty()) {
                            final Profile maxProfile = findMaxProfile(res);
                            collector.add(new FileReference(classPathId, rawEntry, maxProfile, file));
                        }
                    }
                }
            }
        }
    }

    private static boolean isBroken(
            @NonNull final Profile requiredProfile,
            @NullAllowed final Profile profile) {
        if (profile == null) {
            return true;
        }
        final Profile max = max(requiredProfile, profile);
        return !max.equals(Profile.DEFAULT) &&
               !max.equals(requiredProfile);
    }

    @CheckForNull
    private static String getAntPropertyName(@NullAllowed String reference) {
        if (reference != null
                && reference.startsWith("${") // NOI18N
                && reference.endsWith("}")) { // NOI18N
            reference =  reference.substring(2, reference.length() - 1);
        }
        return reference;
    }

    @CheckForNull
    private static Profile findMaxProfile(@NonNull final Iterable<? extends ProfileSupport.Violation> violations) {
        Profile current = null;
        for (ProfileSupport.Violation violation : violations) {
            Profile p = violation.getRequiredProfile();
            if (p == null) {
                //Broken profile - no need to continue
                return null;
            }
            current = max(current, p);
        }
        return current;
    }
    
    @CheckForNull
    static Profile max (@NullAllowed Profile a, @NullAllowed Profile b) {
        if (b == null) {
            return a;
        }
        if (a == null) {
            return b;
        }
        return a.compareTo(b) <= 0 ?
            b :
            a;
    }

    @NonNull
    static Profile requiredProfile(
            @NonNull final Collection<? extends Reference> state,
            @NonNull Profile initial) {
        Profile current = initial;
        for (ProfileProblemsProviderImpl.Reference re : state) {
            current = max(current, re.getRequiredProfile());
        }
        return current;
    }
    
    abstract static class Reference {

        private final String classPathId;
        private final String rawId;
        private final Profile requiredProfile;

        private Reference(
                @NonNull final String classPathId,
                @NonNull final String rawId,
                @NullAllowed final Profile requiredProfile) {
            Parameters.notNull("classPathId", classPathId); //NOI18N
            Parameters.notNull("rawId", rawId);             //NOI18N
            this.classPathId = classPathId;
            this.rawId = rawId;
            this.requiredProfile = requiredProfile;
        }

        @NonNull
        abstract String getDisplayName();

        @NonNull
        abstract String getToolTipText();

        @NonNull
        abstract Icon getIcon();        

        @CheckForNull
        final Profile getRequiredProfile() {
            return requiredProfile;
        }

        private void remove(@NonNull final AntProjectHelper helper) {
            final EditableProperties props = helper.getProperties (AntProjectHelper.PROJECT_PROPERTIES_PATH);
            String rawPath = props.getProperty (classPathId);
            if (rawPath != null) {
                final String[] pathElements = PropertyUtils.tokenizePath(rawPath);
                final List<String> result = new ArrayList<String>(pathElements.length);
                boolean changed = false;
                for (String pathElement : pathElements) {
                    if (rawId.equals(pathElement)) {
                        changed = true;
                        continue;
                    }
                    result.add(pathElement + PATH_SEPARATOR_CHAR);
                }
                if (!result.isEmpty()) {
                    final String last = result.get(result.size()-1);
                    result.set(result.size()-1, last.substring(0, last.length()-1));
                }
                if (changed) {
                    props.setProperty(classPathId, result.toArray(new String[result.size()]));
                    helper.putProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH, props);
                }
            }
        }

        @Override
        public int hashCode() {
            int hash = 17;
            hash = 31 * hash + this.classPathId.hashCode();
            hash = 31 * hash + this.rawId.hashCode();
            return hash;
        }

        @Override
        public boolean equals(Object other) {
            if (other == this) {
               return true;
            }
            if (!(other instanceof Reference)) {
                return false;
            }
            final Reference otherRef = (Reference) other;
            return classPathId.equals(otherRef.classPathId) &&
                rawId.equals(otherRef.rawId);
        }


    }

    private static final class LibraryReference extends Reference {

        private final Library lib;

        private LibraryReference(
                @NonNull final String classPathId,
                @NonNull final String rawId,
                @NullAllowed final Profile requiredProfile,
                @NonNull final Library lib) {
            super(classPathId, rawId, requiredProfile);
            Parameters.notNull("lib", lib); //NOI18N
            this.lib = lib;
        }

        @Override
        String getDisplayName() {
            return this.lib.getDisplayName();
        }

        @Override
        String getToolTipText() {
            return lib.getDisplayName();
        }

        @Override
        Icon getIcon() {
            return ImageUtilities.loadImageIcon(ICON_LIBRARIES, false);
        }       
    }

    private static final class ProjectReference extends Reference {
        private final Project prj;

        private ProjectReference(
                @NonNull final String classPathId,
                @NonNull final String rawId,
                @NullAllowed final Profile requiredProfile,
                @NonNull final Project prj) {
            super(classPathId, rawId, requiredProfile);
            Parameters.notNull("prj", prj); //NOI18N
            this.prj = prj;
        }

        @Override
        String getDisplayName() {
            return ProjectUtils.getInformation(prj).getDisplayName();
        }

        @Override
        String getToolTipText() {
            return FileUtil.getFileDisplayName(prj.getProjectDirectory());
        }

        @Override
        Icon getIcon() {
            return ProjectUtils.getInformation(prj).getIcon();
        }
    }

    private static final class FileReference extends Reference {

        private final File file;

        private FileReference(
                @NonNull final String classPathId,
                @NonNull final String rawId,
                @NullAllowed final Profile requiredProfile,
                @NonNull final File file) {
            super(classPathId, rawId, requiredProfile);
            Parameters.notNull("file", file);   //NOI18N
            this.file = file;
        }

        @Override
        String getDisplayName() {
            return file.getName();
        }

        @Override
        String getToolTipText() {
            return file.getAbsolutePath();
        }

        @Override
        Icon getIcon() {
            return ImageUtilities.loadImageIcon(ICON_FILE, false);
        }        
    }

    private static final class ProfileResolver implements ProjectProblemResolver {

        private final AntProjectHelper antProjectHelper;
        private final String profileProperty;
        private final Profile currentProfile;
        private final Collection<? extends Reference> state;

        private ProfileResolver(
                @NonNull final AntProjectHelper antProjectHelper,
                @NonNull final String profileProperty,
                @NonNull final Profile currentProfile,
                @NonNull final Collection<? extends Reference> state) {
            assert antProjectHelper != null;
            assert profileProperty != null;
            assert currentProfile != null;
            assert state != null;
            this.antProjectHelper = antProjectHelper;
            this.profileProperty = profileProperty;
            this.currentProfile = currentProfile;
            this.state = state;
        }

        @Override
        @NbBundle.Messages({
            "LBL_ResolveProfile=Resolve Invalid Project Profile",
            "LBL_ResolveButton=OK",
            "AN_ResolveButton=Resolve",
            "AD_ResolveButton=Resolve the profile problems"
        })
        public Future<Result> resolve() {
            final JButton ok = new JButton(LBL_ResolveButton());
            ok.getAccessibleContext().setAccessibleName(AN_ResolveButton());
            ok.getAccessibleContext().setAccessibleDescription(AD_ResolveButton());
            final FixProfile panel = new FixProfile(ok, currentProfile, state);
            final DialogDescriptor dd = new DialogDescriptor(
                    panel,
                    LBL_ResolveProfile(),
                    true,
                    new Object[]{
                ok,
                DialogDescriptor.CANCEL_OPTION
            },
                    ok,
                    DialogDescriptor.DEFAULT_ALIGN,
                    null,
                    null);
            if (DialogDisplayer.getDefault().notify(dd) == ok) {
                return RP.submit(new Callable<ProjectProblemsProvider.Result>() {
                    @Override
                    public ProjectProblemsProvider.Result call() throws Exception {
                        ProjectProblemsProvider.Status status = ProjectProblemsProvider.Status.UNRESOLVED;
                        try {
                            ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
                                @Override
                                public Void run() throws IOException {
                                    final boolean shouldUpdate = panel.shouldUpdateProfile();
                                    if (shouldUpdate) {
                                        final Profile newProfile = panel.getProfile();
                                        final EditableProperties props = antProjectHelper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
                                        if (newProfile == null ||
                                            newProfile == Profile.DEFAULT) {
                                            props.remove(profileProperty);
                                        } else {
                                            props.put(profileProperty, newProfile.getName());
                                        }
                                        antProjectHelper.putProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH, props);
                                    }
                                    for (Reference rootToRemove : panel.getRootsToRemove()) {
                                        rootToRemove.remove(antProjectHelper);
                                    }
                                    ProjectManager.getDefault().saveProject(
                                            FileOwnerQuery.getOwner(antProjectHelper.getProjectDirectory()));
                                    return null;
                                }
                            });
                            status = ProjectProblemsProvider.Status.RESOLVED;
                        } catch (MutexException e) {
                            Exceptions.printStackTrace(e);
                        }
                        return ProjectProblemsProvider.Result.create(status);
                    }
                });
            }
            final RunnableFuture<Result> res = new FutureTask<Result>(
                    new Callable<Result>() {
                        @Override
                        public Result call() {
                            return ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.UNRESOLVED);
                        }
                    });
            res.run();
            return res;
        }

        @Override
        public int hashCode() {
            int res = 17;
            final FileObject projDir = antProjectHelper.getProjectDirectory();
            res = res * 31 + (projDir == null ? 0 : projDir.toURI().hashCode());
            res = res * 31 + currentProfile.hashCode();
            return res;
        }

        @Override
        public boolean equals(@NullAllowed Object other) {
            if (other == this) {
                return true;
            }
            if (!(other instanceof ProfileResolver)) {
                return false;
            }
            final ProfileResolver otherResolver = (ProfileResolver) other;
            final FileObject projDir = antProjectHelper.getProjectDirectory();
            final FileObject otherProjDir = otherResolver.antProjectHelper.getProjectDirectory();
            return
                currentProfile.equals(otherResolver.currentProfile) &&
                (projDir == null ? otherProjDir == null : projDir.equals(otherProjDir));
        }
    }    
}