/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed 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
 *
 *      http://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.gradle.api.publication.maven.internal.pom;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.gradle.api.GradleException;
import org.gradle.api.artifacts.*;
import org.gradle.api.artifacts.maven.Conf2ScopeMapping;
import org.gradle.api.artifacts.maven.Conf2ScopeMappingContainer;
import org.gradle.api.publication.maven.internal.VersionRangeMapper;

import java.util.*;

import static com.google.common.base.Strings.emptyToNull;

class DefaultPomDependenciesConverter implements PomDependenciesConverter {
    private static final List<Exclusion> EXCLUDE_ALL = initExcludeAll();
    private ExcludeRuleConverter excludeRuleConverter;
    private VersionRangeMapper versionRangeMapper;

    public DefaultPomDependenciesConverter(ExcludeRuleConverter excludeRuleConverter, VersionRangeMapper versionRangeMapper) {
        this.excludeRuleConverter = excludeRuleConverter;
        this.versionRangeMapper = versionRangeMapper;
    }

    public List<Dependency> convert(Conf2ScopeMappingContainer conf2ScopeMappingContainer, Set<Configuration> configurations) {
        Map<ModuleDependency, Set<Configuration>> dependencyToConfigurations = createDependencyToConfigurationsMap(configurations);
        Map<ModuleDependency, Conf2ScopeMapping> dependenciesMap = createDependencyToScopeMap(conf2ScopeMappingContainer, dependencyToConfigurations);
        Map<Dependency, Integer> dependenciesWithPriorities = new LinkedHashMap<Dependency, Integer>();
        for (ModuleDependency dependency : dependenciesMap.keySet()) {
            Conf2ScopeMapping conf2ScopeMapping = dependenciesMap.get(dependency);
            String scope = conf2ScopeMapping.getScope();
            Integer priority = conf2ScopeMapping.getPriority() == null ? 0 : conf2ScopeMapping.getPriority();
            Set<Configuration> dependencyConfigurations = dependencyToConfigurations.get(dependency);
            if (dependency.getArtifacts().size() == 0) {
                addFromDependencyDescriptor(dependenciesWithPriorities, dependency, scope, priority, dependencyConfigurations);
            } else {
                addFromArtifactDescriptor(dependenciesWithPriorities, dependency, scope, priority, dependencyConfigurations);
            }
        }
        return new ArrayList<Dependency>(dependenciesWithPriorities.keySet());
    }

    private Map<ModuleDependency, Conf2ScopeMapping> createDependencyToScopeMap(Conf2ScopeMappingContainer conf2ScopeMappingContainer,
                                                                                Map<ModuleDependency, Set<Configuration>> dependencyToConfigurations) {
        Map<ModuleDependency, Conf2ScopeMapping> dependencyToScope = new LinkedHashMap<ModuleDependency, Conf2ScopeMapping>();
        for (ModuleDependency dependency : dependencyToConfigurations.keySet()) {
            Conf2ScopeMapping conf2ScopeDependencyMapping = conf2ScopeMappingContainer.getMapping(dependencyToConfigurations.get(dependency));
            if (!useScope(conf2ScopeMappingContainer, conf2ScopeDependencyMapping)) {
                continue;
            }
            dependencyToScope.put(findDependency(dependency, conf2ScopeDependencyMapping.getConfiguration()), conf2ScopeDependencyMapping);
        }
        return dependencyToScope;
    }

    private ModuleDependency findDependency(ModuleDependency dependency, Configuration configuration) {
        for (ModuleDependency configurationDependency : configuration.getDependencies().withType(ModuleDependency.class)) {
            if (dependency.equals(configurationDependency)) {
                return configurationDependency;
            }
        }
        throw new GradleException("Dependency could not be found. We should never get here!");
    }

    private boolean useScope(Conf2ScopeMappingContainer conf2ScopeMappingContainer, Conf2ScopeMapping conf2ScopeMapping) {
        return conf2ScopeMapping.getScope() != null || !conf2ScopeMappingContainer.isSkipUnmappedConfs();
    }

    private Map<ModuleDependency, Set<Configuration>> createDependencyToConfigurationsMap(Set<Configuration> configurations) {
        Map<ModuleDependency, Set<Configuration>> dependencySetMap = new LinkedHashMap<ModuleDependency, Set<Configuration>>();
        for (Configuration configuration : configurations) {
            for (ModuleDependency dependency : configuration.getDependencies().withType(ModuleDependency.class)) {
                if (dependencySetMap.get(dependency) == null) {
                    dependencySetMap.put(dependency, new HashSet<Configuration>());
                }
                dependencySetMap.get(dependency).add(configuration);
            }
        }
        return dependencySetMap;
    }

    private void addFromArtifactDescriptor(Map<Dependency, Integer> dependenciesPriorityMap,
                                           ModuleDependency dependency, String scope, Integer priority,
                                           Set<Configuration> configurations) {
        for (DependencyArtifact artifact : dependency.getArtifacts()) {
            addMavenDependencies(dependenciesPriorityMap, dependency, artifact.getName(), artifact.getType(), scope, artifact.getClassifier(), priority, configurations);
        }
    }

    private void addFromDependencyDescriptor(Map<Dependency, Integer> dependenciesPriorityMap,
                                             ModuleDependency dependency, String scope, Integer priority,
                                             Set<Configuration> configurations) {
        addMavenDependencies(dependenciesPriorityMap, dependency, dependency.getName(), null, scope, null, priority, configurations);
    }

    private static Configuration getTargetConfiguration(ProjectDependency dependency) {
        // todo CC: check that it ok to do this if configurations have attributes
        String targetConfiguration = dependency.getTargetConfiguration();
        if (targetConfiguration == null) {
            targetConfiguration = org.gradle.api.artifacts.Dependency.DEFAULT_CONFIGURATION;
        }
        return dependency.getDependencyProject().getConfigurations().getByName(targetConfiguration);
    }

    private void addMavenDependencies(Map<Dependency, Integer> dependenciesWithPriorities,
                                      ModuleDependency dependency, String name, String type, String scope, String classifier, Integer priority,
                                      Set<Configuration> configurations) {
        List<Dependency> mavenDependencies = new ArrayList<Dependency>();

        if (dependency instanceof ProjectDependency) {
            ProjectDependency projectDependency = (ProjectDependency) dependency;
            final String artifactId = determineProjectDependencyArtifactId((ProjectDependency) dependency);

            Configuration dependencyConfig = getTargetConfiguration(projectDependency);
            for (PublishArtifact artifactToPublish : dependencyConfig.getAllArtifacts()) {
                Dependency mavenDependency = new Dependency();
                mavenDependency.setArtifactId(artifactId);
                if (artifactToPublish.getClassifier() != null && !artifactToPublish.getClassifier().equals("")) {
                    mavenDependency.setClassifier(artifactToPublish.getClassifier());
                }
                mavenDependencies.add(mavenDependency);
            }
        } else {
            Dependency mavenDependency = new Dependency();
            mavenDependency.setArtifactId(name);
            mavenDependency.setClassifier(classifier);
            mavenDependencies.add(mavenDependency);
        }

        for (Dependency mavenDependency : mavenDependencies) {
            mavenDependency.setGroupId(dependency.getGroup());
            mavenDependency.setVersion(mapToMavenSyntax(dependency.getVersion()));
            mavenDependency.setType(type);
            mavenDependency.setScope(scope);
            mavenDependency.setExclusions(getExclusions(dependency, configurations));
            // Dependencies de-duplication
            Optional<Dependency> duplicateDependency = findEqualIgnoreScopeVersionAndExclusions(dependenciesWithPriorities.keySet(), mavenDependency);
            if (!duplicateDependency.isPresent()) {
                // Add if absent
                dependenciesWithPriorities.put(mavenDependency, priority);
            } else {
                // Use highest version on highest scope, keep highest scope exclusions only
                int duplicatePriority = dependenciesWithPriorities.get(duplicateDependency.get());
                boolean samePriority = priority == duplicatePriority;
                boolean higherPriority = priority > duplicatePriority;
                boolean higherVersion = compareMavenVersionStrings(mavenDependency.getVersion(), duplicateDependency.get().getVersion()) > 0;
                if (higherPriority || higherVersion) {
                    // Replace if higher priority or version with highest priority and version
                    dependenciesWithPriorities.remove(duplicateDependency.get());
                    if (!higherPriority) {
                        // Lower or equal priority but higher version, keep higher scope and exclusions
                        mavenDependency.setScope(duplicateDependency.get().getScope());
                        if (!samePriority) {
                            mavenDependency.setExclusions(duplicateDependency.get().getExclusions());
                        }
                    }
                    int highestPriority = higherPriority ? priority : duplicatePriority;
                    dependenciesWithPriorities.put(mavenDependency, highestPriority);
                }
            }
        }
    }

    private int compareMavenVersionStrings(String dependencyVersionString, String duplicateVersionString) {
        String dependencyVersion = emptyToNull(dependencyVersionString);
        String duplicateVersion = emptyToNull(duplicateVersionString);
        if (dependencyVersion == null && duplicateVersion == null) {
            return 0;
        }
        if (dependencyVersion == null) {
            return -1;
        }
        if (duplicateVersion == null) {
            return 1;
        }
        ArtifactVersion dependencyArtifactVersion = new DefaultArtifactVersion(dependencyVersion);
        ArtifactVersion duplicateArtifactVersion = new DefaultArtifactVersion(duplicateVersion);
        return dependencyArtifactVersion.compareTo(duplicateArtifactVersion);
    }

    private Optional<Dependency> findEqualIgnoreScopeVersionAndExclusions(Collection<Dependency> dependencies, Dependency candidate) {
        // For project dependencies de-duplication
        // Ignore scope on purpose
        // Ignore version because Maven doesn't support dependencies with different versions on different scopes
        // Ignore exclusions because we don't know how to choose/merge them
        // Consequence is that we use the highest version and the exclusions of highest priority dependency when de-duplicating
        // Use Maven Dependency "Management Key" as discriminator: groupId:artifactId:type:classifier
        final String candidateManagementKey = candidate.getManagementKey();
        return Iterables.tryFind(dependencies, new Predicate<Dependency>() {
            @Override
            public boolean apply(Dependency dependency) {
                return dependency.getManagementKey().equals(candidateManagementKey);
            }
        });
    }

    private String mapToMavenSyntax(String version) {
        return versionRangeMapper.map(version);
    }

    protected String determineProjectDependencyArtifactId(ProjectDependency dependency) {
        return new ProjectDependencyArtifactIdExtractorHack(dependency).extract();
    }

    private List<Exclusion> getExclusions(ModuleDependency dependency, Set<Configuration> configurations) {
        if (!dependency.isTransitive()) {
            return EXCLUDE_ALL;
        }
        List<Exclusion> mavenExclusions = new ArrayList<Exclusion>();
        Set<ExcludeRule> excludeRules = new HashSet<ExcludeRule>(dependency.getExcludeRules());
        for (Configuration configuration : configurations) {
            excludeRules.addAll(configuration.getExcludeRules());
        }
        for (ExcludeRule excludeRule : excludeRules) {
            Exclusion mavenExclusion = (Exclusion) excludeRuleConverter.convert(excludeRule);
            if (mavenExclusion != null) {
                mavenExclusions.add(mavenExclusion);
            }
        }
        return mavenExclusions;
    }

    private static List<Exclusion> initExcludeAll() {
        Exclusion excludeAll = new Exclusion();
        excludeAll.setGroupId("*");
        excludeAll.setArtifactId("*");
        return Collections.singletonList(excludeAll);
    }

    public ExcludeRuleConverter getExcludeRuleConverter() {
        return excludeRuleConverter;
    }
}
