/*
 *  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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.plugins.conflict.ConflictManager;

public class IvyNodeEviction {
    /**
     * This class contains data about the eviction of an {@link IvyNode}.
     */
    public static class EvictionData {
        /**
         * Can be null in case of transitive eviction.
         */
        private IvyNode parent;

        /**
         * Can be null in case of transitive eviction.
         */
        private ConflictManager conflictManager;

        /**
         * Can be null in case of transitive eviction.
         */
        private Collection<IvyNode> selected;

        private String rootModuleConf;

        private String detail;

        /**
         * Creates a new object containing the eviction data of an {@link IvyNode}.
         *
         * @param rootModuleConf
         *            the root module configuration
         * @param parent
         *            the parent node (or <tt>null</tt> in case of transitive eviction)
         * @param conflictManager
         *            the conflict manager which evicted the node (or <tt>null</tt> in case of
         *            transitive eviction)
         * @param selected
         *            a collection of {@link IvyNode}s which evict the evicted node (or
         *            <tt>null</tt> in case of transitive eviction)
         */
        public EvictionData(String rootModuleConf, IvyNode parent, ConflictManager conflictManager,
                Collection<IvyNode> selected) {
            this(rootModuleConf, parent, conflictManager, selected, null);
        }

        /**
         * Creates a new object containing the eviction data of an {@link IvyNode}.
         *
         * @param rootModuleConf
         *            the root module configuration
         * @param parent
         *            the parent node (or <tt>null</tt> in case of transitive eviction)
         * @param conflictManager
         *            the conflict manager which evicted the node (or <tt>null</tt> in case of
         *            transitive eviction)
         * @param selected
         *            a collection of {@link IvyNode}s which evict the evicted node (or
         *            <tt>null</tt> in case of transitive eviction)
         * @param detail
         *            a String detailing the reason why the node was evicted
         */
        public EvictionData(String rootModuleConf, IvyNode parent, ConflictManager conflictManager,
                Collection<IvyNode> selected, String detail) {
            this.rootModuleConf = rootModuleConf;
            this.parent = parent;
            this.conflictManager = conflictManager;
            this.selected = selected;
            this.detail = detail;
        }

        @Override
        public String toString() {
            if (selected != null) {
                return selected + " in " + parent + (detail == null ? "" : " " + detail) + " ("
                        + conflictManager + ") [" + rootModuleConf + "]";
            } else {
                return "transitively [" + rootModuleConf + "]";
            }
        }

        public ConflictManager getConflictManager() {
            return conflictManager;
        }

        public IvyNode getParent() {
            return parent;
        }

        public Collection<IvyNode> getSelected() {
            return selected;
        }

        public String getRootModuleConf() {
            return rootModuleConf;
        }

        public boolean isTransitivelyEvicted() {
            return parent == null;
        }

        public String getDetail() {
            return detail;
        }
    }

    private static final class ModuleIdConf {
        private ModuleId moduleId;

        private String conf;

        public ModuleIdConf(ModuleId mid, String conf) {
            if (mid == null) {
                throw new NullPointerException("mid cannot be null");
            }
            if (conf == null) {
                throw new NullPointerException("conf cannot be null");
            }
            moduleId = mid;
            this.conf = conf;
        }

        public final String getConf() {
            return conf;
        }

        public final ModuleId getModuleId() {
            return moduleId;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof ModuleIdConf
                    && getModuleId().equals(((ModuleIdConf) obj).getModuleId())
                    && getConf().equals(((ModuleIdConf) obj).getConf());
        }

        @Override
        public int hashCode() {
            // CheckStyle:MagicNumber| OFF
            int hash = 33;
            hash += getModuleId().hashCode() * 17;
            hash += getConf().hashCode() * 17;
            // CheckStyle:MagicNumber| ON
            return hash;
        }
    }

    private IvyNode node;

    // map indicating for each dependency which node has been selected
    private Map<ModuleIdConf, Set<IvyNode>> selectedDeps = new HashMap<>();

    // map indicating for each dependency which nodes are in pending conflict (conflict detected but
    // not yet resolved)
    private Map<ModuleIdConf, Set<IvyNode>> pendingConflicts = new HashMap<>();

    // map indicating for each dependency which node has been evicted
    private Map<ModuleIdConf, Set<IvyNode>> evictedDeps = new HashMap<>();

    // map indicating for each dependency which revision has been evicted
    private Map<ModuleIdConf, Collection<ModuleRevisionId>> evictedRevs = new HashMap<>();

    // indicates if the node is evicted in each root module conf
    private Map<String, EvictionData> evicted = new HashMap<>();

    public IvyNodeEviction(IvyNode node) {
        if (node == null) {
            throw new NullPointerException("node must not be null");
        }
        this.node = node;
    }

    /**
     * @param mid ModuleId
     * @param rootModuleConf String
     * @return A copy of the set of resolved nodes (real nodes)
     */
    public Set<IvyNode> getResolvedNodes(ModuleId mid, String rootModuleConf) {
        Collection<IvyNode> resolved = selectedDeps.get(new ModuleIdConf(mid, rootModuleConf));
        Set<IvyNode> ret = new HashSet<>();
        if (resolved != null) {
            for (IvyNode node : resolved) {
                ret.add(node.getRealNode());
            }
        }
        return ret;
    }

    public Collection<ModuleRevisionId> getResolvedRevisions(ModuleId mid, String rootModuleConf) {
        Collection<IvyNode> resolved = selectedDeps.get(new ModuleIdConf(mid, rootModuleConf));
        if (resolved == null) {
            return new HashSet<>();
        } else {
            Collection<ModuleRevisionId> resolvedRevs = new HashSet<>();
            for (IvyNode node : resolved) {
                ModuleRevisionId resolvedId = node.getResolvedId();
                resolvedRevs.add(node.getId());
                resolvedRevs.add(resolvedId);

                // in case there are extra attributes on the resolved module we also add the
                // the module without these extra attributes (cfr. IVY-1236)
                if (!resolvedId.getExtraAttributes().isEmpty()) {
                    resolvedRevs.add(ModuleRevisionId.newInstance(resolvedId.getOrganisation(),
                        resolvedId.getName(), resolvedId.getBranch(), resolvedId.getRevision()));
                }
            }
            return resolvedRevs;
        }
    }

    public void setResolvedNodes(ModuleId moduleId, String rootModuleConf,
            Collection<IvyNode> resolved) {
        ModuleIdConf moduleIdConf = new ModuleIdConf(moduleId, rootModuleConf);
        selectedDeps.put(moduleIdConf, new HashSet<>(resolved));
    }

    public Collection<IvyNode> getEvictedNodes(ModuleId mid, String rootModuleConf) {
        Collection<IvyNode> resolved = evictedDeps.get(new ModuleIdConf(mid, rootModuleConf));
        Set<IvyNode> ret = new HashSet<>();
        if (resolved != null) {
            for (IvyNode node : resolved) {
                ret.add(node.getRealNode());
            }
        }
        return ret;
    }

    public Collection<ModuleRevisionId> getEvictedRevisions(ModuleId mid, String rootModuleConf) {
        Collection<ModuleRevisionId> evicted = evictedRevs
                .get(new ModuleIdConf(mid, rootModuleConf));
        if (evicted == null) {
            return new HashSet<>();
        } else {
            return new HashSet<>(evicted);
        }
    }

    public void setEvictedNodes(ModuleId moduleId, String rootModuleConf,
            Collection<IvyNode> evicted) {
        ModuleIdConf moduleIdConf = new ModuleIdConf(moduleId, rootModuleConf);
        evictedDeps.put(moduleIdConf, new HashSet<>(evicted));
        Collection<ModuleRevisionId> evictedRevs = new HashSet<>();
        for (IvyNode node : evicted) {
            evictedRevs.add(node.getId());
            evictedRevs.add(node.getResolvedId());
        }
        this.evictedRevs.put(moduleIdConf, evictedRevs);
    }

    public boolean isEvicted(String rootModuleConf) {
        cleanEvicted();
        if (node.isRoot()) {
            return false;
        }
        EvictionData evictedData = getEvictedData(rootModuleConf);
        if (evictedData == null) {
            return false;
        }
        IvyNode root = node.getRoot();
        ModuleId moduleId = node.getId().getModuleId();
        Collection<ModuleRevisionId> resolvedRevisions = root.getResolvedRevisions(moduleId,
            rootModuleConf);
        return !resolvedRevisions.contains(node.getResolvedId())
                || evictedData.isTransitivelyEvicted();
    }

    public boolean isCompletelyEvicted() {
        cleanEvicted();
        if (node.isRoot()) {
            return false;
        }
        for (String rootModuleConfiguration : node.getRootModuleConfigurations()) {
            if (!isEvicted(rootModuleConfiguration)) {
                return false;
            }
        }
        return true;
    }

    private void cleanEvicted() {
        // check if it was evicted by a node that we are now the real node for
        Iterator<String> iter = evicted.keySet().iterator();
        while (iter.hasNext()) {
            Collection<IvyNode> sel = evicted.get(iter.next()).getSelected();
            if (sel != null) {
                for (IvyNode n : sel) {
                    if (n.getRealNode().equals(node)) {
                        // yes, we are the real node for a selected one !
                        // we are no more evicted in this conf !
                        iter.remove();
                    }
                }
            }
        }
    }

    public void markEvicted(EvictionData evictionData) {
        evicted.put(evictionData.getRootModuleConf(), evictionData);
    }

    public EvictionData getEvictedData(String rootModuleConf) {
        cleanEvicted();
        return evicted.get(rootModuleConf);
    }

    public String[] getEvictedConfs() {
        cleanEvicted();
        return evicted.keySet().toArray(new String[evicted.keySet().size()]);
    }

    /**
     * Returns null if this node has only be evicted transitively, or the the collection of selected
     * nodes if it has been evicted by other selected nodes
     *
     * @return Collection&lt;IvyNode&gt;
     */
    public Collection<IvyNode> getAllEvictingNodes() {
        Collection<IvyNode> allEvictingNodes = null;
        for (EvictionData ed : evicted.values()) {
            Collection<IvyNode> selected = ed.getSelected();
            if (selected != null) {
                if (allEvictingNodes == null) {
                    allEvictingNodes = new HashSet<>();
                }
                allEvictingNodes.addAll(selected);
            }
        }
        return allEvictingNodes;
    }

    public Collection<String> getAllEvictingNodesDetails() {
        Collection<String> ret = null;
        for (EvictionData ed : evicted.values()) {
            Collection<IvyNode> selected = ed.getSelected();
            if (selected != null) {
                if (ret == null) {
                    ret = new HashSet<>();
                }
                if (selected.size() == 1) {
                    ret.add(selected.iterator().next()
                            + (ed.getDetail() == null ? "" : " " + ed.getDetail()));
                } else if (selected.size() > 1) {
                    ret.add(selected + (ed.getDetail() == null ? "" : " " + ed.getDetail()));
                }
            }
        }
        return ret;
    }

    public Collection<ConflictManager> getAllEvictingConflictManagers() {
        Collection<ConflictManager> ret = new HashSet<>();
        for (EvictionData ed : evicted.values()) {
            ret.add(ed.getConflictManager());
        }
        return ret;
    }

    /**
     * Returns the eviction data for this node if it has been previously evicted in the root, null
     * otherwise (if it hasn't been evicted in root) for the given rootModuleConf. Note that this
     * method only works if conflict resolution has already be done in all the ancestors.
     *
     * @param rootModuleConf ditto
     * @param ancestor IvyNode
     * @return EvictionData
     */
    public EvictionData getEvictionDataInRoot(String rootModuleConf, IvyNode ancestor) {
        Collection<IvyNode> selectedNodes = node.getRoot().getResolvedNodes(node.getModuleId(),
            rootModuleConf);
        for (IvyNode node : selectedNodes) {
            if (node.getResolvedId().equals(this.node.getResolvedId())) {
                // the node is part of the selected ones for the root: no eviction data to return
                return null;
            }
        }
        // we didn't find this mrid in the selected ones for the root: it has been previously
        // evicted
        return new EvictionData(rootModuleConf, ancestor, node.getRoot().getConflictManager(
            node.getModuleId()), selectedNodes);
    }

    public Collection<IvyNode> getPendingConflicts(String rootModuleConf, ModuleId mid) {
        Collection<IvyNode> resolved = pendingConflicts.get(new ModuleIdConf(mid, rootModuleConf));
        Set<IvyNode> ret = new HashSet<>();
        if (resolved != null) {
            for (IvyNode node : resolved) {
                ret.add(node.getRealNode());
            }
        }
        return ret;
    }

    public void setPendingConflicts(ModuleId moduleId, String rootModuleConf,
            Collection<IvyNode> conflicts) {
        ModuleIdConf moduleIdConf = new ModuleIdConf(moduleId, rootModuleConf);
        pendingConflicts.put(moduleIdConf, new HashSet<>(conflicts));
    }

}
