/*
 *  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.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
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.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.ResolveEngine;
import org.apache.ivy.core.resolve.ResolveOptions;
import org.apache.ivy.core.sort.SortOptions;
import org.apache.ivy.plugins.report.XmlReportParser;
import org.apache.ivy.util.Message;

/**
 * Represents a whole resolution report for a module but for a specific configuration
 */
public class ConfigurationResolveReport {

    private final ModuleDescriptor md;

    private final String conf;

    private final Date date;

    private final ResolveOptions options;

    private Map<IvyNode, List<ArtifactDownloadReport>> dependencyReports = new LinkedHashMap<>();

    private Map<ModuleRevisionId, IvyNode> dependencies = new LinkedHashMap<>();

    private final ResolveEngine resolveEngine;

    private Map<ModuleId, Collection<IvyNode>> modulesIdsMap = new LinkedHashMap<>();

    private List<ModuleId> modulesIds;

    private Boolean hasChanged = null;

    public ConfigurationResolveReport(ResolveEngine resolveEngine, ModuleDescriptor md,
            String conf, Date date, ResolveOptions options) {
        this.resolveEngine = resolveEngine;
        this.md = md;
        this.conf = conf;
        this.date = date;
        this.options = options;
    }

    /**
     * Check if the set of dependencies has changed since the previous execution of a resolution.
     * <p>
     * This function use the report file found in the cache. So the function must be called before
     * the new report is serialized there.
     * </p>
     * <p>
     * This function also use the internal dependencies that must already be filled. This function
     * might be 'heavy' because it may have to parse the previous report.
     * </p>
     */
    public void checkIfChanged() {
        ResolutionCacheManager cache = resolveEngine.getSettings().getResolutionCacheManager();
        String resolveId = options.getResolveId();
        File previousReportFile = cache.getConfigurationResolveReportInCache(resolveId, conf);
        if (previousReportFile.exists()) {
            try {
                XmlReportParser parser = new XmlReportParser();
                parser.parse(previousReportFile);
                Set<ModuleRevisionId> previousDepSet = new HashSet<>(
                        Arrays.asList(parser.getDependencyRevisionIds()));
                hasChanged = !previousDepSet.equals(getModuleRevisionIds());
            } catch (Exception e) {
                Message.warn("Error while parsing configuration resolve report "
                        + previousReportFile.getAbsolutePath(), e);
                hasChanged = Boolean.TRUE;
            }
        } else {
            hasChanged = Boolean.TRUE;
        }
    }

    /**
     * @pre checkIfChanged has been called.
     * @return boolean
     */
    public boolean hasChanged() {
        return hasChanged;
    }

    /**
     * Returns all non evicted and non error dependency mrids The returned set is ordered so that a
     * dependency will always be found before their own dependencies
     *
     * @return all non evicted and non error dependency mrids
     */
    public Set<ModuleRevisionId> getModuleRevisionIds() {
        Set<ModuleRevisionId> mrids = new LinkedHashSet<>();
        for (IvyNode node : getDependencies()) {
            if (!node.isEvicted(getConfiguration()) && !node.hasProblem()) {
                mrids.add(node.getResolvedId());
            }
        }
        return mrids;
    }

    public void addDependency(IvyNode node) {
        dependencies.put(node.getId(), node);
        dependencies.put(node.getResolvedId(), node);
        dependencyReports.put(node, Collections.<ArtifactDownloadReport> emptyList());
    }

    public void updateDependency(ModuleRevisionId mrid, IvyNode node) {
        dependencies.put(mrid, node);
    }

    public void addDependency(IvyNode node, DownloadReport report) {
        dependencies.put(node.getId(), node);
        dependencies.put(node.getResolvedId(), node);
        List<ArtifactDownloadReport> adrs = new ArrayList<>();
        for (Artifact artifact : node.getArtifacts(conf)) {
            ArtifactDownloadReport artifactReport = report.getArtifactReport(artifact);
            if (artifactReport != null) {
                adrs.add(artifactReport);
            } else {
                Message.debug("no report found for " + artifact);
            }
        }
        dependencyReports.put(node, adrs);
    }

    public String getConfiguration() {
        return conf;
    }

    public Date getDate() {
        return date;
    }

    public ModuleDescriptor getModuleDescriptor() {
        return md;
    }

    public ResolveOptions getResolveOptions()  {
        return options;
    }

    public IvyNode[] getUnresolvedDependencies() {
        List<IvyNode> unresolved = new ArrayList<>();
        for (IvyNode node : getDependencies()) {
            if (node.hasProblem()) {
                unresolved.add(node);
            }
        }
        return unresolved.toArray(new IvyNode[unresolved.size()]);
    }

    private Collection<IvyNode> getDependencies() {
        return new LinkedHashSet<>(dependencies.values());
    }

    public IvyNode[] getEvictedNodes() {
        List<IvyNode> evicted = new ArrayList<>();
        for (IvyNode node : getDependencies()) {
            if (node.isEvicted(conf)) {
                evicted.add(node);
            }
        }
        return evicted.toArray(new IvyNode[evicted.size()]);
    }

    private Set<ModuleRevisionId> getEvictedMrids() {
        Set<ModuleRevisionId> evicted = new LinkedHashSet<>();
        for (IvyNode node : getEvictedNodes()) {
            evicted.add(node.getId());
        }
        return evicted;
    }

    public IvyNode[] getDownloadedNodes() {
        List<IvyNode> downloaded = new ArrayList<>();
        for (IvyNode node : getDependencies()) {
            if (node.isDownloaded() && node.getRealNode() == node) {
                downloaded.add(node);
            }
        }
        return downloaded.toArray(new IvyNode[downloaded.size()]);
    }

    public IvyNode[] getSearchedNodes() {
        List<IvyNode> downloaded = new ArrayList<>();
        for (IvyNode node : getDependencies()) {
            if (node.isSearched() && node.getRealNode() == node) {
                downloaded.add(node);
            }
        }
        return downloaded.toArray(new IvyNode[downloaded.size()]);
    }

    public ArtifactDownloadReport[] getDownloadReports(ModuleRevisionId mrid) {
        Collection<ArtifactDownloadReport> col = dependencyReports.get(getDependency(mrid));
        if (col == null) {
            return new ArtifactDownloadReport[0];
        }
        return col.toArray(new ArtifactDownloadReport[col.size()]);
    }

    public IvyNode getDependency(ModuleRevisionId mrid) {
        return dependencies.get(mrid);
    }

    /**
     * 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() {
        if (modulesIds == null) {
            List<IvyNode> sortedDependencies = resolveEngine.getSortEngine().sortNodes(
                getDependencies(), SortOptions.SILENT);
            Collections.reverse(sortedDependencies);
            for (IvyNode dependency : sortedDependencies) {
                ModuleId mid = dependency.getResolvedId().getModuleId();
                Collection<IvyNode> deps = modulesIdsMap.get(mid);
                if (deps == null) {
                    deps = new LinkedHashSet<>();
                    modulesIdsMap.put(mid, deps);
                }
                deps.add(dependency);
            }
            modulesIds = new ArrayList<>(modulesIdsMap.keySet());
        }
        return Collections.unmodifiableList(modulesIds);
    }

    public Collection<IvyNode> getNodes(ModuleId mid) {
        if (modulesIds == null) {
            getModuleIds();
        }
        return modulesIdsMap.get(mid);
    }

    public ResolveEngine getResolveEngine() {
        return resolveEngine;
    }

    public int getArtifactsNumber() {
        int total = 0;
        for (Collection<ArtifactDownloadReport> reports : dependencyReports.values()) {
            total += reports == null ? 0 : reports.size();
        }
        return total;
    }

    /**
     * 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.
     * @return the list of reports, never <code>null</code>
     * @see ArtifactDownloadReport
     */
    public ArtifactDownloadReport[] getArtifactsReports(DownloadStatus downloadStatus,
            boolean withEvicted) {
        Collection<ArtifactDownloadReport> all = new LinkedHashSet<>();
        Collection<ModuleRevisionId> evictedMrids = null;
        if (!withEvicted) {
            evictedMrids = getEvictedMrids();
        }
        for (Collection<ArtifactDownloadReport> reports : dependencyReports.values()) {
            for (ArtifactDownloadReport report : reports) {
                if (downloadStatus != null && report.getDownloadStatus() != downloadStatus) {
                    continue;
                }
                if (withEvicted
                        || !evictedMrids.contains(report.getArtifact().getModuleRevisionId())) {
                    all.add(report);
                }
            }
        }
        return all.toArray(new ArtifactDownloadReport[all.size()]);
    }

    /**
     * Get the report on the successful download requests with the evicted modules
     *
     * @return the list of reports, never <code>null</code>
     */
    public ArtifactDownloadReport[] getDownloadedArtifactsReports() {
        return getArtifactsReports(DownloadStatus.SUCCESSFUL, true);
    }

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

    public boolean hasError() {
        return getUnresolvedDependencies().length > 0 || getFailedArtifactsReports().length > 0;
    }

    public int getNodesNumber() {
        return getDependencies().size();
    }

    public static ArtifactDownloadReport[] filterOutMergedArtifacts(
            ArtifactDownloadReport[] allFailedReports) {
        Collection<ArtifactDownloadReport> adrs = new ArrayList<>(
                Arrays.asList(allFailedReports));
        Iterator<ArtifactDownloadReport> iterator = adrs.iterator();
        while (iterator.hasNext()) {
            ArtifactDownloadReport adr = iterator.next();

            if (adr.getArtifact().getExtraAttribute("ivy:merged") != null) {
                iterator.remove();
            }
        }
        return adrs.toArray(new ArtifactDownloadReport[adrs.size()]);
    }

}
