/*
 *  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.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.LogOptions;
import org.apache.ivy.core.event.resolve.EndResolveDependencyEvent;
import org.apache.ivy.core.event.resolve.StartResolveDependencyEvent;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultArtifact;
import org.apache.ivy.core.module.descriptor.DependencyArtifactDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.IncludeRule;
import org.apache.ivy.core.module.descriptor.MDArtifact;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ArtifactId;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.resolve.IvyNodeCallers.Caller;
import org.apache.ivy.core.resolve.IvyNodeEviction.EvictionData;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.plugins.conflict.LatestCompatibleConflictManager;
import org.apache.ivy.plugins.matcher.MatcherHelper;
import org.apache.ivy.plugins.resolver.DependencyResolver;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.StringUtils;
import org.apache.ivy.util.filter.Filter;
import org.apache.ivy.util.filter.FilterHelper;

import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PRIVATE;
import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PUBLIC;
import static org.apache.ivy.util.StringUtils.splitToArray;

public class IvyNode implements Comparable<IvyNode> {
    private static final Pattern FALLBACK_CONF_PATTERN = Pattern.compile("(.+)\\((.*)\\)");

    // //////// CONTEXT
    private ResolveData data;

    private ResolveEngineSettings settings;

    // //////// DELEGATES
    private IvyNodeCallers callers;

    private IvyNodeEviction eviction;

    // //////// MAIN DATA

    private IvyNode root;

    // id as requested, i.e. may be with latest rev
    private ModuleRevisionId id;

    // set only when node has been built or updated from a DependencyDescriptor
    private Map<IvyNode, DependencyDescriptor> dds = new HashMap<>();

    // Set when data has been loaded only, or when constructed from a module descriptor
    private ModuleDescriptor md;

    private ResolvedModuleRevision module;

    // //////// LOADING METADATA
    private Exception problem = null;

    private boolean downloaded = false;

    private boolean searched = false;

    private Collection<String> confsToFetch = new HashSet<>();

    private Collection<String> fetchedConfigurations = new HashSet<>();

    private Collection<String> loadedRootModuleConfs = new HashSet<>();

    // //////// USAGE DATA

    private IvyNodeUsage usage = new IvyNodeUsage(this);

    // usage information merged from evicted nodes this node is "replacing"
    private Map<ModuleRevisionId, IvyNodeUsage> mergedUsages = new LinkedHashMap<>();

    public IvyNode(ResolveData data, IvyNode parent, DependencyDescriptor dd) {
        id = dd.getDependencyRevisionId();
        dds.put(parent, dd);
        root = parent.getRoot();
        init(data);
    }

    public IvyNode(ResolveData data, ModuleDescriptor md) {
        id = md.getModuleRevisionId();
        this.md = md;
        root = this;
        init(data);
    }

    private void init(ResolveData data) {
        this.data = data;
        settings = data.getSettings();
        eviction = new IvyNodeEviction(this);
        callers = new IvyNodeCallers(this);
    }

    /**
     * After the call node may be discarded. To avoid using discarded node, make sure to get the
     * real node after the call
     * <code>IvyNode node = ... node.loadData(); node = node.getRealNode(); ...</code>
     *
     * @param rootModuleConf String
     * @param parent IvyNode
     * @param parentConf String
     * @param conf String
     * @param shouldBePublic boolean
     * @param usage IvyNodeUsage
     * @return boolean
     */
    public boolean loadData(String rootModuleConf, IvyNode parent, String parentConf, String conf,
            boolean shouldBePublic, IvyNodeUsage usage) {
        Message.debug("loadData of " + this.toString() + " of rootConf=" + rootModuleConf);
        if (!isRoot() && (data.getReport() != null)) {
            data.getReport().addDependency(this);
        }

        boolean loaded = false;
        if (hasProblem()) {
            Message.debug("Node has problem.  Skip loading");
        } else if (isEvicted(rootModuleConf)) {
            Message.debug(rootModuleConf + " is evicted.  Skip loading");
        } else if (!hasConfigurationsToLoad() && isRootModuleConfLoaded(rootModuleConf)) {
            Message.debug(rootModuleConf + " is loaded and no conf to load.  Skip loading");
        } else {
            markRootModuleConfLoaded(rootModuleConf);
            if (md == null) {
                DependencyResolver resolver = data.getSettings().getResolver(getId());
                if (resolver == null) {
                    Message.error("no resolver found for " + getModuleId()
                            + ": check your configuration");
                    problem = new RuntimeException("no resolver found for " + getModuleId()
                            + ": check your configuration");
                    return false;
                }
                try {
                    Message.debug("\tusing " + resolver + " to resolve " + getId());
                    DependencyDescriptor dependencyDescriptor = getDependencyDescriptor(parent);
                    long start = System.currentTimeMillis();
                    ModuleRevisionId requestedRevisionId = dependencyDescriptor
                            .getDependencyRevisionId();
                    data.getEventManager().fireIvyEvent(
                        new StartResolveDependencyEvent(resolver, dependencyDescriptor,
                                requestedRevisionId));
                    module = resolver.getDependency(dependencyDescriptor, data);
                    data.getEventManager().fireIvyEvent(
                        new EndResolveDependencyEvent(resolver, dependencyDescriptor,
                                requestedRevisionId, module, System.currentTimeMillis() - start));

                    if (module != null) {
                        module.getResolver()
                                .getRepositoryCacheManager()
                                .saveResolvers(module.getDescriptor(),
                                    module.getResolver().getName(),
                                    module.getArtifactResolver().getName());
                        if (settings.logModuleWhenFound()
                                && LogOptions.LOG_DEFAULT.equals(getData().getOptions().getLog())) {
                            Message.info("\tfound " + module.getId() + " in "
                                    + module.getResolver().getName());
                        } else {
                            Message.verbose("\tfound " + module.getId() + " in "
                                    + module.getResolver().getName());
                        }

                        // IVY-56: check if revision has actually been resolved
                        if (settings.getVersionMatcher().isDynamic(getId())
                                && settings.getVersionMatcher().isDynamic(module.getId())) {
                            Message.error("impossible to resolve dynamic revision for " + getId()
                                    + ": check your configuration and make sure revision is part of your pattern");
                            problem = new RuntimeException("impossible to resolve dynamic revision");
                            return false;
                        }
                        if (!getId().equals(module.getId())) {
                            IvyNode resolved = data.getNode(module.getId());
                            if (resolved != null) {
                                // found revision has already been resolved
                                // => update it and discard this node
                                md = module.getDescriptor(); // needed for handleConfiguration
                                if (!handleConfiguration(loaded, rootModuleConf, parent,
                                    parentConf, conf, shouldBePublic, usage)) {
                                    return false;
                                }

                                moveToRealNode(rootModuleConf, parent, parentConf, conf,
                                    shouldBePublic, resolved);

                                return true;
                            }
                            String log = "\t[" + module.getId().getRevision() + "] " + getId();
                            if (!settings.getVersionMatcher().isDynamic(getId())) {
                                log += " (forced)";
                            }
                            if (settings.logResolvedRevision()
                                    && LogOptions.LOG_DEFAULT.equals(getData().getOptions()
                                            .getLog())) {
                                Message.info(log);
                            } else {
                                Message.verbose(log);
                            }
                        }
                        downloaded = module.getReport().isDownloaded();
                        searched = module.getReport().isSearched();
                        loaded = true;

                        md = module.getDescriptor();
                        confsToFetch.remove("*");
                        updateConfsToFetch(Arrays
                                .asList(resolveSpecialConfigurations(getRequiredConfigurations(
                                    parent, parentConf))));
                    } else {
                        Message.warn("\tmodule not found: " + getId());
                        resolver.reportFailure();
                        problem = new RuntimeException("not found");
                        return false;
                    }
                } catch (ResolveProcessException e) {
                    throw e;
                } catch (Exception e) {
                    problem = e;
                    Message.debug("Unexpected error: " + problem.getMessage(), problem);
                    return false;
                }
            } else {
                loaded = true;
            }
        }
        handleConfiguration(loaded, rootModuleConf, parent, parentConf, conf, shouldBePublic, usage);
        if (hasProblem()) {
            Message.debug("problem : " + problem.getMessage());
            return false;
        }
        DependencyDescriptor dd = getDependencyDescriptor(parent);
        if (dd != null) {
            usage.addUsage(rootModuleConf, dd, parentConf);
        }
        return loaded;
    }

    private void moveToRealNode(String rootModuleConf, IvyNode parent, String parentConf,
            String conf, boolean shouldBePublic, IvyNode resolved) {
        if (resolved.md == null) {
            resolved.md = md;
        }
        if (resolved.module == null) {
            resolved.module = module;
        }
        resolved.downloaded |= module.getReport().isDownloaded();
        resolved.searched |= module.getReport().isSearched();
        resolved.dds.putAll(dds);
        resolved.updateDataFrom(this, rootModuleConf, true);
        resolved.loadData(rootModuleConf, parent, parentConf, conf, shouldBePublic, usage);
        resolved.usage.updateDataFrom(getAllUsages(), rootModuleConf);
        usage = resolved.usage;

        data.replaceNode(getId(), resolved, rootModuleConf); // this actually discards the node

        if (settings.logResolvedRevision()
                && LogOptions.LOG_DEFAULT.equals(getData().getOptions().getLog())) {
            Message.info("\t[" + module.getId().getRevision() + "] " + getId());
        } else {
            Message.verbose("\t[" + module.getId().getRevision() + "] " + getId());
        }
    }

    public Collection<IvyNode> getDependencies(String rootModuleConf, String[] confs,
            String requestedConf) {
        if (md == null) {
            throw new IllegalStateException(
                    "impossible to get dependencies when data has not been loaded");
        }
        if (Arrays.asList(confs).contains("*")) {
            if (isRoot()) {
                confs = md.getConfigurationsNames();
            } else {
                confs = md.getPublicConfigurationsNames();
            }
        }
        Collection<IvyNode> deps = new HashSet<>();
        for (String conf : confs) {
            deps.addAll(getDependencies(rootModuleConf, conf, requestedConf));
        }
        return deps;
    }

    /**
     * Load the dependencies of the current node
     * <p>
     * The resulting collection of nodes may have some configuration to load
     *
     * @param rootModuleConf
     *            the requested configuration of the root module
     * @param conf
     *            the configuration to load of this node
     * @param requestedConf
     *            the actual node conf requested, possibly extending the <code>conf</code> one.
     * @return {@link Collection} of {@link IvyNode}
     */
    public Collection<IvyNode> getDependencies(String rootModuleConf, String conf,
            String requestedConf) {
        if (md == null) {
            throw new IllegalStateException(
                    "impossible to get dependencies when data has not been loaded");
        }
        // it's important to respect order => LinkedHashMap
        Map<ModuleRevisionId, IvyNode> dependencies = new LinkedHashMap<>();
        for (DependencyDescriptor dependencyDescriptor : md.getDependencies()) {
            DependencyDescriptor dd = data.mediate(dependencyDescriptor);
            String[] dependencyConfigurations = dd.getDependencyConfigurations(conf, requestedConf);
            if (dependencyConfigurations.length == 0) {
                // no configuration of the dependency is required for current confs :
                // it is exactly the same as if there was no dependency at all on it
                continue;
            }
            ModuleRevisionId requestedDependencyRevisionId = dd.getDependencyRevisionId();
            if (isDependencyModuleExcluded(dd, rootModuleConf, requestedDependencyRevisionId, conf)) {
                // the whole module is excluded, it is considered as not being part of dependencies
                // at all
                Message.verbose("excluding " + dd + " in " + conf);
                continue;
            }

            // check if not already loaded here
            IvyNode depNode = dependencies.get(requestedDependencyRevisionId);
            if (depNode == null) {
                // check if not already loaded during the resolve session
                depNode = data.getNode(requestedDependencyRevisionId);
            }

            if (depNode == null) {
                depNode = new IvyNode(data, this, dd);
            } else {
                depNode.addDependencyDescriptor(this, dd);
                if (depNode.hasProblem()) {
                    // dependency already tried to be resolved, but unsuccessfully
                    // nothing special to do
                }

            }
            String[] confsArray = depNode.resolveSpecialConfigurations(dependencyConfigurations);
            Collection<String> confs = Arrays.asList(confsArray);
            depNode.updateConfsToFetch(confs);
            depNode.addRootModuleConfigurations(depNode.usage, rootModuleConf, confsArray);
            depNode.usage.setRequiredConfs(this, conf, confs);

            depNode.addCaller(rootModuleConf, this, conf, requestedConf, dependencyConfigurations,
                dd);
            dependencies.put(requestedDependencyRevisionId, depNode);
        }
        return dependencies.values();
    }

    private void addDependencyDescriptor(IvyNode parent, DependencyDescriptor dd) {
        dds.put(parent, dd);
    }

    public DependencyDescriptor getDependencyDescriptor(IvyNode parent) {
        return dds.get(parent);
    }

    private boolean isDependencyModuleExcluded(DependencyDescriptor dd, String rootModuleConf,
            ModuleRevisionId dependencyRevisionId, String conf) {
        Artifact a = DefaultArtifact.newIvyArtifact(dependencyRevisionId, null);
        if (isRoot()) {
            // no callers, but maybe some exclude
            Boolean exclude = doesExclude(md, rootModuleConf, new String[] {rootModuleConf}, dd, a,
                new ArrayDeque<IvyNode>());
            return exclude != null && exclude;
        }
        return callers.doesCallersExclude(rootModuleConf, a);
    }

    Boolean doesExclude(ModuleDescriptor md, String rootModuleConf, String[] moduleConfs,
            DependencyDescriptor dd, Artifact artifact, Deque<IvyNode> callersStack) {
        // artifact is excluded if it match any of the exclude pattern for this dependency...
        if (directlyExcludes(md, moduleConfs, dd, artifact)) {
            return Boolean.TRUE;
        }
        // ... or if it is excluded by all its callers
        IvyNode c = getData().getNode(md.getModuleRevisionId());
        if (c != null) {
            if (callersStack.contains(c)) {
                // a circular dependency, we cannot be conclusive here
                return null;
            }
            return c.doesCallersExclude(rootModuleConf, artifact, callersStack);
        }
        return Boolean.FALSE;
    }

    public boolean directlyExcludes(ModuleDescriptor md, String[] moduleConfs,
            DependencyDescriptor dd, Artifact artifact) {
        return dd != null && dd.doesExclude(moduleConfs, artifact.getId().getArtifactId())
                || md.doesExclude(moduleConfs, artifact.getId().getArtifactId());
    }

    public boolean hasConfigurationsToLoad() {
        return !confsToFetch.isEmpty();
    }

    private boolean markRootModuleConfLoaded(String rootModuleConf) {
        return loadedRootModuleConfs.add(rootModuleConf);
    }

    private boolean isRootModuleConfLoaded(String rootModuleConf) {
        return loadedRootModuleConfs.contains(rootModuleConf);
    }

    private boolean handleConfiguration(boolean loaded, String rootModuleConf, IvyNode parent,
            String parentConf, String conf, boolean shouldBePublic, IvyNodeUsage usage) {
        if (md != null) {
            String[] confs = getRealConfs(conf);
            addRootModuleConfigurations(usage, rootModuleConf, confs);
            for (String realConf : confs) {
                Configuration c = md.getConfiguration(realConf);
                if (c == null) {
                    confsToFetch.remove(conf);
                    if (isConfRequiredByMergedUsageOnly(rootModuleConf, conf)) {
                        Message.verbose("configuration required by evicted revision is not available in "
                                + "selected revision. skipping " + conf + " in " + this);
                    } else if (!conf.equals(realConf)) {
                        problem = new RuntimeException("configuration not found in " + this + ": '"
                                + conf + "'. Missing configuration: '" + realConf
                                + "'. It was required from " + parent + " " + parentConf);
                    } else {
                        problem = new RuntimeException("configuration not found in " + this + ": '"
                                + realConf + "'. It was required from " + parent + " " + parentConf);
                    }
                    return false;
                }
                if (shouldBePublic && !isRoot() && !PUBLIC.equals(c.getVisibility())) {
                    confsToFetch.remove(conf);
                    if (isConfRequiredByMergedUsageOnly(rootModuleConf, conf)) {
                        Message.verbose("configuration required by evicted revision is not visible in "
                                + "selected revision. skipping " + conf + " in " + this);
                    } else {
                        problem = new RuntimeException("configuration not public in " + this
                                + ": '" + c + "'. It was required from " + parent + " "
                                + parentConf);
                    }
                    return false;
                }
            }
            if (loaded) {
                fetchedConfigurations.add(conf);
                confsToFetch.removeAll(Arrays.asList(confs));
                confsToFetch.remove(conf);
            }
        }
        return true;
    }

    private String getDefaultConf(String conf) {
        Matcher m = FALLBACK_CONF_PATTERN.matcher(conf);
        return m.matches() ? m.group(2) : conf;
    }

    private String getMainConf(String conf) {
        Matcher m = FALLBACK_CONF_PATTERN.matcher(conf);
        return m.matches() ? m.group(1) : null;
    }

    public void updateConfsToFetch(Collection<String> confs) {
        confsToFetch.addAll(confs);
        confsToFetch.removeAll(fetchedConfigurations);
    }

    /**
     * resolve the '*' special configurations if necessary and possible
     */
    private String[] resolveSpecialConfigurations(String[] dependencyConfigurations) {
        if (dependencyConfigurations.length == 1 && dependencyConfigurations[0].startsWith("*")
                && isLoaded()) {
            String conf = dependencyConfigurations[0];
            if ("*".equals(conf)) {
                return getDescriptor().getPublicConfigurationsNames();
            }
            // there are exclusions in the configuration
            List<String> exclusions = Arrays.asList(conf.substring(2).split("\\!"));

            List<String> ret = new ArrayList<>(Arrays.asList(getDescriptor()
                    .getPublicConfigurationsNames()));
            ret.removeAll(exclusions);

            return ret.toArray(new String[ret.size()]);
        }
        return dependencyConfigurations;
    }

    /**
     * returns the required configurations from the given node
     *
     * @param in IvyNode
     * @param inConf ditto
     * @return array of configuration names
     */
    public String[] getRequiredConfigurations(IvyNode in, String inConf) {
        Collection<String> req = new LinkedHashSet<>();
        addAllIfNotNull(req, usage.getRequiredConfigurations(in, inConf));
        for (IvyNodeUsage usage : mergedUsages.values()) {
            addAllIfNotNull(req, usage.getRequiredConfigurations(in, inConf));
        }
        return req.toArray(new String[req.size()]);
    }

    private <T> void addAllIfNotNull(Collection<T> into, Collection<T> col) {
        if (col != null) {
            into.addAll(col);
        }
    }

    /**
     * returns all the current required configurations of the node
     *
     * @return array of configuration names
     */
    public String[] getRequiredConfigurations() {
        Collection<String> required = new ArrayList<>(confsToFetch.size()
                + fetchedConfigurations.size());
        required.addAll(fetchedConfigurations);
        required.addAll(confsToFetch);
        return required.toArray(new String[required.size()]);
    }

    public Configuration getConfiguration(String conf) {
        if (md == null) {
            throw new IllegalStateException(
                    "impossible to get configuration when data has not been loaded");
        }
        String defaultConf = getDefaultConf(conf);
        conf = getMainConf(conf);
        Configuration configuration = md.getConfiguration(conf);
        if (configuration == null) {
            configuration = md.getConfiguration(defaultConf);
        }
        return configuration;
    }

    /**
     * Returns the configurations of the dependency required in a given root module configuration.
     *
     * @param rootModuleConf String
     * @return array of configuration names
     */
    public String[] getConfigurations(String rootModuleConf) {
        Set<String> depConfs = new LinkedHashSet<>();
        addAllIfNotNull(depConfs, usage.getConfigurations(rootModuleConf));
        for (IvyNodeUsage usage : mergedUsages.values()) {
            addAllIfNotNull(depConfs, usage.getConfigurations(rootModuleConf));
        }
        return depConfs.toArray(new String[depConfs.size()]);
    }

    protected boolean isConfRequiredByMergedUsageOnly(String rootModuleConf, String conf) {
        Set<String> confs = usage.getConfigurations(rootModuleConf);
        return confs == null || !confs.contains(conf);
    }

    // This is never called. Could we remove it?
    @Deprecated
    public void discardConf(String rootModuleConf, String conf) {
        Set<String> depConfs = usage.addAndGetConfigurations(rootModuleConf);
        if (md == null) {
            depConfs.remove(conf);
        } else {
            // remove all given dependency configurations to the set + extended ones
            Configuration c = md.getConfiguration(conf);
            if (conf == null) {
                Message.warn("unknown configuration in " + getId() + ": " + conf);
            } else {
                // recursive remove of extended configurations
                for (String ext : c.getExtends()) {
                    discardConf(rootModuleConf, ext);
                }
                depConfs.remove(c.getName());
            }
        }
    }

    private void addRootModuleConfigurations(IvyNodeUsage usage, String rootModuleConf,
            String[] dependencyConfs) {
        if (md != null) {
            // add all given dependency configurations to the set + extended ones
            for (String dependencyConf : dependencyConfs) {
                Configuration conf = md.getConfiguration(dependencyConf);
                if (conf != null) {
                    // recursive add of extended
                    addRootModuleConfigurations(usage, rootModuleConf, conf.getExtends());
                }
            }
        }
        Collections.addAll(usage.addAndGetConfigurations(rootModuleConf), dependencyConfs);
    }

    /**
     * Returns the root module configurations in which this dependency is required
     *
     * @return array of configuration names
     */
    public String[] getRootModuleConfigurations() {
        Set<String> confs = getRootModuleConfigurationsSet();
        return confs.toArray(new String[confs.size()]);
    }

    /**
     * Returns the root module configurations in which this dependency is required
     *
     * @return {@link Set} of configuration names
     */
    public Set<String> getRootModuleConfigurationsSet() {
        Set<String> confs = new LinkedHashSet<>();
        addAllIfNotNull(confs, usage.getRootModuleConfigurations());
        for (IvyNodeUsage usage : mergedUsages.values()) {
            addAllIfNotNull(confs, usage.getRootModuleConfigurations());
        }
        return confs;
    }

    public String[] getConfsToFetch() {
        return confsToFetch.toArray(new String[confsToFetch.size()]);
    }

    public String[] getRealConfs(String conf) {
        if (md == null) {
            return new String[] {conf};
        }
        String defaultConf = getDefaultConf(conf);
        conf = getMainConf(conf);
        if (md.getConfiguration(conf) == null
                || PRIVATE.equals(md.getConfiguration(conf).getVisibility())) {
            if ("".equals(defaultConf)) {
                return new String[0];
            }
            conf = defaultConf;
        }
        if (conf.charAt(0) == '*') {
            return resolveSpecialConfigurations(new String[] {conf});
        }
        if (conf.contains(",")) {
            return splitToArray(conf);
        }
        return new String[] {conf};

    }

    /**
     * Finds and returns a path in callers from the given module id to the current node
     *
     * @param from
     *            the module id to start the path from
     * @return a collection representing the path, starting with the from node, followed by the list
     *         of nodes being one path to the current node, excluded
     */
    private Collection<IvyNode> findPath(ModuleId from) {
        return findPath(from, this, new LinkedList<IvyNode>());
    }

    private Collection<IvyNode> findPath(ModuleId from, IvyNode node, List<IvyNode> path) {
        IvyNode parent = node.getDirectCallerFor(from);
        if (parent == null) {
            throw new IllegalArgumentException("no path from " + from + " to " + getId() + " found");
        }
        if (path.contains(parent)) {
            path.add(0, parent);
            Message.verbose(
                "circular dependency found while looking for the path for another one: was looking for "
                        + from + " as a caller of " + path.get(path.size() - 1));
            return path;
        }
        path.add(0, parent);
        if (parent.getId().getModuleId().equals(from)) {
            return path;
        }
        return findPath(from, parent, path);
    }

    /**
     * Update data in this node from data of the given node, for the given root module
     * configuration.
     *
     * @param node
     *            the source node from which data should be copied
     * @param rootModuleConf
     *            the root module configuration for which data should be updated
     * @param real
     *            true if the node to update from actually corresponds to the same real node
     *            (usually updated because of dynamic revision resolution), false if it's not the
     *            same real node (usually updated because of node eviction)
     */
    private void updateDataFrom(IvyNode node, String rootModuleConf, boolean real) {
        // update callers
        callers.updateFrom(node.callers, rootModuleConf, real);

        if (real) {
            usage.updateDataFrom(node.getAllUsages(), rootModuleConf);
        } else {
            // let's copy usage information for the given rootModuleConf, into a separate usage
            // object to keep detailed data about where usage comes from
            IvyNodeUsage mergedUsage = mergedUsages.get(node.getId());
            if (mergedUsage == null) {
                mergedUsage = new IvyNodeUsage(node);
                mergedUsages.put(node.getId(), mergedUsage);
            }
            mergedUsage.updateDataFrom(node.getAllUsages(), rootModuleConf);
        }

        // update confsToFetch
        updateConfsToFetch(node.fetchedConfigurations);
        updateConfsToFetch(node.confsToFetch);
    }

    private Collection<IvyNodeUsage> getAllUsages() {
        Collection<IvyNodeUsage> usages = new ArrayList<>();
        usages.add(usage);
        usages.addAll(mergedUsages.values());
        return usages;
    }

    /**
     * Returns all the artifacts of this dependency required in all the root module configurations
     *
     * @return array of {@link Artifact}s
     */
    public Artifact[] getAllArtifacts() {
        Set<Artifact> ret = new HashSet<>();
        for (String rootModuleConf : getRootModuleConfigurationsSet()) {
            ret.addAll(Arrays.asList(getArtifacts(rootModuleConf)));
        }
        return ret.toArray(new Artifact[ret.size()]);
    }

    /**
     * Returns all the artifacts of this dependency required in the root module configurations in
     * which the node is not evicted nor blacklisted
     *
     * @param artifactFilter Filter
     * @return array of {@link Artifact}s
     */
    public Artifact[] getSelectedArtifacts(Filter<Artifact> artifactFilter) {
        Collection<Artifact> ret = new HashSet<>();
        for (String rootModuleConf : getRootModuleConfigurationsSet()) {
            if (!isEvicted(rootModuleConf) && !isBlacklisted(rootModuleConf)) {
                ret.addAll(Arrays.asList(getArtifacts(rootModuleConf)));
            }
        }
        ret = FilterHelper.filter(ret, artifactFilter);
        return ret.toArray(new Artifact[ret.size()]);
    }

    /**
     * Returns the artifacts of this dependency required in the configurations themselves required
     * in the given root module configuration
     *
     * @param rootModuleConf String
     * @return array of {@link Artifact}s
     */
    public Artifact[] getArtifacts(String rootModuleConf) {
        // first we look for the dependency configurations required
        // in the given root module configuration
        String[] confs = getConfigurations(rootModuleConf);
        if (confs == null || confs.length == 0) {
            // no configuration required => no artifact required
            return new Artifact[0];
        }
        if (md == null) {
            throw new IllegalStateException(
                    "impossible to get artifacts when data has not been loaded. IvyNode = "
                            + this);
        }

        Set<Artifact> artifacts = new HashSet<>(); // the set we fill before returning

        // we check if we have dependencyArtifacts includes description for this rootModuleConf
        Set<DependencyArtifactDescriptor> dependencyArtifacts = usage
                .getDependencyArtifactsSet(rootModuleConf);

        if (md.isDefault() && dependencyArtifacts != null && !dependencyArtifacts.isEmpty()) {
            addArtifactsFromOwnUsage(artifacts, dependencyArtifacts);
            addArtifactsFromMergedUsage(rootModuleConf, artifacts);
        } else {
            Set<IncludeRule> includes = new LinkedHashSet<>();
            addAllIfNotNull(includes, usage.getDependencyIncludesSet(rootModuleConf));
            for (IvyNodeUsage usage : mergedUsages.values()) {
                addAllIfNotNull(includes, usage.getDependencyIncludesSet(rootModuleConf));
            }

            if ((dependencyArtifacts == null || dependencyArtifacts.isEmpty())
                    && includes.isEmpty()) {
                // no artifacts / includes: we get all artifacts as defined by the descriptor
                for (String conf : confs) {
                    artifacts.addAll(Arrays.asList(md.getArtifacts(conf)));
                }
            } else {
                // we have to get only artifacts listed as "includes";
                // first we get all artifacts as defined by the module descriptor
                // and classify them by artifact id
                Map<ArtifactId, Artifact> allArtifacts = new HashMap<>();
                for (String conf : confs) {
                    for (Artifact art : md.getArtifacts(conf)) {
                        allArtifacts.put(art.getId().getArtifactId(), art);
                    }
                }

                // now we add caller defined ones
                if (dependencyArtifacts != null) {
                    addArtifactsFromOwnUsage(artifacts, dependencyArtifacts);
                }
                addArtifactsFromMergedUsage(rootModuleConf, artifacts);

                // and now we filter according to include rules
                Iterator<IncludeRule> it = includes.iterator();
                while (it.hasNext()) {
                    IncludeRule dad = it.next();
                    Collection<Artifact> arts = findArtifactsMatching(dad, allArtifacts);
                    if (arts.isEmpty()) {
                        Message.error("a required artifact is not listed by module descriptor: "
                                + dad.getId());
                        // we remove it from required list to prevent message to be displayed more
                        // than once
                        it.remove();
                    } else {
                        Message.debug(this + " in " + rootModuleConf + ": including " + arts);
                        artifacts.addAll(arts);
                    }
                }
            }
        }

        // now exclude artifacts that aren't accepted by any caller
        Iterator<Artifact> iter = artifacts.iterator();
        while (iter.hasNext()) {
            Artifact artifact = iter.next();
            boolean excluded = callers.doesCallersExclude(rootModuleConf, artifact);
            if (excluded) {
                Message.debug(this + " in " + rootModuleConf + ": excluding " + artifact);
                iter.remove();
            }
        }
        return artifacts.toArray(new Artifact[artifacts.size()]);
    }

    private void addArtifactsFromOwnUsage(Set<Artifact> artifacts,
            Set<DependencyArtifactDescriptor> dependencyArtifacts) {
        for (DependencyArtifactDescriptor dad : dependencyArtifacts) {
            artifacts.add(new MDArtifact(md, dad.getName(), dad.getType(), dad.getExt(), dad
                    .getUrl(), dad.getQualifiedExtraAttributes()));
        }
    }

    private void addArtifactsFromMergedUsage(String rootModuleConf, Set<Artifact> artifacts) {
        for (IvyNodeUsage usage : mergedUsages.values()) {
            Set<DependencyArtifactDescriptor> mergedDependencyArtifacts = usage
                    .getDependencyArtifactsSet(rootModuleConf);
            if (mergedDependencyArtifacts != null) {
                for (DependencyArtifactDescriptor dad : mergedDependencyArtifacts) {
                    Map<String, String> extraAttributes = new HashMap<>(
                            dad.getQualifiedExtraAttributes());
                    MDArtifact artifact = new MDArtifact(md, dad.getName(), dad.getType(),
                            dad.getExt(), dad.getUrl(), extraAttributes);

                    if (!artifacts.contains(artifact)) {
                        // this is later used to know that this is a merged artifact
                        extraAttributes.put("ivy:merged", dad.getDependencyDescriptor()
                                .getParentRevisionId() + " -> " + usage.getNode().getId());
                        artifacts.add(artifact);
                    }
                }
            }
        }
    }

    private static Collection<Artifact> findArtifactsMatching(IncludeRule rule,
            Map<ArtifactId, Artifact> allArtifacts) {
        Collection<Artifact> ret = new ArrayList<>();
        for (Map.Entry<ArtifactId, Artifact> entry : allArtifacts.entrySet()) {
            if (MatcherHelper.matches(rule.getMatcher(), rule.getId(), entry.getKey())) {
                ret.add(entry.getValue());
            }
        }
        return ret;
    }

    public boolean hasProblem() {
        return problem != null;
    }

    public Exception getProblem() {
        return problem;
    }

    public String getProblemMessage() {
        return StringUtils.getErrorMessage(problem);
    }

    public boolean isDownloaded() {
        return downloaded;
    }

    public boolean isSearched() {
        return searched;
    }

    public boolean isLoaded() {
        return md != null;
    }

    public boolean isFetched(String conf) {
        return fetchedConfigurations.contains(conf);
    }

    public IvyNode findNode(ModuleRevisionId mrid) {
        return data.getNode(mrid);
    }

    boolean isRoot() {
        return root == this;
    }

    public IvyNode getRoot() {
        return root;
    }

    public ConflictManager getConflictManager(ModuleId mid) {
        if (md == null) {
            throw new IllegalStateException(
                    "impossible to get conflict manager when data has not been loaded. IvyNode = "
                            + this);
        }
        ConflictManager cm = md.getConflictManager(mid);
        return cm == null ? settings.getConflictManager(mid) : cm;
    }

    public IvyNode getRealNode() {
        IvyNode real = data.getNode(getId());
        return real != null ? real : this;
    }

    public ModuleRevisionId getId() {
        return id;
    }

    public ModuleId getModuleId() {
        return id.getModuleId();
    }

    public ModuleDescriptor getDescriptor() {
        return md;
    }

    public ResolveData getData() {
        return data;
    }

    public ResolvedModuleRevision getModuleRevision() {
        return module;
    }

    public long getPublication() {
        if (module != null) {
            return module.getPublicationDate().getTime();
        }
        return 0;
    }

    /**
     * Returns the last modified timestamp of the module represented by this Node, or 0 if the last
     * modified timestamp is currently unknown (module not loaded)
     *
     * @return the last modified timestamp of the module represented by this Node
     */
    public long getLastModified() {
        if (md != null) {
            return md.getLastModified();
        }
        return 0;
    }

    public ModuleRevisionId getResolvedId() {
        if (md != null && md.getResolvedModuleRevisionId().getRevision() != null) {
            return md.getResolvedModuleRevisionId();
        }
        if (module != null) {
            return module.getId();
        }
        return getId();
    }

    /**
     * Clean data related to one root module configuration only
     */
    public void clean() {
        confsToFetch.clear();
    }

    // /////////////////////////////////////////////////////////////////////////////
    // CALLERS MANAGEMENT
    // /////////////////////////////////////////////////////////////////////////////

    boolean canExclude(String rootModuleConf) {
        for (Caller caller : getCallers(rootModuleConf)) {
            if (caller.canExclude()) {
                return true;
            }
        }
        return false;
    }

    private IvyNode getDirectCallerFor(ModuleId from) {
        return callers.getDirectCallerFor(from);
    }

    public Caller[] getCallers(String rootModuleConf) {
        return callers.getCallers(rootModuleConf);
    }

    public Collection<ModuleId> getAllCallersModuleIds() {
        return callers.getAllCallersModuleIds();
    }

    public Caller[] getAllCallers() {
        return callers.getAllCallers();
    }

    public Caller[] getAllRealCallers() {
        return callers.getAllRealCallers();
    }

    public void addCaller(String rootModuleConf, IvyNode callerNode, String callerConf,
            String requestedConf, String[] dependencyConfs, DependencyDescriptor dd) {
        callers.addCaller(rootModuleConf, callerNode, callerConf, requestedConf, dependencyConfs,
            dd);
        boolean isCircular = callers.getAllCallersModuleIds().contains(getId().getModuleId());
        if (isCircular) {
            IvyContext.getContext().getCircularDependencyStrategy()
                    .handleCircularDependency(toMrids(findPath(getId().getModuleId()), this));
        }
    }

    public boolean doesCallersExclude(String rootModuleConf, Artifact artifact,
            Deque<IvyNode> callersStack) {
        return callers.doesCallersExclude(rootModuleConf, artifact, callersStack);
    }

    @Deprecated
    public boolean doesCallersExclude(String rootModuleConf, Artifact artifact,
            Stack<ModuleRevisionId> callersStack) {
        Deque<IvyNode> callersDeque = new ArrayDeque<>();
        for (ModuleRevisionId mrid : callersStack) {
            for (Caller caller : getCallers(rootModuleConf)) {
                if (caller.getModuleRevisionId().equals(mrid)) {
                    callersDeque.add(data.getNode(mrid));
                }
            }
        }
        return callers.doesCallersExclude(rootModuleConf, artifact, callersDeque);
    }

    private ModuleRevisionId[] toMrids(Collection<IvyNode> path, IvyNode depNode) {
        ModuleRevisionId[] ret = new ModuleRevisionId[path.size() + 1];
        int i = 0;
        for (IvyNode node : path) {
            ret[i++] = node.getId();
        }
        ret[ret.length - 1] = depNode.getId();
        return ret;
    }

    // /////////////////////////////////////////////////////////////////////////////
    // EVICTION MANAGEMENT
    // /////////////////////////////////////////////////////////////////////////////

    /**
     * A copy of the set of resolved nodes (real nodes)
     *
     * @param moduleId ditto
     * @param rootModuleConf String
     * @return Set&lt;IvyNode&gt;
     */
    public Set<IvyNode> getResolvedNodes(ModuleId moduleId, String rootModuleConf) {
        return eviction.getResolvedNodes(moduleId, rootModuleConf);
    }

    public Collection<ModuleRevisionId> getResolvedRevisions(ModuleId moduleId,
            String rootModuleConf) {
        return eviction.getResolvedRevisions(moduleId, rootModuleConf);
    }

    public void markEvicted(EvictionData evictionData) {
        eviction.markEvicted(evictionData);
        String rootModuleConf = evictionData.getRootModuleConf();

        // bug 105: update selected data with evicted one
        if (evictionData.getSelected() != null) {
            for (IvyNode selected : evictionData.getSelected()) {
                selected.updateDataFrom(this, rootModuleConf, false);
            }
        }
    }

    public Collection<ConflictManager> getAllEvictingConflictManagers() {
        return eviction.getAllEvictingConflictManagers();
    }

    public Collection<IvyNode> getAllEvictingNodes() {
        return eviction.getAllEvictingNodes();
    }

    public Collection<String> getAllEvictingNodesDetails() {
        return eviction.getAllEvictingNodesDetails();
    }

    public String[] getEvictedConfs() {
        return eviction.getEvictedConfs();
    }

    public EvictionData getEvictedData(String rootModuleConf) {
        return eviction.getEvictedData(rootModuleConf);
    }

    public Collection<IvyNode> getEvictedNodes(ModuleId mid, String rootModuleConf) {
        return eviction.getEvictedNodes(mid, rootModuleConf);
    }

    public Collection<ModuleRevisionId> getEvictedRevisions(ModuleId mid, String rootModuleConf) {
        return eviction.getEvictedRevisions(mid, rootModuleConf);
    }

    public EvictionData getEvictionDataInRoot(String rootModuleConf, IvyNode ancestor) {
        return eviction.getEvictionDataInRoot(rootModuleConf, ancestor);
    }

    public boolean isCompletelyEvicted() {
        return eviction.isCompletelyEvicted();
    }

    public boolean isEvicted(String rootModuleConf) {
        return eviction.isEvicted(rootModuleConf);
    }

    public void markEvicted(String rootModuleConf, IvyNode node, ConflictManager conflictManager,
            Collection<IvyNode> resolved) {
        EvictionData evictionData = new EvictionData(rootModuleConf, node, conflictManager,
                resolved);
        markEvicted(evictionData);
    }

    public void setEvictedNodes(ModuleId moduleId, String rootModuleConf,
            Collection<IvyNode> evicted) {
        eviction.setEvictedNodes(moduleId, rootModuleConf, evicted);
    }

    public void setResolvedNodes(ModuleId moduleId, String rootModuleConf,
            Collection<IvyNode> resolved) {
        eviction.setResolvedNodes(moduleId, rootModuleConf, resolved);
    }

    @Override
    public String toString() {
        return getResolvedId().toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof IvyNode)) {
            return false;
        }
        IvyNode node = (IvyNode) obj;
        return node.getId().equals(getId());
    }

    public int compareTo(IvyNode that) {
        return this.getModuleId().compareTo(that.getModuleId());
    }

    @Override
    public int hashCode() {
        return getId().hashCode();
    }

    /**
     * Returns a collection of Nodes in conflict for which conflict has been detected but conflict
     * resolution hasn't been done yet
     *
     * @param rootModuleConf ditto
     * @param mid
     *            the module id for which pending conflicts should be found
     * @return a Collection of IvyNode in pending conflict
     */
    public Collection<IvyNode> getPendingConflicts(String rootModuleConf, ModuleId mid) {
        return eviction.getPendingConflicts(rootModuleConf, mid);
    }

    public void setPendingConflicts(ModuleId moduleId, String rootModuleConf,
            Collection<IvyNode> conflicts) {
        eviction.setPendingConflicts(moduleId, rootModuleConf, conflicts);
    }

    // /////////////////////////////////////////////////////////////////////////////
    // BLACKLISTING MANAGEMENT
    // /////////////////////////////////////////////////////////////////////////////

    /**
     * Blacklists the current node, so that a new resolve process won't ever consider this node as
     * available in the repository.
     * <p>
     * This is useful in combination with {@link RestartResolveProcess} for conflict manager
     * implementation which use a best effort strategy to find compatible dependency set, like
     * {@link LatestCompatibleConflictManager}
     * </p>
     *
     * @param bdata
     *            the root module configuration in which the node should be blacklisted
     */
    public void blacklist(IvyNodeBlacklist bdata) {
        if (data.getSettings().logResolvedRevision()) {
            Message.info("BLACKLISTING " + bdata);
        } else {
            Message.verbose("BLACKLISTING " + bdata);
        }

        Stack<IvyNode> callerStack = new Stack<>();
        callerStack.push(this);
        clearEvictionDataInAllCallers(bdata.getRootModuleConf(), callerStack);

        usage.blacklist(bdata);
        data.blacklist(this);
    }

    private void clearEvictionDataInAllCallers(String rootModuleConf, Stack<IvyNode> callerStack) {
        for (Caller caller : callerStack.peek().getCallers(rootModuleConf)) {
            IvyNode callerNode = findNode(caller.getModuleRevisionId());
            if (callerNode != null) {
                callerNode.eviction = new IvyNodeEviction(callerNode);
                if (!callerStack.contains(callerNode)) {
                    callerStack.push(callerNode);
                    clearEvictionDataInAllCallers(rootModuleConf, callerStack);
                    callerStack.pop();
                }
            }
        }
    }

    /**
     * Indicates if this node has been blacklisted in the given root module conf.
     * <p>
     * A blacklisted node should be considered as if it doesn't even exist on the repository.
     * </p>
     *
     * @param rootModuleConf
     *            the root module conf for which we'd like to know if the node is blacklisted
     *
     * @return true if this node is blacklisted int he given root module conf, false otherwise
     * @see #blacklist(IvyNodeBlacklist)
     */
    public boolean isBlacklisted(String rootModuleConf) {
        return usage.isBlacklisted(rootModuleConf);
    }

    /**
     * Indicates if this node has been blacklisted in all root module configurations.
     *
     * @return true if this node is blacklisted in all root module configurations, false otherwise
     * @see #blacklist(IvyNodeBlacklist)
     */
    public boolean isCompletelyBlacklisted() {
        if (isRoot()) {
            return false;
        }
        for (String rootModuleConfiguration : getRootModuleConfigurations()) {
            if (!isBlacklisted(rootModuleConfiguration)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the blacklist data of this node in the given root module conf, or <code>null</code>
     * if this node is not blacklisted in this root module conf.
     *
     * @param rootModuleConf
     *            the root module configuration to consider
     * @return the blacklist data if any
     */
    public IvyNodeBlacklist getBlacklistData(String rootModuleConf) {
        return usage.getBlacklistData(rootModuleConf);
    }

    public IvyNodeUsage getMainUsage() {
        return usage;
    }

    /**
     * Indicates if there is any of the merged usages of this node which has a depender with
     * transitive dependency descriptor.
     * <p>
     * If at there is at least one usage from the merged usages for which there is a depender in the
     * given root module conf which has a dependency descriptor with transitive == true, then it
     * returns true. Otherwise it returns false.
     * </p>
     *
     * @param rootModuleConf
     *            the root module configuration to consider
     * @return true if there is any merged usage with transitive dd, false otherwise.
     */
    public boolean hasAnyMergedUsageWithTransitiveDependency(String rootModuleConf) {
        if (mergedUsages == null) {
            return false;
        }
        for (IvyNodeUsage usage : mergedUsages.values()) {
            if (usage.hasTransitiveDepender(rootModuleConf)) {
                return true;
            }
        }
        return false;
    }

}
