/*
 * 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
 *
 *   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.apache.maven.lifecycle.internal;

import javax.inject.Inject;
import javax.inject.Named;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.maven.RepositoryUtils;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.eventspy.internal.EventSpyDispatcher;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.project.DefaultDependencyResolutionRequest;
import org.apache.maven.project.DependencyResolutionException;
import org.apache.maven.project.DependencyResolutionResult;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectDependenciesResolver;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;
import org.apache.maven.project.artifact.ProjectArtifactsCache;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.util.filter.AndDependencyFilter;
import org.eclipse.aether.util.filter.ScopeDependencyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>
 * Resolves dependencies for the artifacts in context of the lifecycle build
 * </p>
 * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
 * @since 3.0
 */
@Named
public class LifecycleDependencyResolver {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final ProjectDependenciesResolver dependenciesResolver;

    private final ProjectArtifactFactory artifactFactory;

    private final EventSpyDispatcher eventSpyDispatcher;

    private final ProjectArtifactsCache projectArtifactsCache;

    private final MessageBuilderFactory messageBuilderFactory;

    @Inject
    public LifecycleDependencyResolver(
            ProjectDependenciesResolver dependenciesResolver,
            ProjectArtifactFactory artifactFactory,
            EventSpyDispatcher eventSpyDispatcher,
            ProjectArtifactsCache projectArtifactsCache,
            MessageBuilderFactory messageBuilderFactory) {
        this.dependenciesResolver = dependenciesResolver;
        this.artifactFactory = artifactFactory;
        this.eventSpyDispatcher = eventSpyDispatcher;
        this.projectArtifactsCache = projectArtifactsCache;
        this.messageBuilderFactory = messageBuilderFactory;
    }

    public static List<MavenProject> getProjects(MavenProject project, MavenSession session, boolean aggregator) {
        if (aggregator && project.getCollectedProjects() != null) {
            // get the unsorted list of wanted projects
            Set<MavenProject> projectAndSubmodules = new HashSet<>(project.getCollectedProjects());
            projectAndSubmodules.add(project);
            return session.getProjects().stream() // sorted all
                    .filter(projectAndSubmodules::contains)
                    .collect(Collectors.toList()); // sorted and filtered to what we need
        } else {
            return Collections.singletonList(project);
        }
    }

    public void resolveProjectDependencies(
            MavenProject project,
            Collection<String> scopesToCollect,
            Collection<String> scopesToResolve,
            MavenSession session,
            boolean aggregating,
            Set<Artifact> projectArtifacts)
            throws LifecycleExecutionException {
        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        try {
            ClassLoader projectRealm = project.getClassRealm();
            if (projectRealm != null && projectRealm != tccl) {
                Thread.currentThread().setContextClassLoader(projectRealm);
            }

            if (project.getDependencyArtifacts() == null) {
                try {
                    project.setDependencyArtifacts(artifactFactory.createArtifacts(project));
                } catch (InvalidDependencyVersionException e) {
                    throw new LifecycleExecutionException(e);
                }
            }

            Set<Artifact> resolvedArtifacts = resolveProjectArtifacts(
                    project, scopesToCollect, scopesToResolve, session, aggregating, projectArtifacts);

            Map<Artifact, File> reactorProjects =
                    new HashMap<>(session.getProjects().size());
            for (MavenProject reactorProject : session.getProjects()) {
                reactorProjects.put(
                        reactorProject.getArtifact(),
                        reactorProject.getArtifact().getFile());
            }

            Map<String, Artifact> map = new HashMap<>();
            for (Artifact artifact : resolvedArtifacts) {
                /**
                 * MNG-6300: resolvedArtifacts can be cache result; this ensures reactor files are always up-to-date
                 * During lifecycle the Artifact.getFile() can change from target/classes to the actual jar.
                 * This clearly shows that target/classes should not be abused as artifactFile just for the classpath
                 */
                File reactorProjectFile = reactorProjects.get(artifact);
                if (reactorProjectFile != null) {
                    artifact.setFile(reactorProjectFile);
                }

                map.put(artifact.getDependencyConflictId(), artifact);
            }

            project.setResolvedArtifacts(resolvedArtifacts);

            for (Artifact artifact : project.getDependencyArtifacts()) {
                if (artifact.getFile() == null) {
                    Artifact resolved = map.get(artifact.getDependencyConflictId());
                    if (resolved != null) {
                        artifact.setFile(resolved.getFile());
                        artifact.setDependencyTrail(resolved.getDependencyTrail());
                        artifact.setResolvedVersion(resolved.getVersion());
                        artifact.setResolved(true);
                    }
                }
            }
        } finally {
            Thread.currentThread().setContextClassLoader(tccl);
        }
    }

    public DependencyResolutionResult getProjectDependencyResolutionResult(
            MavenProject project,
            Collection<String> scopesToCollect,
            Collection<String> scopesToResolve,
            MavenSession session,
            boolean aggregating,
            Set<Artifact> projectArtifacts)
            throws LifecycleExecutionException {

        Set<Artifact> resolvedArtifacts = resolveProjectArtifacts(
                project, scopesToCollect, scopesToResolve, session, aggregating, projectArtifacts);
        if (resolvedArtifacts instanceof ProjectArtifactsCache.ArtifactsSetWithResult artifactsSetWithResult) {
            return artifactsSetWithResult.getResult();
        } else {
            throw new IllegalStateException();
        }
    }

    public Set<Artifact> resolveProjectArtifacts(
            MavenProject project,
            Collection<String> scopesToCollect,
            Collection<String> scopesToResolve,
            MavenSession session,
            boolean aggregating,
            Set<Artifact> projectArtifacts)
            throws LifecycleExecutionException {
        ProjectArtifactsCache.Key cacheKey = projectArtifactsCache.createKey(
                project, scopesToCollect, scopesToResolve, aggregating, session.getRepositorySession());

        ProjectArtifactsCache.CacheRecord recordArtifacts;
        recordArtifacts = projectArtifactsCache.get(cacheKey);
        if (recordArtifacts == null) {
            synchronized (cacheKey) {
                recordArtifacts = projectArtifactsCache.get(cacheKey);
                if (recordArtifacts == null) {
                    try {
                        Set<Artifact> resolvedArtifacts = getDependencies(
                                project, scopesToCollect, scopesToResolve, session, aggregating, projectArtifacts);
                        recordArtifacts = projectArtifactsCache.put(cacheKey, resolvedArtifacts);
                    } catch (LifecycleExecutionException e) {
                        projectArtifactsCache.put(cacheKey, e);
                        projectArtifactsCache.register(project, cacheKey, recordArtifacts);
                        throw e;
                    }
                }
            }
        }
        projectArtifactsCache.register(project, cacheKey, recordArtifacts);

        return recordArtifacts.getArtifacts();
    }

    private Set<Artifact> getDependencies(
            MavenProject project,
            Collection<String> scopesToCollect,
            Collection<String> scopesToResolve,
            MavenSession session,
            boolean aggregating,
            Set<Artifact> projectArtifacts)
            throws LifecycleExecutionException {
        if (scopesToCollect == null) {
            scopesToCollect = Collections.emptySet();
        }
        if (scopesToResolve == null) {
            scopesToResolve = Collections.emptySet();
        }

        if (scopesToCollect.isEmpty() && scopesToResolve.isEmpty()) {
            return new SetWithResolutionResult(null, new LinkedHashSet<>());
        }

        scopesToCollect = new HashSet<>(scopesToCollect);
        scopesToCollect.addAll(scopesToResolve);

        DependencyFilter collectionFilter = new ScopeDependencyFilter(null, negate(scopesToCollect));
        DependencyFilter resolutionFilter = new ScopeDependencyFilter(null, negate(scopesToResolve));
        resolutionFilter = AndDependencyFilter.newInstance(collectionFilter, resolutionFilter);
        resolutionFilter =
                AndDependencyFilter.newInstance(resolutionFilter, new ReactorDependencyFilter(projectArtifacts));

        DependencyResolutionResult result;
        try {
            DefaultDependencyResolutionRequest request =
                    new DefaultDependencyResolutionRequest(project, session.getRepositorySession());
            request.setResolutionFilter(resolutionFilter);

            eventSpyDispatcher.onEvent(request);

            result = dependenciesResolver.resolve(request);
        } catch (DependencyResolutionException e) {
            result = e.getResult();

            /*
             * MNG-2277, the check below compensates for our bad plugin support where we ended up with aggregator
             * plugins that require dependency resolution, although they usually run in phases of the build where project
             * artifacts haven't been assembled yet. The prime example of this is "mvn release:prepare".
             */
            if (aggregating && areAllDependenciesInReactor(session.getProjects(), result.getUnresolvedDependencies())) {
                logger.warn("The following dependencies could not be resolved at this point of the build"
                        + " but seem to be part of the reactor:");

                for (Dependency dependency : result.getUnresolvedDependencies()) {
                    logger.warn("o {}", dependency);
                }

                logger.warn("Try running the build up to the lifecycle phase \"package\"");
            } else {
                throw new LifecycleExecutionException(messageBuilderFactory, null, project, e);
            }
        }

        eventSpyDispatcher.onEvent(result);

        Set<Artifact> artifacts = new LinkedHashSet<>();
        if (result.getDependencyGraph() != null
                && !result.getDependencyGraph().getChildren().isEmpty()) {
            RepositoryUtils.toArtifacts(
                    artifacts,
                    result.getDependencyGraph().getChildren(),
                    Collections.singletonList(project.getArtifact().getId()),
                    collectionFilter);
        }
        return new SetWithResolutionResult(result, artifacts);
    }

    private boolean areAllDependenciesInReactor(
            Collection<MavenProject> projects, Collection<Dependency> dependencies) {
        Set<String> projectKeys = getReactorProjectKeys(projects);

        for (Dependency dependency : dependencies) {
            org.eclipse.aether.artifact.Artifact a = dependency.getArtifact();
            String key = ArtifactUtils.key(a.getGroupId(), a.getArtifactId(), a.getVersion());
            if (!projectKeys.contains(key)) {
                return false;
            }
        }

        return true;
    }

    private Set<String> getReactorProjectKeys(Collection<MavenProject> projects) {
        Set<String> projectKeys = new HashSet<>(projects.size() * 2);
        for (MavenProject project : projects) {
            String key = ArtifactUtils.key(project.getGroupId(), project.getArtifactId(), project.getVersion());
            projectKeys.add(key);
        }
        return projectKeys;
    }

    private Collection<String> negate(Collection<String> scopes) {
        Collection<String> result = new HashSet<>();
        Collections.addAll(result, "system", "compile", "provided", "runtime", "test");

        for (String scope : scopes) {
            if ("compile".equals(scope)) {
                result.remove("compile");
                result.remove("system");
                result.remove("provided");
            } else if ("runtime".equals(scope)) {
                result.remove("compile");
                result.remove("runtime");
            } else if ("compile+runtime".equals(scope)) {
                result.remove("compile");
                result.remove("system");
                result.remove("provided");
                result.remove("runtime");
            } else if ("runtime+system".equals(scope)) {
                result.remove("compile");
                result.remove("system");
                result.remove("runtime");
            } else if ("test".equals(scope)) {
                result.clear();
            }
        }

        return result;
    }

    private static class ReactorDependencyFilter implements DependencyFilter {

        private final Set<String> keys = new HashSet<>();

        ReactorDependencyFilter(Collection<Artifact> artifacts) {
            for (Artifact artifact : artifacts) {
                String key = ArtifactUtils.key(artifact);
                keys.add(key);
            }
        }

        @Override
        public boolean accept(DependencyNode node, List<DependencyNode> parents) {
            Dependency dependency = node.getDependency();
            if (dependency != null) {
                org.eclipse.aether.artifact.Artifact a = dependency.getArtifact();
                String key = ArtifactUtils.key(a.getGroupId(), a.getArtifactId(), a.getVersion());
                return !keys.contains(key);
            }
            return false;
        }
    }
}
