/*
 *  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.report;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ivy.core.cache.ResolutionCacheManager;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.resolve.IvyNode;
import org.apache.ivy.core.resolve.ResolveOptions;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.plugins.report.ReportOutputter;
import org.apache.ivy.util.filter.Filter;

/**
 * Represents a whole resolution report for a module
 */
public class ResolveReport {
    private ModuleDescriptor md;

    private Map<String, ConfigurationResolveReport> confReports = new LinkedHashMap<>();

    private List<String> problemMessages = new ArrayList<>();

    /**
     * the list of all dependencies resolved, ordered from the more dependent to the less dependent
     */
    private List<IvyNode> dependencies = new ArrayList<>();

    private List<Artifact> artifacts = new ArrayList<>();

    private long resolveTime;

    private long downloadTime;

    private String resolveId;

    private long downloadSize;

    public ResolveReport(ModuleDescriptor md) {
        this(md, ResolveOptions.getDefaultResolveId(md));
    }

    public ResolveReport(ModuleDescriptor md, String resolveId) {
        this.md = md;
        this.resolveId = resolveId;
    }

    public void addReport(String conf, ConfigurationResolveReport report) {
        confReports.put(conf, report);
    }

    public ConfigurationResolveReport getConfigurationReport(String conf) {
        return confReports.get(conf);
    }

    public String[] getConfigurations() {
        return confReports.keySet().toArray(new String[confReports.size()]);
    }

    public boolean hasError() {
        for (ConfigurationResolveReport report : confReports.values()) {
            if (report.hasError()) {
                return true;
            }
        }
        return false;
    }

    public void output(ReportOutputter[] outputters, ResolutionCacheManager cacheMgr,
            ResolveOptions options) throws IOException {
        for (ReportOutputter outputter : outputters) {
            outputter.output(this, cacheMgr, options);
        }
    }

    public ModuleDescriptor getModuleDescriptor() {
        return md;
    }

    public IvyNode[] getEvictedNodes() {
        Collection<IvyNode> all = new LinkedHashSet<>();
        for (ConfigurationResolveReport report : confReports.values()) {
            all.addAll(Arrays.asList(report.getEvictedNodes()));
        }
        return all.toArray(new IvyNode[all.size()]);
    }

    public IvyNode[] getUnresolvedDependencies() {
        Collection<IvyNode> all = new LinkedHashSet<>();
        for (ConfigurationResolveReport report : confReports.values()) {
            all.addAll(Arrays.asList(report.getUnresolvedDependencies()));
        }
        return all.toArray(new IvyNode[all.size()]);
    }

    /**
     * Get every report on the download requests.
     *
     * @return the list of reports, never <code>null</code>
     */
    public ArtifactDownloadReport[] getFailedArtifactsReports() {
        return ConfigurationResolveReport.filterOutMergedArtifacts(getArtifactsReports(
            DownloadStatus.FAILED, true));
    }

    /**
     * Get every report on the download requests.
     *
     * @return the list of reports, never <code>null</code>
     */
    public ArtifactDownloadReport[] getAllArtifactsReports() {
        return getArtifactsReports(null, true);
    }

    /**
     * Get the report on the download requests. The list of download report can be restricted to a
     * specific download status, and also remove the download report for the evicted modules.
     *
     * @param downloadStatus
     *            the status of download to retrieve. Set it to <code>null</code> for no restriction
     *            on the download status
     * @param withEvicted
     *            set it to <code>true</code> if the report for the evicted modules have to be
     *            retrieved, <code>false</code> to exclude reports from modules evicted in all
     *            configurations.
     * @return the list of reports, never <code>null</code>
     * @see ConfigurationResolveReport#getArtifactsReports(DownloadStatus, boolean)
     */
    public ArtifactDownloadReport[] getArtifactsReports(DownloadStatus downloadStatus,
            boolean withEvicted) {
        Collection<ArtifactDownloadReport> all = new LinkedHashSet<>();
        for (ConfigurationResolveReport report : confReports.values()) {
            ArtifactDownloadReport[] reports = report.getArtifactsReports(downloadStatus,
                withEvicted);
            all.addAll(Arrays.asList(reports));
        }
        return all.toArray(new ArtifactDownloadReport[all.size()]);
    }

    public ArtifactDownloadReport[] getArtifactsReports(ModuleRevisionId mrid) {
        Collection<ArtifactDownloadReport> all = new LinkedHashSet<>();
        for (ConfigurationResolveReport report : confReports.values()) {
            all.addAll(Arrays.asList(report.getDownloadReports(mrid)));
        }
        return all.toArray(new ArtifactDownloadReport[all.size()]);
    }

    public void checkIfChanged() {
        for (ConfigurationResolveReport report : confReports.values()) {
            report.checkIfChanged();
        }
    }

    /**
     * Can only be called if checkIfChanged has been called
     *
     * @return boolean
     */
    public boolean hasChanged() {
        for (ConfigurationResolveReport report : confReports.values()) {
            if (report.hasChanged()) {
                return true;
            }
        }
        return false;
    }

    public void setProblemMessages(List<String> problems) {
        problemMessages = problems;
    }

    public List<String> getProblemMessages() {
        return problemMessages;
    }

    public List<String> getAllProblemMessages() {
        List<String> ret = new ArrayList<>(problemMessages);
        for (ConfigurationResolveReport r : confReports.values()) {
            for (IvyNode unresolved : r.getUnresolvedDependencies()) {
                String errMsg = unresolved.getProblemMessage();
                if (errMsg.isEmpty()) {
                    ret.add("unresolved dependency: " + unresolved.getId());
                } else {
                    ret.add("unresolved dependency: " + unresolved.getId() + ": " + errMsg);
                }
            }
            for (ArtifactDownloadReport adr : r.getFailedArtifactsReports()) {
                ret.add("download failed: " + adr.getArtifact());
            }
        }
        return ret;
    }

    public void setDependencies(List<IvyNode> dependencies, Filter<Artifact> artifactFilter) {
        this.dependencies = dependencies;
        // collect list of artifacts
        artifacts = new ArrayList<>();
        for (IvyNode dependency : dependencies) {
            if (!dependency.isCompletelyEvicted() && !dependency.hasProblem()) {
                artifacts.addAll(Arrays.asList(dependency.getSelectedArtifacts(artifactFilter)));
            }
            // update the configurations reports with the dependencies
            // these reports will be completed later with download information, if any
            for (String dconf : dependency.getRootModuleConfigurations()) {
                ConfigurationResolveReport configurationReport = getConfigurationReport(dconf);
                if (configurationReport != null) {
                    configurationReport.addDependency(dependency);
                }
            }
        }
    }

    /**
     * Returns the list of all dependencies concerned by this report as a List of IvyNode ordered
     * from the more dependent to the least one
     *
     * @return The list of all dependencies.
     */
    public List<IvyNode> getDependencies() {
        return dependencies;
    }

    /**
     * Returns the list of all artifacts which should be downloaded per this resolve To know if the
     * artifact have actually been downloaded use information found in ConfigurationResolveReport.
     *
     * @return The list of all artifacts.
     */
    public List<Artifact> getArtifacts() {
        return artifacts;
    }

    /**
     * gives all the modules ids concerned by this report, from the most dependent to the least one
     *
     * @return a list of ModuleId
     */
    public List<ModuleId> getModuleIds() {
        List<ModuleId> ret = new ArrayList<>();
        List<IvyNode> sortedDependencies = new ArrayList<>(dependencies);
        for (IvyNode dependency : sortedDependencies) {
            ModuleId mid = dependency.getResolvedId().getModuleId();
            if (!ret.contains(mid)) {
                ret.add(mid);
            }
        }
        return ret;
    }

    public void setResolveTime(long elapsedTime) {
        resolveTime = elapsedTime;
    }

    public long getResolveTime() {
        return resolveTime;
    }

    public void setDownloadTime(long elapsedTime) {
        downloadTime = elapsedTime;
    }

    public long getDownloadTime() {
        return downloadTime;
    }

    public void setDownloadSize(long size) {
        this.downloadSize = size;
    }

    /**
     * The total size of downloaded artifacts, in bytes.
     * <p>
     * This only includes artifacts actually downloaded to cache (DownloadStatus.SUCCESSFUL), and
     * not artifacts already in cache or used at their original location.
     * </p>
     *
     * @return The total size of downloaded artifacts, in bytes.
     */
    public long getDownloadSize() {
        return downloadSize;
    }

    public String getResolveId() {
        return resolveId;
    }

    /**
     * Get every configuration which extends the specified one. The returned list also includes the
     * specified one.
     *
     * @param extended String
     * @return Set of String
     */
    @SuppressWarnings("unused")
    private Set<String> getExtendingConfs(String extended) {
        Set<String> extendingConfs = new HashSet<>();
        extendingConfs.add(extended);
        for (String conf : md.getConfigurationsNames()) {
            gatherExtendingConfs(extendingConfs, conf, extended);
        }
        return extendingConfs;
    }

    private boolean gatherExtendingConfs(Set<String> extendingConfs, String conf, String extended) {
        if (extendingConfs.contains(conf)) {
            return true;
        }
        String[] exts = md.getConfiguration(conf).getExtends();
        if (exts == null || exts.length == 0) {
            return false;
        }
        for (String ext : exts) {
            if (extendingConfs.contains(ext)) {
                extendingConfs.add(conf);
                return true;
            }
            if (ext.equals(extended)) {
                extendingConfs.add(conf);
                return true;
            }
            if (gatherExtendingConfs(extendingConfs, ext, extended)) {
                extendingConfs.add(conf);
                return true;
            }
        }
        return false;
    }

    public ModuleDescriptor toFixedModuleDescriptor(IvySettings settings, List<ModuleId> midToKeep) {
        DefaultModuleDescriptor fixedmd = new DefaultModuleDescriptor(md.getModuleRevisionId(),
                md.getStatus(), new Date());

        // copy namespaces
        for (Map.Entry<String, String> ns : md.getExtraAttributesNamespaces().entrySet()) {
            fixedmd.addExtraAttributeNamespace(ns.getKey(), ns.getValue());
        }

        // copy info
        fixedmd.setDescription(md.getDescription());
        fixedmd.setHomePage(md.getHomePage());
        fixedmd.getExtraInfos().addAll(md.getExtraInfos());

        // copy configurations
        List<String> resolvedConfs = Arrays.asList(getConfigurations());
        for (String conf : resolvedConfs) {
            fixedmd.addConfiguration(new Configuration(conf));
        }

        // copy artifacts
        for (String conf : resolvedConfs) {
            for (Artifact a : md.getArtifacts(conf)) {
                fixedmd.addArtifact(conf, a);
            }
        }

        // add resolved dependencies
        for (IvyNode dep : dependencies) {
            ModuleRevisionId depMrid;
            boolean force;
            if (midToKeep != null && midToKeep.contains(dep.getModuleId())) {
                depMrid = dep.getId();
                force = false;
            } else {
                depMrid = dep.getResolvedId();
                force = true;
            }
            DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(fixedmd, depMrid,
                    force, false, false);
            boolean evicted = true;
            for (String rootConf : dep.getRootModuleConfigurations()) {
                if (dep.isEvicted(rootConf)) {
                    continue;
                }
                evicted = false;
                for (String targetConf : dep.getConfigurations(rootConf)) {
                    dd.addDependencyConfiguration(rootConf, targetConf);
                }
            }
            if (!evicted) {
                fixedmd.addDependency(dd);
            }
        }

        return fixedmd;
    }
}
