/*
 *  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
 *
 *      https://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.apache.ivy.core.resolve;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.LogOptions;
import org.apache.ivy.core.cache.ArtifactOrigin;
import org.apache.ivy.core.cache.ResolutionCacheManager;
import org.apache.ivy.core.event.EventManager;
import org.apache.ivy.core.event.download.PrepareDownloadEvent;
import org.apache.ivy.core.event.resolve.EndResolveEvent;
import org.apache.ivy.core.event.resolve.StartResolveEvent;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.report.ArtifactDownloadReport;
import org.apache.ivy.core.report.ConfigurationResolveReport;
import org.apache.ivy.core.report.DownloadReport;
import org.apache.ivy.core.report.DownloadStatus;
import org.apache.ivy.core.report.ResolveReport;
import org.apache.ivy.core.resolve.IvyNodeEviction.EvictionData;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.core.sort.SortEngine;
import org.apache.ivy.core.sort.SortOptions;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.ModuleDescriptorParserRegistry;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.plugins.resolver.DependencyResolver;
import org.apache.ivy.plugins.version.VersionMatcher;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.filter.Filter;

/**
 * The resolve engine which is the core of the dependency resolution mechanism used in Ivy. It
 * features several resolve methods, some very simple, like {@link #resolve(File)} and
 * {@link #resolve(URL)} which allow to simply resolve dependencies of a single module descriptor,
 * or more complete one, like the {@link #resolve(ModuleDescriptor, ResolveOptions)} which allows to
 * provide options to the resolution engine.
 *
 * @see ResolveOptions
 */
public class ResolveEngine {
    private ResolveEngineSettings settings;

    private EventManager eventManager;

    private SortEngine sortEngine;

    private DependencyResolver dictatorResolver;

    /**
     * Constructs a ResolveEngine.
     *
     * @param settings
     *            the settings to use to configure the engine. Must not be null.
     * @param eventManager
     *            the event manager to use to send events about the resolution process. Must not be
     *            null.
     * @param sortEngine
     *            the sort engine to use to sort modules before producing the dependency resolution
     *            report. Must not be null.
     */
    public ResolveEngine(ResolveEngineSettings settings, EventManager eventManager,
            SortEngine sortEngine) {
        this.settings = settings;
        this.eventManager = eventManager;
        this.sortEngine = sortEngine;
    }

    /**
     * Returns the currently configured dictator resolver, which when non null is used in place of
     * any specified resolver in the {@link IvySettings}
     *
     * @return the currently configured dictator resolver, may be null.
     */
    public DependencyResolver getDictatorResolver() {
        return dictatorResolver;
    }

    /**
     * Sets a dictator resolver, which is used in place of regular dependency resolver for
     * subsequent dependency resolution by this engine.
     *
     * @param dictatorResolver
     *            the dictator resolver to use in this engine, null if regular settings should used
     */
    public void setDictatorResolver(DependencyResolver dictatorResolver) {
        this.dictatorResolver = dictatorResolver;
        settings.setDictatorResolver(dictatorResolver);
    }

    public ResolveReport resolve(File ivySource) throws ParseException, IOException {
        return resolve(ivySource.toURI().toURL());
    }

    public ResolveReport resolve(URL ivySource) throws ParseException, IOException {
        return resolve(ivySource, new ResolveOptions());
    }

    /**
     * Resolves the module identified by the given mrid with its dependencies if transitive is set
     * to true.
     *
     * @param mrid ModuleRevisionId
     * @param options ResolveOptions
     * @param changing boolean
     * @return ResolveReport
     * @throws ParseException if something goes wrong
     * @throws IOException if something goes wrong
     */
    public ResolveReport resolve(final ModuleRevisionId mrid, ResolveOptions options,
            boolean changing) throws ParseException, IOException {
        DefaultModuleDescriptor md;
        ResolveOptions optionsToUse = new ResolveOptions(options);

        if (options.useSpecialConfs()) {
            // create new resolve options because this is a different resolve than the real resolve
            // (which will be a resolve of a newCallerInstance module)
            ResolvedModuleRevision rmr = findModule(mrid, new ResolveOptions(options));
            if (rmr == null) {
                Message.verbose("module not found " + mrid);

                // we will continue the resolve anyway to get a nice error message back
                // to the user, however reduce the amount of logging in this case
                optionsToUse.setLog(LogOptions.LOG_DOWNLOAD_ONLY);
                md = DefaultModuleDescriptor.newCallerInstance(mrid, new String[] {"default"},
                    options.isTransitive(), changing);
            } else {
                String[] confs = options.getConfs(rmr.getDescriptor());
                md = DefaultModuleDescriptor.newCallerInstance(
                    ModuleRevisionId.newInstance(mrid, rmr.getId().getRevision()), confs,
                    options.isTransitive(), changing);
            }
        } else {
            md = DefaultModuleDescriptor.newCallerInstance(mrid, options.getConfs(),
                options.isTransitive(), changing);
        }

        return resolve(md, optionsToUse);
    }

    /**
     * Resolve dependencies of a module described by an ivy file.
     *
     * @param ivySource URL
     * @param options ResolveOptions
     * @return ResolveReport
     * @throws ParseException if something goes wrong
     * @throws IOException if something goes wrong
     */
    public ResolveReport resolve(URL ivySource, ResolveOptions options) throws ParseException,
            IOException {
        URLResource res = new URLResource(ivySource);
        ModuleDescriptorParser parser = ModuleDescriptorParserRegistry.getInstance().getParser(res);
        Message.verbose("using " + parser + " to parse " + ivySource);
        ModuleDescriptor md = parser.parseDescriptor(settings, ivySource, options.isValidate());
        String revision = options.getRevision();
        if (revision == null && md.getResolvedModuleRevisionId().getRevision() == null) {
            revision = Ivy.getWorkingRevision();
        }
        if (revision != null) {
            md.setResolvedModuleRevisionId(ModuleRevisionId.newInstance(md.getModuleRevisionId(),
                revision));
        }

        return resolve(md, options);
    }

    /**
     * Resolve dependencies of a module described by a module descriptor.
     * @param md ModuleDescriptor
     * @param options ResolveOptions
     * @return ResolveReport
     * @throws ParseException if something goes wrong
     * @throws IOException if something goes wrong
     */
    public ResolveReport resolve(ModuleDescriptor md, ResolveOptions options)
            throws ParseException, IOException {
        DependencyResolver oldDictator = getDictatorResolver();
        IvyContext context = IvyContext.getContext();
        try {
            String[] confs = options.getConfs(md);
            options.setConfs(confs);

            if (options.getResolveId() == null) {
                options.setResolveId(ResolveOptions.getDefaultResolveId(md));
            }

            eventManager.fireIvyEvent(new StartResolveEvent(md, confs));

            long start = System.currentTimeMillis();
            if (ResolveOptions.LOG_DEFAULT.equals(options.getLog())) {
                Message.info(":: resolving dependencies :: " + md.getResolvedModuleRevisionId()
                        + (options.isTransitive() ? "" : " [not transitive]"));
                Message.info("\tconfs: " + Arrays.asList(confs));
            } else {
                Message.verbose(":: resolving dependencies :: " + md.getResolvedModuleRevisionId()
                        + (options.isTransitive() ? "" : " [not transitive]"));
                Message.verbose("\tconfs: " + Arrays.asList(confs));
            }
            Message.verbose("\tvalidate = " + options.isValidate());
            Message.verbose("\trefresh = " + options.isRefresh());

            ResolveReport report = new ResolveReport(md, options.getResolveId());

            ResolveData data = new ResolveData(this, options);
            context.setResolveData(data);

            // resolve dependencies
            IvyNode[] dependencies = getDependencies(md, options, report);
            report.setDependencies(Arrays.asList(dependencies), options.getArtifactFilter());

            if (options.getCheckIfChanged()) {
                report.checkIfChanged();
            }

            // produce resolved ivy file and ivy properties in cache
            ResolutionCacheManager cacheManager = settings.getResolutionCacheManager();
            cacheManager.saveResolvedModuleDescriptor(md);

            // we store the resolved dependencies revisions and statuses per asked dependency
            // revision id, for direct dependencies only.
            // this is used by the deliver task to resolve dynamic revisions to static ones
            File ivyPropertiesInCache = cacheManager.getResolvedIvyPropertiesInCache(md
                    .getResolvedModuleRevisionId());
            Properties props = new Properties();
            if (dependencies.length > 0) {
                Map<ModuleId, ModuleRevisionId> forcedRevisions = new HashMap<>();
                for (IvyNode dependency : dependencies) {
                    if (dependency.getModuleRevision() != null
                            && dependency.getModuleRevision().isForce()) {
                        forcedRevisions.put(dependency.getModuleId(),
                                dependency.getResolvedId());
                    }
                }

                IvyNode root = dependencies[0].getRoot();

                Map<ModuleId, IvyNode> topLevelDeps = new HashMap<>();
                for (IvyNode dependency : dependencies) {
                    if (!dependency.hasProblem()) {
                        DependencyDescriptor dd = dependency.getDependencyDescriptor(root);
                        if (dd != null) {
                            ModuleId orgMod = dependency.getModuleId();
                            topLevelDeps.put(orgMod, dependency);
                        }
                    }
                }

                for (IvyNode dependency : dependencies) {
                    if (!dependency.hasProblem() && !dependency.isCompletelyEvicted()) {
                        DependencyDescriptor dd = dependency.getDependencyDescriptor(root);
                        if (dd == null) {
                            ModuleId mid = dependency.getModuleId();
                            IvyNode tlDep = topLevelDeps.get(mid);
                            if (tlDep != null) {
                                dd = tlDep.getDependencyDescriptor(root);
                            }
                        }
                        if (dd != null) {
                            ModuleRevisionId depResolvedId = dependency.getResolvedId();
                            ModuleDescriptor depDescriptor = dependency.getDescriptor();
                            ModuleRevisionId depRevisionId = dd.getDependencyRevisionId();
                            ModuleRevisionId forcedRevisionId = forcedRevisions.get(dependency
                                    .getModuleId());

                            if (dependency.getModuleRevision() != null
                                    && dependency.getModuleRevision().isForce()
                                    && !depResolvedId.equals(depRevisionId)
                                    && !settings.getVersionMatcher().isDynamic(depRevisionId)) {
                                // if we were forced to this revision and we
                                // are not a dynamic revision, reset to the
                                // asked revision
                                depResolvedId = depRevisionId;
                                depDescriptor = null;
                            }

                            if (depResolvedId == null) {
                                throw new NullPointerException("getResolvedId() is null for "
                                        + dependency.toString());
                            }
                            if (depRevisionId == null) {
                                throw new NullPointerException("getDependencyRevisionId() "
                                        + "is null for " + dd.toString());
                            }
                            String rev = depResolvedId.getRevision();
                            String forcedRev = forcedRevisionId == null ? rev : forcedRevisionId
                                    .getRevision();

                            // The evicted modules have no description, so we can't put the status
                            String status = depDescriptor == null ? "?" : depDescriptor.getStatus();
                            Message.debug("storing dependency " + depResolvedId + " in props");
                            props.put(depRevisionId.encodeToString(), rev + " " + status + " "
                                    + forcedRev + " " + depResolvedId.getBranch());
                        }
                    }
                }
            }
            FileOutputStream out = new FileOutputStream(ivyPropertiesInCache);
            props.store(out, md.getResolvedModuleRevisionId() + " resolved revisions");
            out.close();
            Message.verbose("\tresolved ivy file produced in cache");

            report.setResolveTime(System.currentTimeMillis() - start);

            if (options.isDownload()) {
                Message.verbose(":: downloading artifacts ::");

                DownloadOptions downloadOptions = new DownloadOptions();
                downloadOptions.setLog(options.getLog());
                downloadArtifacts(report, options.getArtifactFilter(), downloadOptions);
            }

            if (options.isOutputReport()) {
                outputReport(report, cacheManager, options);
            }

            Message.verbose("\tresolve done (" + report.getResolveTime() + "ms resolve - "
                    + report.getDownloadTime() + "ms download)");
            Message.sumupProblems();

            eventManager.fireIvyEvent(new EndResolveEvent(md, confs, report));
            return report;
        } catch (RuntimeException ex) {
            Message.debug(ex);
            Message.error(ex.getMessage());
            Message.sumupProblems();
            throw ex;
        } finally {
            context.setResolveData(null);
            setDictatorResolver(oldDictator);
        }
    }

    public void outputReport(ResolveReport report, ResolutionCacheManager cacheMgr,
            ResolveOptions options) throws IOException {
        if (ResolveOptions.LOG_DEFAULT.equals(options.getLog())) {
            Message.info(":: resolution report :: resolve " + report.getResolveTime() + "ms"
                    + " :: artifacts dl " + report.getDownloadTime() + "ms");
        } else {
            Message.verbose(":: resolution report :: resolve " + report.getResolveTime() + "ms"
                    + " :: artifacts dl " + report.getDownloadTime() + "ms");
        }
        report.setProblemMessages(Message.getProblems());
        // output report
        report.output(settings.getReportOutputters(), cacheMgr, options);
    }

    public void downloadArtifacts(ResolveReport report, Filter<Artifact> artifactFilter,
            DownloadOptions options) {
        long start = System.currentTimeMillis();

        eventManager.fireIvyEvent(new PrepareDownloadEvent(report.getArtifacts().toArray(
            new Artifact[report.getArtifacts().size()])));

        long totalSize = 0;
        for (IvyNode dependency : report.getDependencies()) {
            checkInterrupted();
            // download artifacts required in all asked configurations
            if (!dependency.isCompletelyEvicted() && !dependency.hasProblem()
                    && dependency.getModuleRevision() != null) {
                DependencyResolver resolver = dependency.getModuleRevision()
                        .getArtifactResolver();
                Artifact[] selectedArtifacts = dependency.getSelectedArtifacts(artifactFilter);
                DownloadReport dReport = resolver.download(selectedArtifacts, options);
                for (ArtifactDownloadReport adr : dReport.getArtifactsReports()) {
                    if (adr.getDownloadStatus() == DownloadStatus.FAILED) {
                        if (adr.getArtifact().getExtraAttribute("ivy:merged") != null) {
                            Message.warn("\tmerged artifact not found: " + adr.getArtifact()
                                    + ". It was required in "
                                    + adr.getArtifact().getExtraAttribute("ivy:merged"));
                        } else {
                            Message.warn("\t" + adr);
                            resolver.reportFailure(adr.getArtifact());
                        }
                    } else if (adr.getDownloadStatus() == DownloadStatus.SUCCESSFUL) {
                        totalSize += adr.getSize();
                    }
                }
                // update concerned reports
                for (String dconf : dependency.getRootModuleConfigurations()) {
                    // the report itself is responsible to take into account only
                    // artifacts required in its corresponding configuration
                    // (as described by the Dependency object)
                    if (dependency.isEvicted(dconf)
                            || dependency.isBlacklisted(dconf)) {
                        report.getConfigurationReport(dconf).addDependency(dependency);
                    } else {
                        report.getConfigurationReport(dconf).addDependency(dependency,
                                dReport);
                    }
                }
            }
        }
        report.setDownloadTime(System.currentTimeMillis() - start);
        report.setDownloadSize(totalSize);
    }

    /**
     * Download an artifact to the cache. Not used internally, useful especially for IDE plugins
     * needing to download artifact one by one (for source or javadoc artifact, for instance).
     * <p>
     * Downloaded artifact file can be accessed using {@link ArtifactDownloadReport#getLocalFile()}.
     * </p>
     * <p>
     * It is possible to track the progression of the download using classical ivy progress
     * monitoring feature (see addTransferListener).
     * </p>
     *
     * @param artifact
     *            the artifact to download
     * @param options DownloadOptions
     * @return a report concerning the download
     * @see #download(ArtifactOrigin, DownloadOptions)
     */
    public ArtifactDownloadReport download(Artifact artifact, DownloadOptions options) {
        DependencyResolver resolver = settings.getResolver(artifact.getModuleRevisionId());
        DownloadReport r = resolver.download(new Artifact[] {artifact}, options);
        return r.getArtifactReport(artifact);
    }

    /**
     * Locates an artifact in dependency resolvers, and return its location if it can be located and
     * actually exists, or an unknown {@link ArtifactOrigin} in other cases.
     *
     * @param artifact
     *            the artifact to locate.
     * @return the artifact location, should be tested with
     *         {@link ArtifactOrigin#isUnknown(ArtifactOrigin)} to check if the artifact has
     *         actually been located.
     */
    public ArtifactOrigin locate(Artifact artifact) {
        DependencyResolver resolver = settings.getResolver(artifact.getModuleRevisionId());
        return resolver.locate(artifact);
    }

    /**
     * Materialize an artifact already located.
     * <p>
     * Not used internally, useful especially for IDE plugins needing to download artifact one by
     * one (for source or javadoc artifact, for instance).
     * </p>
     * <p>
     * Materialized artifact file can be accessed using
     * {@link ArtifactDownloadReport#getLocalFile()}.
     * </p>
     * <p>
     * It is possible to track the progression of the download using classical ivy progress
     * monitoring feature (see addTransferListener).
     * </p>
     *
     * @param origin
     *            the artifact origin to materialize
     * @param options DownloadOptions
     * @return a report concerning the download
     * @see #download(Artifact, DownloadOptions)
     * @see #locate(Artifact)
     */
    public ArtifactDownloadReport download(ArtifactOrigin origin, DownloadOptions options) {
        DependencyResolver resolver = settings.getResolver(origin.getArtifact()
                .getModuleRevisionId());
        return resolver.download(origin, options);
    }

    /**
     * Resolve the dependencies of a module without downloading corresponding artifacts. The module
     * to resolve is given by its ivy file URL. This method requires appropriate configuration of
     * the ivy instance, especially resolvers.
     *
     * @param ivySource
     *            url of the ivy file to use for dependency resolving
     * @param options
     *            ditto
     * @return an array of the resolved dependencies
     * @throws ParseException
     *             if a parsing problem occurred in the ivy file
     * @throws IOException
     *             if an IO problem was raised during ivy file parsing
     */
    public IvyNode[] getDependencies(URL ivySource, ResolveOptions options) throws ParseException,
            IOException {
        return getDependencies(
            ModuleDescriptorParserRegistry.getInstance().parseDescriptor(settings, ivySource,
                options.isValidate()), options, null);
    }

    /**
     * Resolve the dependencies of a module without downloading corresponding artifacts. The module
     * to resolve is given by its module descriptor. This method requires appropriate configuration
     * of the ivy instance, especially resolvers.
     * <p>
     * The <code>IvyNode</code>s are ordered from the most dependent to the less dependent, so that
     * an IvyNode is always found in the list after all IvyNode depending directly on it.
     *
     * @param md
     *            the descriptor of the module for which we want to get dependencies - must not be
     *            null
     * @param options
     *            the resolve options to use to resolve the dependencies
     * @param report
     *            a resolve report to fill during resolution - may be null
     * @return an array of the resolved Dependencies
     */
    public IvyNode[] getDependencies(ModuleDescriptor md, ResolveOptions options,
            ResolveReport report) {
        // check parameters
        if (md == null) {
            throw new NullPointerException("module descriptor must not be null");
        }
        String[] confs = options.getConfs(md);
        Collection<String> missingConfs = new ArrayList<>();
        for (String conf : confs) {
            if (conf == null) {
                throw new NullPointerException("null conf not allowed: confs where: "
                        + Arrays.asList(confs));
            }

            if (md.getConfiguration(conf) == null) {
                missingConfs.add(" '" + conf + "' ");
            }
        }
        if (!missingConfs.isEmpty()) {
            throw new IllegalArgumentException("requested configuration"
                    + (missingConfs.size() > 1 ? "s" : "") + " not found in "
                    + md.getModuleRevisionId() + ": " + missingConfs);
        }

        IvyContext context = IvyContext.pushNewCopyContext();
        try {
            options.setConfs(confs);

            Date reportDate = new Date();
            ResolveData data = context.getResolveData();
            if (data == null) {
                data = new ResolveData(this, options);
                context.setResolveData(data);
            }
            IvyNode rootNode = new IvyNode(data, md);

            for (String conf : confs) {
                Message.verbose("resolving dependencies for configuration '" + conf + "'");

                ConfigurationResolveReport confReport = null;
                if (report != null) {
                    confReport = report.getConfigurationReport(conf);
                    if (confReport == null) {
                        confReport = new ConfigurationResolveReport(this, md, conf, reportDate,
                                options);
                        report.addReport(conf, confReport);
                    }
                }
                // we reuse the same resolve data with a new report for each conf
                data.setReport(confReport);

                // update the root module conf we are about to fetch
                VisitNode root = new VisitNode(data, rootNode, null, conf, null);
                root.setRequestedConf(conf);
                rootNode.updateConfsToFetch(Collections.singleton(conf));

                // go fetch !
                boolean fetched = false;
                while (!fetched) {
                    try {
                        fetchDependencies(root, conf, new HashSet<String>(), false);
                        fetched = true;
                    } catch (RestartResolveProcess restart) {
                        Message.verbose("====================================================");
                        Message.verbose("=           RESTARTING RESOLVE PROCESS");
                        Message.verbose("= " + restart.getMessage());
                        Message.verbose("====================================================");
                    }
                }

                // clean data
                for (IvyNode dep : data.getNodes()) {
                    dep.clean();
                }
            }

            // prune and reverse sort fetched dependencies
            Collection<IvyNode> nodes = data.getNodes();
            // use a Set to avoid duplicates, linked to preserve order
            Collection<IvyNode> dependencies = new LinkedHashSet<>(nodes.size());
            for (IvyNode node : nodes) {
                if (node != null && !node.isRoot() && !node.isCompletelyBlacklisted()) {
                    dependencies.add(node);
                }
            }
            List<IvyNode> sortedDependencies = sortEngine.sortNodes(dependencies,
                SortOptions.SILENT);
            Collections.reverse(sortedDependencies);

            handleTransitiveEviction(md, confs, data, sortedDependencies);

            return dependencies.toArray(new IvyNode[dependencies.size()]);
        } finally {
            IvyContext.popContext();
        }
    }

    private void handleTransitiveEviction(ModuleDescriptor md, String[] confs, ResolveData data,
                                          List<IvyNode> sortedDependencies) {
        // handle transitive eviction now:
        // if a module has been evicted then all its dependencies required only by it should be
        // evicted too. Since nodes are now sorted from the more dependent to the less one, we
        // can traverse the list and check only the direct parent and not all the ancestors
        for (IvyNode node : sortedDependencies) {
            if (!node.isCompletelyEvicted()) {
                for (String conf : confs) {
                    IvyNodeCallers.Caller[] callers = node.getCallers(conf);
                    if (settings.debugConflictResolution()) {
                        Message.debug("checking if " + node.getId()
                                + " is transitively evicted in " + conf);
                    }
                    boolean allEvicted = callers.length > 0;
                    for (IvyNodeCallers.Caller caller : callers) {
                        if (caller.getModuleRevisionId().equals(md.getModuleRevisionId())) {
                            // the caller is the root module itself, it can't be evicted
                            allEvicted = false;
                            break;
                        } else {
                            IvyNode callerNode = data.getNode(caller.getModuleRevisionId());
                            if (callerNode == null) {
                                Message.warn("ivy internal error: no node found for "
                                        + caller.getModuleRevisionId() + ": looked in "
                                        + data.getNodeIds() + " and root module id was "
                                        + md.getModuleRevisionId());
                            } else if (!callerNode.isEvicted(conf)) {
                                allEvicted = false;
                                break;
                            } else {
                                if (settings.debugConflictResolution()) {
                                    Message.debug("caller " + callerNode.getId() + " of "
                                            + node.getId() + " is evicted");
                                }
                            }
                        }
                    }
                    if (allEvicted) {
                        Message.verbose("all callers are evicted for " + node + ": evicting too");
                        node.markEvicted(conf, null, null, null);
                    } else {
                        if (settings.debugConflictResolution()) {
                            Message.debug(node.getId()
                                    + " isn't transitively evicted, at least one caller was"
                                    + " not evicted");
                        }
                    }
                }
            }
        }
    }

    private void fetchDependencies(VisitNode node, String conf, Set<String> fetchedSet, boolean shouldBePublic) {
        checkInterrupted();
        long start = System.currentTimeMillis();
        if (node.getParent() != null) {
            Message.verbose("== resolving dependencies " + node.getParent().getId() + "->"
                    + node.getId() + " [" + node.getParentConf() + "->" + conf + "]");
        } else {
            Message.verbose("== resolving dependencies for " + node.getId() + " [" + conf + "]");
        }
        ResolveData data = node.getNode().getData();
        VisitNode parentVisitNode = data.getCurrentVisitNode();

        data.setCurrentVisitNode(node);
        DependencyDescriptor dd = node.getDependencyDescriptor();
        VersionMatcher versionMatcher = node.getNode().getData().getSettings().getVersionMatcher();
        if (dd != null && !(node.getRoot() == node.getParent()
                && versionMatcher.isDynamic(dd.getDependencyRevisionId()))) {
            /*
             * we don't resolve conflicts before loading data for direct dependencies on dynamic
             * revisions, so that direct dynamic revisions are always resolved, which is mandatory
             * for proper replacement of dynamic revisions during 'deliver'
             */
            resolveConflict(node, conf);
        }

        if (node.loadData(conf, shouldBePublic)) {
            // we resolve conflict again now that we have all information loaded
            // indeed in some cases conflict manager need more information than just asked
            // dependency to take the decision
            resolveConflict(node, conf);
            if (!node.isEvicted() && !node.isCircular()) {
                for (String rconf : node.getRealConfs(conf)) {
                    doFetchDependencies(node, rconf, fetchedSet);
                }
            }
        } else if (!node.hasProblem()) {
            // the node has not been loaded but hasn't problem: it was already loaded
            // => we just have to update its dependencies data
            if (!node.isEvicted() && !node.isCircular()) {
                for (String rconf : node.getRealConfs(conf)) {
                    doFetchDependencies(node, rconf, fetchedSet);
                }
            }
        }
        if (node.isEvicted()) {
            // update selected nodes with confs asked in evicted one
            EvictionData ed = node.getEvictedData();
            if (ed.getSelected() != null) {
                for (IvyNode selected : ed.getSelected()) {
                    if (!selected.isLoaded()) {
                        // the node is not yet loaded, we can simply update its set of
                        // configurations to fetch
                        selected.updateConfsToFetch(Collections.singleton(conf));
                    } else {
                        // the node has already been loaded, we must fetch its dependencies in the
                        // required conf
                        fetchDependencies(node.gotoNode(selected), conf, fetchedSet, true);
                    }
                }
            }
        }
        if (settings.debugConflictResolution()) {
            Message.debug(node.getId() + " => dependencies resolved in " + conf + " ("
                    + (System.currentTimeMillis() - start) + "ms)");
        }
        data.setCurrentVisitNode(parentVisitNode);
    }

    private void doFetchDependencies(VisitNode node, String conf, Set<String> fetchedSet) {
        Configuration c = node.getConfiguration(conf);
        if (c == null) {
            if (!node.isConfRequiredByMergedUsageOnly(conf)) {
                Message.warn("configuration not found '" + conf + "' in " + node.getResolvedId()
                        + ": ignoring");
                if (node.getParent() != null) {
                    Message.warn("it was required from " + node.getParent().getResolvedId());
                }
            }
            return;
        }
        // we handle the case where the asked configuration extends others:
        // we have to first fetch the extended configurations

        // first we check if this is the actual requested conf (not an extended one)
        boolean requestedConfSet = false;
        if (node.getRequestedConf() == null) {
            node.setRequestedConf(conf);
            requestedConfSet = true;
        }
        // now let's recurse in extended confs
        String[] extendedConfs = c.getExtends();
        if (extendedConfs.length > 0) {
            node.updateConfsToFetch(Arrays.asList(extendedConfs));
        }
        for (String extendedConf : extendedConfs) {
            fetchDependencies(node, extendedConf, fetchedSet, false);
        }

        // now we can actually resolve this configuration dependencies
        if (!isDependenciesFetched(node.getNode(), conf, fetchedSet) && node.isTransitive()) {
            for (VisitNode dep : node.getDependencies(conf)) {
                dep.useRealNode(); // the node may have been resolved to another real one while
                // resolving other deps
                for (String rconf : dep.getRequiredConfigurations(node, conf)) {
                    fetchDependencies(dep, rconf, fetchedSet, true);
                }
                if (!dep.isEvicted() && !dep.hasProblem()) {
                    // if there are still confs to fetch (usually because they have
                    // been updated when evicting another module), we fetch them now
                    for (String fconf : dep.getConfsToFetch()) {
                        // shouldBeFixed=false to because some of those dependencies might
                        // be private when they were actually extending public conf.
                        // Should we keep two list of confs to fetch (private&public)?
                        // I don't think, visibility is already checked, and a change in the
                        // configuration between version might anyway have worse problems.
                        fetchDependencies(dep, fconf, fetchedSet, false);
                    }
                }
            }
            markDependenciesFetched(node.getNode(), conf, fetchedSet);
        }
        // we have finished with this configuration, if it was the original requested conf
        // we can clean it now
        if (requestedConfSet) {
            node.setRequestedConf(null);
        }

    }

    /**
     * Returns true if we've already fetched the dependencies for this node and configuration
     *
     * @param node
     *            node to check
     * @param conf
     *            configuration to check
     * @return true if we've already fetched this dependency
     */
    private boolean isDependenciesFetched(IvyNode node, String conf, Set<String> fetchedSet) {
        String key = getDependenciesFetchedKey(node, conf);
        return fetchedSet.contains(key);
    }

    private void markDependenciesFetched(IvyNode node, String conf, Set<String> fetchedSet) {
        String key = getDependenciesFetchedKey(node, conf);
        fetchedSet.add(key);
    }

    private String getDependenciesFetchedKey(IvyNode node, String conf) {
        ModuleRevisionId moduleRevisionId = node.getResolvedId();
        return moduleRevisionId.getOrganisation() + "|" + moduleRevisionId.getName() + "|"
                + moduleRevisionId.getRevision() + "|" + conf;
    }

    private void resolveConflict(VisitNode node, String conf) {
        resolveConflict(node, node.getParent(), conf, Collections.<IvyNode> emptySet());
    }

    /**
     * Resolves conflict for the given node in the given ancestor. This method do conflict
     * resolution in ancestor parents recursively, unless not necessary.
     *
     * @param node
     *            the node for which conflict resolution should be done
     * @param ancestor
     *            the ancestor in which the conflict resolution should be done
     * @param conf String
     * @param toevict
     *            a collection of IvyNode to evict (as computed by conflict resolution in
     *            descendants of ancestor)
     * @return true if conflict resolution has been done, false it can't be done yet
     */
    private boolean resolveConflict(VisitNode node, VisitNode ancestor, String conf,
            Collection<IvyNode> toevict) {
        if (ancestor == null || node == ancestor) {
            return true;
        }
        // check if job is not already done
        if (checkConflictSolvedEvicted(node, ancestor)) {
            // job is done and node is evicted, nothing to do
            return true;
        }
        boolean debugConflictResolution = settings.debugConflictResolution();
        if (checkConflictSolvedSelected(node, ancestor)) {
            // job is done and node is selected, nothing to do for this ancestor, but we still have
            // to check higher levels, for which conflict resolution might have been impossible
            // before
            if (resolveConflict(node, ancestor.getParent(), conf, toevict)) {
                // now that conflict resolution is ok in ancestors
                // we just have to check if the node wasn't previously evicted in root ancestor
                EvictionData evictionData = node.getEvictionDataInRoot(node.getRootModuleConf(),
                    ancestor);
                if (evictionData != null) {
                    // node has been previously evicted in an ancestor: we mark it as evicted
                    if (debugConflictResolution) {
                        Message.debug(node + " was previously evicted in root module conf "
                                + node.getRootModuleConf());
                    }
                    node.markEvicted(evictionData);
                    if (debugConflictResolution) {
                        Message.debug("evicting " + node + " by " + evictionData);
                    }
                }
                return true;
            } else {
                return false;
            }
        }

        // compute conflicts
        Set<IvyNode> resolvedNodes = ancestor.getNode().getResolvedNodes(node.getModuleId(),
            node.getRootModuleConf());
        resolvedNodes.addAll(ancestor.getNode().getPendingConflicts(node.getRootModuleConf(),
            node.getModuleId()));
        Collection<IvyNode> conflicts = computeConflicts(node, ancestor, conf, toevict,
            resolvedNodes);

        ConflictManager conflictManager = null;
        for (VisitNode current : ancestor.getPath()) {
            ModuleDescriptor descriptor = current.getNode().getDescriptor();
            if (descriptor == null) {
                throw new IllegalStateException(
                        "impossible to get conflict manager when data has not been loaded. IvyNode = "
                                + current.getNode());
            }

            conflictManager = descriptor.getConflictManager(node.getModuleId());
            if (conflictManager != null) {
                break;
            }
        }

        if (conflictManager == null) {
            conflictManager = settings.getConflictManager(node.getModuleId());
        }

        Collection<IvyNode> resolved = resolveConflicts(node, ancestor, conflicts, conflictManager);

        if (resolved == null) {
            if (debugConflictResolution) {
                Message.debug("impossible to resolve conflicts for " + node + " in " + ancestor
                        + " yet");
                Message.debug("setting all nodes as pending conflicts for later conflict"
                        + " resolution: " + conflicts);
            }
            ancestor.getNode().setPendingConflicts(node.getModuleId(), node.getRootModuleConf(),
                conflicts);
            return false;
        }

        if (debugConflictResolution) {
            Message.debug("selected revisions for " + node + " in " + ancestor + ": " + resolved);
        }
        if (resolved.contains(node.getNode())) {
            // node has been selected for the current parent

            // handle previously selected nodes that are now evicted by this new node
            toevict = resolvedNodes;
            toevict.removeAll(resolved);

            for (IvyNode te : toevict) {
                te.markEvicted(node.getRootModuleConf(), ancestor.getNode(), conflictManager,
                    resolved);

                if (debugConflictResolution) {
                    Message.debug("evicting " + te + " by "
                            + te.getEvictedData(node.getRootModuleConf()));
                }
            }

            // it's very important to update resolved and evicted nodes BEFORE recompute parent call
            // to allow it to recompute its resolved collection with correct data
            // if necessary
            ancestor.getNode().setResolvedNodes(node.getModuleId(), node.getRootModuleConf(),
                resolved);

            Collection<IvyNode> evicted = new HashSet<>(ancestor.getNode().getEvictedNodes(
                    node.getModuleId(), node.getRootModuleConf()));
            evicted.removeAll(resolved);
            evicted.addAll(toevict);
            ancestor.getNode().setEvictedNodes(node.getModuleId(), node.getRootModuleConf(),
                evicted);
            ancestor.getNode().setPendingConflicts(node.getModuleId(), node.getRootModuleConf(),
                Collections.<IvyNode> emptySet());

            return resolveConflict(node, ancestor.getParent(), conf, toevict);
        } else {
            // node has been evicted for the current parent
            if (resolved.isEmpty()) {
                if (debugConflictResolution) {
                    Message.verbose("conflict manager '" + conflictManager
                            + "' evicted all revisions among " + conflicts);
                }
            }

            // it's time to update parent resolved and evicted with what was found

            Collection<IvyNode> evicted = new HashSet<>(ancestor.getNode().getEvictedNodes(
                    node.getModuleId(), node.getRootModuleConf()));
            toevict.removeAll(resolved);
            evicted.removeAll(resolved);
            evicted.addAll(toevict);
            evicted.add(node.getNode());
            ancestor.getNode().setEvictedNodes(node.getModuleId(), node.getRootModuleConf(),
                evicted);
            ancestor.getNode().setPendingConflicts(node.getModuleId(), node.getRootModuleConf(),
                Collections.<IvyNode> emptySet());

            node.markEvicted(ancestor, conflictManager, resolved);
            if (debugConflictResolution) {
                Message.debug("evicting " + node + " by " + node.getEvictedData());
            }

            // if resolved changed we have to go up in the graph
            Collection<IvyNode> prevResolved = ancestor.getNode().getResolvedNodes(
                node.getModuleId(), node.getRootModuleConf());
            boolean solved = true;
            if (!prevResolved.equals(resolved)) {
                ancestor.getNode().setResolvedNodes(node.getModuleId(), node.getRootModuleConf(),
                    resolved);
                for (IvyNode sel : resolved) {
                    if (!prevResolved.contains(sel)) {
                        solved &= resolveConflict(node.gotoNode(sel), ancestor.getParent(), conf,
                            toevict);
                    }
                }
            }
            return solved;
        }
    }

    private Collection<IvyNode> resolveConflicts(VisitNode node, VisitNode ancestor,
            Collection<IvyNode> conflicts, ConflictManager conflictManager) {
        if (node.getParent() != ancestor
        // we are not handling the direct parent

                && conflictManager == settings.getConflictManager(node.getModuleId())
                // the conflict manager is the default one

                && node.getParent().getNode()
                        .getResolvedNodes(node.getModuleId(), node.getRootModuleConf())
                        .equals(conflicts)
        // there is no new conflict in this ancestor

        ) {
            // IVY-465 case
            if (settings.debugConflictResolution()) {
                Message.debug("no new conflicting revisions for " + node + " in " + ancestor + ": "
                        + conflicts);
            }

            return conflicts;
        } else {
            if (settings.debugConflictResolution()) {
                Message.debug("found conflicting revisions for " + node + " in " + ancestor + ": "
                        + conflicts);
            }

            return conflictManager.resolveConflicts(ancestor.getNode(), conflicts);
        }
    }

    /**
     * Compute possible conflicts for a node, in the context of an ancestor (a node which has a
     * dependency - direct or indirect - on the node for which conflicts should be computed.
     *
     * @param node
     *            the node for which conflicts should be computed
     * @param ancestor
     *            the ancestor in which conflicts should be computed
     * @param conf
     *            the configuration of the node in which conflicts should be computed
     * @param toevict
     *            a collection of nodes which have been evicted during conflict resolution at lower
     *            level. It may be empty if no conflict resolution has occurred for this node yet,
     *            or if no node has been evicted.
     * @param selectedNodes
     *            a collection of nodes selected during previous conflict resolution for the given
     *            node and ancestor. This collection is updated by this call, removing nodes which
     *            should be evicted.
     * @return a collection of IvyNode which may be in conflict with the given node in the given
     *         ancestor. This collection always contain at least the given node.
     */
    private Collection<IvyNode> computeConflicts(VisitNode node, VisitNode ancestor, String conf,
            Collection<IvyNode> toevict, Collection<IvyNode> selectedNodes) {
        Collection<IvyNode> conflicts = new LinkedHashSet<>();
        conflicts.add(node.getNode());
        /*
         * We first try to remove all evicted nodes from the collection of selected nodes to update
         * this collection. If the collection changes, it means that it contained evicted nodes, and
         * thus is not up to date.
         */
        boolean evictedInSelected = selectedNodes.removeAll(toevict);
        /*
         * Another case where we need to deeply compute selected nodes is when selectedNodes is
         * empty (not computed yet) and we aren't in the context of the direct parent of the node.
         */
        if (evictedInSelected
                || (selectedNodes.isEmpty() && !node.getParent().getNode()
                        .equals(ancestor.getNode()))) {
            IvyContext context = IvyContext.getContext();
            ResolveData data = context.getResolveData();
            VisitNode oldVisitNode = data.getCurrentVisitNode();
            data.setCurrentVisitNode(ancestor);
            try {
                // In this case we need to compute selected nodes again.
                Collection<IvyNode> deps = ancestor.getNode().getDependencies(
                    node.getRootModuleConf(),
                    ancestor.getNode().getConfigurations(node.getRootModuleConf()),
                    ancestor.getRequestedConf());
                for (IvyNode dep : deps) {
                    if (dep.getModuleId().equals(node.getModuleId())) {
                        conflicts.add(dep);
                    }
                    conflicts.addAll(dep.getResolvedNodes(node.getModuleId(),
                        node.getRootModuleConf()));
                }
            } finally {
                data.setCurrentVisitNode(oldVisitNode);
            }
        } else if (selectedNodes.isEmpty()) {
            /*
             * No selected nodes at all yet, and we are in the context of the direct parent
             * (otherwise previous block would have been reached). We can compute conflicts based on
             * the parent direct dependencies in current root module conf.
             */
            VisitNode parent = node.getParent();
            Collection<IvyNode> parentDepIvyNodes = parent.getNode().getDependencies(
                node.getRootModuleConf(),
                parent.getNode().getConfigurations(node.getRootModuleConf()),
                parent.getRequestedConf());
            for (IvyNode parentDep : parentDepIvyNodes) {
                if (parentDep.getModuleId().equals(node.getModuleId())) {
                    conflicts.add(parentDep);
                }
            }
        } else {
            conflicts.addAll(selectedNodes);
        }
        return conflicts;
    }

    private boolean checkConflictSolvedSelected(VisitNode node, VisitNode ancestor) {
        if (ancestor.getResolvedRevisions(node.getModuleId()).contains(node.getResolvedId())) {
            // resolve conflict has already be done with node with the same id
            if (settings.debugConflictResolution()) {
                Message.debug("conflict resolution already done for " + node + " in " + ancestor);
            }
            return true;
        }
        return false;
    }

    private boolean checkConflictSolvedEvicted(VisitNode node, VisitNode ancestor) {
        if (ancestor.getEvictedRevisions(node.getModuleId()).contains(node.getResolvedId())) {
            // resolve conflict has already be done with node with the same id
            if (settings.debugConflictResolution()) {
                Message.debug("conflict resolution already done for " + node + " in " + ancestor);
            }
            return true;
        }
        return false;
    }

    public ResolvedModuleRevision findModule(ModuleRevisionId id, ResolveOptions options) {
        DependencyResolver r = settings.getResolver(id);
        if (r == null) {
            throw new IllegalStateException("no resolver found for " + id.getModuleId());
        }
        DefaultModuleDescriptor md = DefaultModuleDescriptor.newCallerInstance(id,
            new String[] {"*"}, false, false);

        if (options.getResolveId() == null) {
            options.setResolveId(ResolveOptions.getDefaultResolveId(md));
        }

        try {
            return r.getDependency(new DefaultDependencyDescriptor(id, true), new ResolveData(this,
                    options, new ConfigurationResolveReport(this, md, "default", null, options)));
        } catch (ParseException e) {
            throw new RuntimeException("problem while parsing repository module descriptor for "
                    + id + ": " + e, e);
        }
    }

    /**
     * Mediates the given dependency descriptor according to given options.
     * <p>
     * The mediated dependency descriptor must return the actually requested module revision id when
     * the method {@link DependencyDescriptor#getDependencyRevisionId()} is called.
     * </p>
     *
     * @param dd
     *            the dependency descriptor for which the requested module revision id should be
     *            returned
     * @param options
     *            the resolve options to use
     * @return the mediated {@link DependencyDescriptor}.
     */
    public DependencyDescriptor mediate(DependencyDescriptor dd, ResolveOptions options) {
        if (dd == null) {
            return null;
        }
        String resolveMode = options.getResolveMode() == null ? settings.getResolveMode(dd
                .getDependencyId()) : options.getResolveMode();
        if (ResolveOptions.RESOLVEMODE_DYNAMIC.equals(resolveMode)
                && !dd.getDynamicConstraintDependencyRevisionId().equals(
                    dd.getDependencyRevisionId())) {
            // the dynamicRevId can contain a null branch, so make sure this
            // has been replaced by the default branch (if any!)
            return dd.clone(ModuleRevisionId.newInstance(dd
                    .getDynamicConstraintDependencyRevisionId(), dd
                    .getDynamicConstraintDependencyRevisionId().getRevision()));
        } else {
            return dd;
        }
    }

    public EventManager getEventManager() {
        return eventManager;
    }

    public ResolveEngineSettings getSettings() {
        return settings;
    }

    public SortEngine getSortEngine() {
        return sortEngine;
    }

    private void checkInterrupted() {
        IvyContext.getContext().getIvy().checkInterrupted();
    }

}
