/*
 *  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.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.ivy.core.event.EventManager;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.report.ConfigurationResolveReport;
import org.apache.ivy.util.Message;

public class ResolveData {
    private ResolveEngine engine;

    // shared map of all visit data
    private Map<ModuleRevisionId, VisitData> visitData;

    private ConfigurationResolveReport report;

    private ResolveOptions options;

    private VisitNode currentVisitNode = null;

    private ResolvedModuleRevision currentResolvedModuleRevision;

    public ResolveData(ResolveData data, boolean validate) {
        this(data.engine, new ResolveOptions(data.options).setValidate(validate), data.report,
                data.visitData);
        setCurrentVisitNode(data.currentVisitNode);
        setCurrentResolvedModuleRevision(data.currentResolvedModuleRevision);
    }

    public ResolveData(ResolveEngine engine, ResolveOptions options) {
        this(engine, options, null, new LinkedHashMap<ModuleRevisionId, VisitData>());
    }

    public ResolveData(ResolveEngine engine, ResolveOptions options,
            ConfigurationResolveReport report) {
        this(engine, options, report, new LinkedHashMap<ModuleRevisionId, VisitData>());
    }

    public ResolveData(ResolveEngine engine, ResolveOptions options,
            ConfigurationResolveReport report, Map<ModuleRevisionId, VisitData> visitData) {
        this.engine = engine;
        this.report = report;
        this.visitData = visitData;
        this.options = options;
    }

    public ConfigurationResolveReport getReport() {
        return report;
    }

    public IvyNode getNode(ModuleRevisionId mrid) {
        VisitData visitData = getVisitData(mrid);
        return visitData == null ? null : visitData.getNode();
    }

    public Collection<IvyNode> getNodes() {
        Collection<IvyNode> nodes = new ArrayList<>();
        for (VisitData vdata : visitData.values()) {
            nodes.add(vdata.getNode());
        }
        return nodes;
    }

    public Collection<ModuleRevisionId> getNodeIds() {
        return visitData.keySet();
    }

    public VisitData getVisitData(ModuleRevisionId mrid) {
        VisitData result = visitData.get(mrid);

        if (result == null) {
            // search again, now ignore the missing extra attributes
            for (Map.Entry<ModuleRevisionId, VisitData> entry : visitData.entrySet()) {
                ModuleRevisionId current = entry.getKey();

                if (isSubMap(mrid.getAttributes(), current.getAttributes())) {
                    result = entry.getValue();
                    break;
                }
            }
        }

        return result;
    }

    /**
     * Checks whether one map is a sub-map of the other.
     */
    private static <K, V> boolean isSubMap(Map<K, V> map1, Map<K, V> map2) {
        int map1Size = map1.size();
        int map2Size = map2.size();

        if (map1Size == map2Size) {
            return map1.equals(map2);
        }

        Map<K, V> smallest = map1Size < map2Size ? map1 : map2;
        Map<K, V> largest = map1Size < map2Size ? map2 : map1;

        for (Map.Entry<K, V> entry : smallest.entrySet()) {

            if (!largest.containsKey(entry.getKey())) {
                return false;
            }

            Object map1Value = smallest.get(entry.getKey());
            Object map2Value = largest.get(entry.getKey());
            if (!isEqual(map1Value, map2Value)) {
                return false;
            }
        }

        return true;
    }

    private static boolean isEqual(Object obj1, Object obj2) {
        return obj1 == obj2 || obj1 != null && obj2 != null && obj1.equals(obj2);
    }

    /**
     * Returns the VisitNode currently visited, or <code>null</code> if there is no node currently
     * visited in this context.
     *
     * @return the VisitNode currently visited
     */
    public VisitNode getCurrentVisitNode() {
        return currentVisitNode;
    }

    /**
     * Sets the currently visited node. WARNING: This should only be called by Ivy core
     * ResolveEngine!
     *
     * @param currentVisitNode VisitNode
     */
    void setCurrentVisitNode(VisitNode currentVisitNode) {
        this.currentVisitNode = currentVisitNode;
    }

    public void register(VisitNode node) {
        register(node.getId(), node);
    }

    public void register(ModuleRevisionId mrid, VisitNode node) {
        VisitData visitData = getVisitData(mrid);
        if (visitData == null) {
            visitData = new VisitData(node.getNode());
            visitData.addVisitNode(node);
            this.visitData.put(mrid, visitData);
        } else {
            visitData.setNode(node.getNode());
            visitData.addVisitNode(node);
        }
    }

    /**
     * Updates the visit data currently associated with the given mrid with the given node and the
     * visit nodes of the old visitData for the given rootModuleConf
     *
     * @param mrid
     *            the module revision id for which the update should be done
     * @param node
     *            the IvyNode to associate with the visit data to update
     * @param rootModuleConf
     *            the root module configuration in which the update is made
     */
    void replaceNode(ModuleRevisionId mrid, IvyNode node, String rootModuleConf) {
        VisitData visitData = getVisitData(mrid);
        if (visitData == null) {
            throw new IllegalArgumentException("impossible to replace node for id " + mrid
                    + ". No registered node found.");
        }
        VisitData keptVisitData = getVisitData(node.getId());
        if (keptVisitData == null) {
            throw new IllegalArgumentException("impossible to replace node with " + node
                    + ". No registered node found for " + node.getId() + ".");
        }
        // replace visit data in Map (discards old one)
        this.visitData.put(mrid, keptVisitData);
        // update visit data with discarded visit nodes
        keptVisitData.addVisitNodes(rootModuleConf, visitData.getVisitNodes(rootModuleConf));

        report.updateDependency(mrid, node);
    }

    public void setReport(ConfigurationResolveReport report) {
        this.report = report;
    }

    public Date getDate() {
        return options.getDate();
    }

    public boolean isValidate() {
        return options.isValidate();
    }

    public boolean isTransitive() {
        return options.isTransitive();
    }

    public ResolveOptions getOptions() {
        return options;
    }

    public ResolveEngineSettings getSettings() {
        return engine.getSettings();
    }

    public EventManager getEventManager() {
        return engine.getEventManager();
    }

    public ResolveEngine getEngine() {
        return engine;
    }

    void blacklist(IvyNode node) {
        Iterator<Map.Entry<ModuleRevisionId, VisitData>> iter = visitData.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<ModuleRevisionId, VisitData> entry = iter.next();
            if (entry.getValue().getNode() == node && !node.getResolvedId().equals(entry.getKey())) {
                // this visit data was associated with the blacklisted node,
                // we discard this association
                iter.remove();
            }
        }
    }

    public boolean isBlacklisted(String rootModuleConf, ModuleRevisionId mrid) {
        IvyNode node = getNode(mrid);

        /*
        if (node == null) {
            // search again, now ignore the extra attributes
            // TODO: maybe we should search the node that has at least the same attributes as mrid
            for (Map.Entry<ModuleRevisionId, VisitData> entry : visitData.entrySet()) {
                ModuleRevisionId current = entry.getKey();
                if (current.getModuleId().equals(mrid.getModuleId())
                        && current.getRevision().equals(mrid.getRevision())) {
                    VisitData data = entry.getValue();
                    node = data.getNode();
                    break;
                }
            }
        }
        */

        return node != null && node.isBlacklisted(rootModuleConf);
    }

    public DependencyDescriptor mediate(DependencyDescriptor dd) {
        DependencyDescriptor originalDD = dd;
        dd = getEngine().mediate(dd, getOptions());

        VisitNode current = getCurrentVisitNode();
        if (current != null) {
            // mediating dd through dependers stack
            List<VisitNode> dependers = new ArrayList<>(current.getPath());
            // the returned path contains the currently visited node, we are only interested in
            // the dependers, so we remove the currently visited node from the end
            dependers.remove(dependers.size() - 1);
            // we want to apply mediation going up in the dependers stack, not the opposite
            Collections.reverse(dependers);
            for (VisitNode n : dependers) {
                ModuleDescriptor md = n.getDescriptor();
                if (md != null) {
                    dd = md.mediate(dd);
                }
            }
        }

        if (originalDD != dd) {
            Message.verbose("dependency descriptor has been mediated: " + originalDD + " => " + dd);
        }

        return dd;
    }

    /**
     * Sets the last {@link ResolvedModuleRevision} which has been currently resolved.
     * <p>
     * This can be used especially in dependency resolvers, to know if another dependency resolver
     * has already resolved the requested dependency, to take a decision if the resolver should try
     * to resolve it by itself or not. Indeed, the dependency resolver is responsible for taking
     * this decision, even when included in a chain. The chain responsibility is only to set this
     * current resolved module revision to enable the resolver to take the decision.
     * </p>
     *
     * @param mr
     *            the last {@link ResolvedModuleRevision} which has been currently resolved.
     */
    public void setCurrentResolvedModuleRevision(ResolvedModuleRevision mr) {
        this.currentResolvedModuleRevision = mr;
    }

    /**
     * Returns the last {@link ResolvedModuleRevision} which has been currently resolved.
     * <p>
     * It can be <code>null</code>.
     * </p>
     *
     * @return the last {@link ResolvedModuleRevision} which has been currently resolved.
     */
    public ResolvedModuleRevision getCurrentResolvedModuleRevision() {
        return currentResolvedModuleRevision;
    }
}
