/*
 *  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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;

import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.module.descriptor.Configuration;
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.resolve.IvyNodeEviction.EvictionData;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.util.Checks;

/**
 * A visit node is an object used to represent one visit from one parent on an {@link IvyNode} of
 * the dependency graph. During dependency resolution, the {@link ResolveEngine} visits nodes of the
 * dependency graph following the dependencies, thus the same node can be visited several times, if
 * it is requested from several module. In this case you will have one VisitNode per parent and per
 * root module configuration. Thus VisitNode stores data specific to the visit:
 * <dl>
 * <dd>parent</dd><dt>the node from which the visit is occurring</dt>
 * <dd>parentConf</dd><dt>the configuration of the parent in which this node is visited</dt>
 * <dd>rootModuleConf</dd><dt>the configuration of the root module which is currently resolved</dt>
 * </dl>
 */
public class VisitNode {
    /**
     * The node which is currently visited
     */
    private IvyNode node;

    /**
     * Represents the current parent of the node during ivy visit of dependency graph.
     */
    private VisitNode parent = null;

    /**
     * The root node of the current visit It is null until it is required, see getRoot
     */
    private VisitNode root = null;

    /**
     * Direct path from root to this node. Note that the collection is ordered but is not a list
     * implementation This collection is null until it is required, see getPath
     */
    private Collection<VisitNode> path = null;

    /**
     * The configuration of the parent module in the current visit
     */
    private String parentConf = null;

    /**
     * The configuration requested by the parent Note that this is the actual conf requested by the
     * parent, not a configuration extended by the requested conf which actually trigger the node
     * visit
     */
    private String requestedConf;

    /**
     * The root configuration which is currently visited
     */
    private String rootModuleConf;

    /**
     * Shared ResolveData instance, which can be used to get info on the current resolve process
     */
    private ResolveData data;

    /**
     * Boolean.TRUE if a node with a same module id as the one visited has already been visited in
     * the current path. null if not computed yet Boolean.FALSE otherwise
     */
    private Boolean isCircular;

    /**
     * IvyNode usage information to update when visiting the underlying IvyNode. This is usually the
     * main IvyNodeUsage of the underlying node, except when we are visiting it coming from an
     * evicted node replaced by the other one.
     */
    private IvyNodeUsage usage;

    public VisitNode(ResolveData data, IvyNode node, VisitNode parent, String rootModuleConf,
            String parentConf) {
        this(data, node, parent, rootModuleConf, parentConf, null);
    }

    public VisitNode(ResolveData data, IvyNode node, VisitNode parent, String rootModuleConf,
            String parentConf, IvyNodeUsage usage) {
        Checks.checkNotNull(data, "data");
        Checks.checkNotNull(node, "node");
        Checks.checkNotNull(rootModuleConf, "rootModuleConf");

        this.data = data;
        this.node = node;
        this.parent = parent;
        this.rootModuleConf = rootModuleConf;
        this.parentConf = parentConf;
        this.usage = usage;

        this.data.register(this);
    }

    public IvyNode getNode() {
        return node;
    }

    /**
     * @return Returns the configuration requested by the parent
     */
    public String getRequestedConf() {
        return requestedConf;
    }

    public void setRequestedConf(String requestedConf) {
        this.requestedConf = requestedConf;
    }

    public VisitNode getParent() {
        return parent;
    }

    public VisitNode getRoot() {
        if (root == null) {
            root = computeRoot();
        }
        return root;
    }

    /**
     * Get an ordered collection with the nodes from the root to this node
     *
     * @return Collection&lt;VisitNode&gt;
     */
    public Collection<VisitNode> getPath() {
        if (path == null) {
            path = computePath();
        }
        return path;
    }

    private Collection<VisitNode> computePath() {
        if (parent != null) {
            Collection<VisitNode> p = new LinkedHashSet<>(parent.getPath());
            p.add(this);
            return p;
        } else {
            return Collections.singletonList(this);
        }
    }

    private VisitNode computeRoot() {
        if (node.isRoot()) {
            return this;
        } else if (parent != null) {
            return parent.getRoot();
        } else {
            return null;
        }
    }

    public String getParentConf() {
        return parentConf;
    }

    public void setParentConf(String parentConf) {
        this.parentConf = parentConf;
    }

    public String getRootModuleConf() {
        return rootModuleConf;
    }

    public static VisitNode getRoot(VisitNode parent) {
        VisitNode root = parent;
        Collection<VisitNode> path = new HashSet<>();
        path.add(root);
        while (root.getParent() != null && !root.getNode().isRoot()) {
            if (path.contains(root.getParent())) {
                return root;
            }
            root = root.getParent();
            path.add(root);
        }
        return root;
    }

    /**
     * Returns true if the current dependency descriptor is transitive and the parent configuration
     * is transitive. Otherwise returns false.
     *
     * @return true if current node is transitive and the parent configuration is transitive.
     */
    public boolean isTransitive() {
        if (node.isRoot()) {
            // the root node is always considered transitive!
            return true;
        }

        if (!data.isTransitive()) {
            return false;
        }

        if (!isParentConfTransitive()) {
            return false;
        }

        DependencyDescriptor dd = node.getDependencyDescriptor(getParentNode());
        return (dd != null) && dd.isTransitive()
                || node.hasAnyMergedUsageWithTransitiveDependency(rootModuleConf);

    }

    /**
     * Checks if the current node's parent configuration is transitive.
     *
     * @return true if the node's parent configuration is transitive
     */
    protected boolean isParentConfTransitive() {
        String conf = getParent().getRequestedConf();
        if (conf == null) {
            return true;
        }
        Configuration parentConf = getParentNode().getConfiguration(conf);
        return parentConf.isTransitive();

    }

    /**
     * Returns the 'real' node currently visited. 'Real' means that if we are visiting a node
     * created originally with only a version constraint, and if this version constraint has been
     * resolved to an existing node in the graph, we will return the existing node, and not the one
     * originally used which is about to be discarded, since it's not possible to have in the graph
     * two nodes for the same ModuleRevisionId
     *
     * @return the 'real' node currently visited.
     */
    public IvyNode getRealNode() {
        IvyNode node = this.node.getRealNode();
        if (node != null) {
            return node;
        } else {
            return this.node;
        }
    }

    /**
     * Ask to the current visited node to use a real node only, if one exist. See getRealNode for
     * details about what a 'real' node is.
     */
    public void useRealNode() {
        if (parent != null) { // use real node make sense only for non root module
            IvyNode node = data.getNode(this.node.getId());
            if (node != null && node != this.node) {
                this.node = node;
            }
        }
    }

    public boolean loadData(String conf, boolean shouldBePublic) {
        boolean loaded = node.loadData(rootModuleConf, getParentNode(), parentConf, conf,
            shouldBePublic, getUsage());
        if (loaded) {
            useRealNode();

            // if the loaded revision is different from original one
            // we now register this node on the new resolved id
            // this includes two cases:
            // - the id refers to a dynamic revision, which has been resolved by loadData
            // - the loaded module descriptor has extra attributes in his info tag which are not
            // used when declaring the dependency
            if (data.getNode(node.getResolvedId()) == null
                    || !data.getNode(node.getResolvedId()).getId().equals(node.getResolvedId())) {
                data.register(node.getResolvedId(), this);
            }
        }

        return loaded;
    }

    public Collection<VisitNode> getDependencies(String conf) {
        Collection<IvyNode> deps = node.getDependencies(rootModuleConf, conf, requestedConf);
        Collection<VisitNode> ret = new ArrayList<>(deps.size());
        for (IvyNode depNode : deps) {
            ret.add(traverseChild(conf, depNode));
        }
        return ret;
    }

    /**
     * Returns a VisitNode for the given node. The given node must be a representation of the same
     * module (usually in another revision) as the one visited by this node.
     *
     * @param node
     *            the node to visit
     * @return a VisitNode for the given node
     */
    VisitNode gotoNode(IvyNode node) {
        if (!getModuleId().equals(node.getModuleId())) {
            throw new IllegalArgumentException(
                    "You can't use gotoNode for a node which does not represent the same Module "
                            + "as the one represented by this node.\nCurrent node module id="
                            + getModuleId() + " Given node module id=" + node.getModuleId());
        }
        VisitData visitData = data.getVisitData(node.getId());
        if (visitData != null) {
            List<VisitNode> visitNodes = visitData.getVisitNodes(rootModuleConf);
            for (VisitNode vnode : visitNodes) {
                if ((parent == null && vnode.getParent() == null)
                        || (parent != null && parent.getId().equals(vnode.getParent().getId()))) {
                    vnode.parentConf = parentConf;
                    vnode.usage = getUsage();
                    return vnode;
                }
            }
        }
        // the node has not yet been visited from the current parent, we create a new visit node
        return traverse(parent, parentConf, node, getUsage());
    }

    private IvyNodeUsage getUsage() {
        return usage == null ? node.getMainUsage() : usage;
    }

    private VisitNode traverseChild(String parentConf, IvyNode child) {
        VisitNode parent = this;
        return traverse(parent, parentConf, child, null);
    }

    private VisitNode traverse(VisitNode parent, String parentConf, IvyNode node, IvyNodeUsage usage) {
        if (getPath().contains(node)) {
            IvyContext.getContext().getCircularDependencyStrategy()
                    .handleCircularDependency(toMrids(getPath(), node.getId()));
            // we do not use the new parent, but the first one, to always be able to go up to the
            // root
            // parent = getVisitNode(depNode).getParent();
        }
        return new VisitNode(data, node, parent, rootModuleConf, parentConf, usage);
    }

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

    public ModuleRevisionId getResolvedId() {
        return node.getResolvedId();
    }

    public void updateConfsToFetch(Collection<String> confs) {
        node.updateConfsToFetch(confs);
    }

    public ModuleRevisionId getId() {
        return node.getId();
    }

    public boolean isEvicted() {
        return node.isEvicted(rootModuleConf);
    }

    public String[] getRealConfs(String conf) {
        return node.getRealConfs(conf);
    }

    public boolean hasProblem() {
        return node.hasProblem();
    }

    public Configuration getConfiguration(String conf) {
        return node.getConfiguration(conf);
    }

    public EvictionData getEvictedData() {
        return node.getEvictedData(rootModuleConf);
    }

    public DependencyDescriptor getDependencyDescriptor() {
        return node.getDependencyDescriptor(getParentNode());
    }

    private IvyNode getParentNode() {
        return parent == null ? null : parent.getNode();
    }

    /**
     * Returns true if this node can already be found in the path
     *
     * @return boolean
     */
    public boolean isCircular() {
        if (isCircular == null) {
            if (parent != null) {
                isCircular = Boolean.FALSE; // assume it's false, and see if it isn't by checking
                // the parent path
                for (VisitNode ancestor : parent.getPath()) {
                    if (getId().getModuleId().equals(ancestor.getId().getModuleId())) {
                        isCircular = Boolean.TRUE;
                        break;
                    }
                }
            } else {
                isCircular = Boolean.FALSE;
            }
        }
        return isCircular;
    }

    public String[] getConfsToFetch() {
        return node.getConfsToFetch();
    }

    public String[] getRequiredConfigurations(VisitNode in, String inConf) {
        return node.getRequiredConfigurations(in.getNode(), inConf);
    }

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

    public Collection<ModuleRevisionId> getResolvedRevisions(ModuleId mid) {
        return node.getResolvedRevisions(mid, rootModuleConf);
    }

    public void markEvicted(EvictionData evictionData) {
        node.markEvicted(evictionData);
    }

    public String[] getRequiredConfigurations() {
        return node.getRequiredConfigurations();
    }

    /**
     * Marks the current node as evicted by the the given selected IvyNodes, in the given parent and
     * root module configuration, with the given {@link ConflictManager}
     *
     * @param parent
     *            the VisitNode in which eviction has been made
     * @param conflictMgr
     *            the conflict manager responsible for the eviction
     * @param selected
     *            a Collection of {@link IvyNode} which have been selected
     */
    public void markEvicted(VisitNode parent, ConflictManager conflictMgr,
            Collection<IvyNode> selected) {
        node.markEvicted(rootModuleConf, parent.getNode(), conflictMgr, selected);
    }

    public ModuleDescriptor getDescriptor() {
        return node.getDescriptor();
    }

    public EvictionData getEvictionDataInRoot(String rootModuleConf, VisitNode ancestor) {
        return node.getEvictionDataInRoot(rootModuleConf, ancestor.getNode());
    }

    public Collection<ModuleRevisionId> getEvictedRevisions(ModuleId moduleId) {
        return node.getEvictedRevisions(moduleId, rootModuleConf);
    }

    // public void setRootModuleConf(String rootModuleConf) {
    // if (rootModuleConf != null && !rootModuleConf.equals(rootModuleConf)) {
    // _confsToFetch.clear(); // we change of root module conf => we discard all confs to fetch
    // }
    // if (rootModuleConf != null && rootModuleConf.equals(rootModuleConf)) {
    // _selectedDeps.put(new ModuleIdConf(_id.getModuleId(), rootModuleConf),
    // Collections.singleton(this));
    // }
    // rootModuleConf = rootModuleConf;
    // }

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

    public boolean isConfRequiredByMergedUsageOnly(String conf) {
        return node.isConfRequiredByMergedUsageOnly(rootModuleConf, conf);
    }

}
