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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;

import org.apache.maven.api.Artifact;
import org.apache.maven.api.DependencyCoordinates;
import org.apache.maven.api.JavaPathType;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.PathType;
import org.apache.maven.api.Project;
import org.apache.maven.api.RemoteRepository;
import org.apache.maven.api.Session;
import org.apache.maven.api.SourceRoot;
import org.apache.maven.api.Version;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Immutable;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.NotThreadSafe;
import org.apache.maven.api.annotations.Nullable;

import static java.util.Objects.requireNonNull;

/**
 * A request to collect the transitive dependencies and to build a dependency graph from them. There are three ways to
 * create a dependency graph. First, only the root dependency can be given. Second, a root dependency and direct
 * dependencies can be specified in which case the specified direct dependencies are merged with the direct dependencies
 * retrieved from the artifact descriptor of the root dependency. And last, only direct dependencies can be specified in
 * which case the root node of the resulting graph has no associated dependency.
 *
 * @since 4.0.0
 */
@Experimental
@Immutable
public interface DependencyResolverRequest extends RepositoryAwareRequest {

    enum RequestType {
        COLLECT,
        FLATTEN,
        RESOLVE
    }

    @Nonnull
    RequestType getRequestType();

    @Nonnull
    Optional<Project> getProject();

    @Nonnull
    Optional<Artifact> getRootArtifact();

    @Nonnull
    Optional<DependencyCoordinates> getRoot();

    @Nonnull
    Collection<DependencyCoordinates> getDependencies();

    @Nonnull
    Collection<DependencyCoordinates> getManagedDependencies();

    boolean getVerbose();

    @Nonnull
    PathScope getPathScope();

    /**
     * Returns a filter for the types of path (class-path, module-path, …) accepted by the tool.
     * For example, if a Java tools accepts only class-path elements, then the filter should return
     * {@code true} for {@link JavaPathType#CLASSES} and {@code false} for {@link JavaPathType#MODULES}.
     * If no filter is explicitly set, then the default is a filter accepting everything.
     *
     * @return a filter for the types of path (class-path, module-path, …) accepted by the tool
     */
    @Nullable
    Predicate<PathType> getPathTypeFilter();

    /**
     * Returns the version of the platform where the code will be executed.
     * It should be the highest value of the {@code <targetVersion>} elements
     * inside the {@code <source>} elements of a <abbr>POM</abbr> file.
     *
     * <h4>Application to Java</h4>
     * In the context of a Java project, this is the value given to the {@code --release} compiler option.
     * This value can determine whether a dependency will be placed on the class-path or on the module-path.
     * For example, if the {@code module-info.class} entry of a <abbr>JAR</abbr> file exists only in the
     * {@code META-INF/versions/17/} sub-directory, then the default location of that dependency will be
     * the module-path only if the {@code --release} option is equal or greater than 17.
     *
     * <p>If this value is not provided, then the default value in the context of Java projects
     * is the Java version on which Maven is running, as given by {@link Runtime#version()}.</p>
     *
     * @return version of the platform where the code will be executed, or {@code null} for default
     *
     * @see SourceRoot#targetVersion()
     */
    @Nullable
    Version getTargetVersion();

    @Nonnull
    static DependencyResolverRequestBuilder builder() {
        return new DependencyResolverRequestBuilder();
    }

    @Nonnull
    static DependencyResolverRequest build(Session session, RequestType requestType, Artifact rootArtifact) {
        return build(session, requestType, rootArtifact, PathScope.MAIN_RUNTIME);
    }

    @Nonnull
    static DependencyResolverRequest build(
            Session session, RequestType requestType, Artifact rootArtifact, PathScope scope) {
        return new DependencyResolverRequestBuilder()
                .session(session)
                .requestType(requestType)
                .rootArtifact(rootArtifact)
                .pathScope(scope)
                .build();
    }

    @Nonnull
    static DependencyResolverRequest build(Session session, RequestType requestType, Project project) {
        return build(session, requestType, project, PathScope.MAIN_RUNTIME);
    }

    @Nonnull
    static DependencyResolverRequest build(Session session, RequestType requestType, Project project, PathScope scope) {
        return new DependencyResolverRequestBuilder()
                .session(session)
                .requestType(requestType)
                .project(project)
                .pathScope(scope)
                .build();
    }

    @Nonnull
    static DependencyResolverRequest build(Session session, RequestType requestType, DependencyCoordinates dependency) {
        return build(session, requestType, dependency, PathScope.MAIN_RUNTIME);
    }

    @Nonnull
    static DependencyResolverRequest build(
            Session session, RequestType requestType, DependencyCoordinates dependency, PathScope scope) {
        return new DependencyResolverRequestBuilder()
                .session(session)
                .requestType(requestType)
                .dependency(dependency)
                .pathScope(scope)
                .build();
    }

    @Nonnull
    static DependencyResolverRequest build(
            Session session, RequestType requestType, List<DependencyCoordinates> dependencies) {
        return build(session, requestType, dependencies, PathScope.MAIN_RUNTIME);
    }

    @Nonnull
    static DependencyResolverRequest build(
            Session session, RequestType requestType, List<DependencyCoordinates> dependencies, PathScope scope) {
        return new DependencyResolverRequestBuilder()
                .session(session)
                .requestType(requestType)
                .dependencies(dependencies)
                .pathScope(scope)
                .build();
    }

    @NotThreadSafe
    class DependencyResolverRequestBuilder {

        Session session;
        RequestTrace trace;
        RequestType requestType;
        Project project;
        Artifact rootArtifact;
        DependencyCoordinates root;
        List<DependencyCoordinates> dependencies = Collections.emptyList();
        List<DependencyCoordinates> managedDependencies = Collections.emptyList();
        boolean verbose;
        PathScope pathScope;
        Predicate<PathType> pathTypeFilter;
        Version targetVersion;
        List<RemoteRepository> repositories;

        DependencyResolverRequestBuilder() {}

        @Nonnull
        public DependencyResolverRequestBuilder session(@Nonnull Session session) {
            this.session = session;
            return this;
        }

        @Nonnull
        public DependencyResolverRequestBuilder trace(RequestTrace trace) {
            this.trace = trace;
            return this;
        }

        @Nonnull
        public DependencyResolverRequestBuilder requestType(@Nonnull RequestType requestType) {
            this.requestType = requestType;
            return this;
        }

        @Nonnull
        public DependencyResolverRequestBuilder project(@Nullable Project project) {
            this.project = project;
            return this;
        }

        /**
         * Sets the root artifact for the dependency graph.
         * This must not be confused with {@link #root(DependencyCoordinates)}: The root <em>dependency</em>, like any
         * other specified dependency, will be subject to dependency collection/resolution, i.e. should have an artifact
         * descriptor and a corresponding artifact file. The root <em>artifact</em> on the other hand is only used
         * as a label for the root node of the graph in case no root dependency was specified. As such, the configured
         * root artifact is ignored if {@link #root(DependencyCoordinates)} has been set.
         *
         * @param rootArtifact the root artifact for the dependency graph, may be {@code null}
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder rootArtifact(@Nullable Artifact rootArtifact) {
            this.rootArtifact = rootArtifact;
            return this;
        }

        /**
         * @param root The root dependency
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder root(@Nonnull DependencyCoordinates root) {
            this.root = root;
            return this;
        }

        /**
         * Sets the direct dependencies. If both a root dependency and direct dependencies are given in the request, the
         * direct dependencies from the request will be merged with the direct dependencies from the root dependency's
         * artifact descriptor, giving higher priority to the dependencies from the request.
         *
         * @param dependencies the direct dependencies, may be {@code null}
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder dependencies(@Nullable List<DependencyCoordinates> dependencies) {
            this.dependencies = (dependencies != null) ? dependencies : Collections.emptyList();
            return this;
        }

        /**
         * Adds the specified direct dependency.
         *
         * @param dependency the dependency to add, may be {@code null}
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder dependency(@Nullable DependencyCoordinates dependency) {
            if (dependency != null) {
                if (this.dependencies.isEmpty()) {
                    this.dependencies = new ArrayList<>();
                }
                this.dependencies.add(dependency);
            }
            return this;
        }

        /**
         * Sets the dependency management to apply to transitive dependencies. To clarify, this management does not
         * apply to
         * the direct dependencies of the root node.
         *
         * @param managedDependencies the dependency management, may be {@code null}
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder managedDependencies(
                @Nullable List<DependencyCoordinates> managedDependencies) {
            this.managedDependencies = (managedDependencies != null) ? managedDependencies : Collections.emptyList();
            return this;
        }

        /**
         * Adds the specified managed dependency.
         *
         * @param managedDependency The managed dependency to add, may be {@code null} in which case the call
         *                          will have no effect.
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder managedDependency(@Nullable DependencyCoordinates managedDependency) {
            if (managedDependency != null) {
                if (this.managedDependencies.isEmpty()) {
                    this.managedDependencies = new ArrayList<>();
                }
                this.managedDependencies.add(managedDependency);
            }
            return this;
        }

        /**
         * Specifies that the collection should be verbose.
         *
         * @param verbose whether the collection should be verbose or not
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyResolverRequestBuilder verbose(boolean verbose) {
            this.verbose = verbose;
            return this;
        }

        @Nonnull
        public DependencyResolverRequestBuilder pathScope(@Nullable PathScope pathScope) {
            this.pathScope = pathScope;
            return this;
        }

        /**
         * Filters the types of paths to include in the result.
         * The result will contain only the paths of types for which the predicate returned {@code true}.
         * It is recommended to apply a filter for retaining only the types of paths of interest,
         * because it can resolve ambiguities when a path could be of many types.
         *
         * @param pathTypeFilter predicate evaluating whether a path type should be included in the result
         * @return {@code this} for method call chaining
         */
        @Nonnull
        public DependencyResolverRequestBuilder pathTypeFilter(@Nonnull Predicate<PathType> pathTypeFilter) {
            this.pathTypeFilter = pathTypeFilter;
            return this;
        }

        /**
         * Specifies the type of paths to include in the result. This is a convenience method for
         * {@link #pathTypeFilter(Predicate)} using {@link Collection#contains(Object)} as the filter.
         *
         * @param desiredTypes the type of paths to include in the result
         * @return {@code this} for method call chaining
         */
        @Nonnull
        public DependencyResolverRequestBuilder pathTypeFilter(@Nonnull Collection<? extends PathType> desiredTypes) {
            return pathTypeFilter(desiredTypes::contains);
        }

        /**
         * Sets the version of the platform where the code will be executed.
         *
         * @param target version of the platform where the code will be executed, or {@code null} for the default
         * @return {@code this} for method call chaining
         */
        @Nonnull
        public DependencyResolverRequestBuilder targetVersion(@Nullable Version target) {
            targetVersion = target;
            return this;
        }

        @Nonnull
        public DependencyResolverRequestBuilder repositories(@Nonnull List<RemoteRepository> repositories) {
            this.repositories = repositories;
            return this;
        }

        @Nonnull
        public DependencyResolverRequest build() {
            return new DefaultDependencyResolverRequest(
                    session,
                    trace,
                    requestType,
                    project,
                    rootArtifact,
                    root,
                    dependencies,
                    managedDependencies,
                    verbose,
                    pathScope,
                    pathTypeFilter,
                    targetVersion,
                    repositories);
        }

        static class DefaultDependencyResolverRequest extends BaseRequest<Session>
                implements DependencyResolverRequest {

            static final class AlwaysTrueFilter implements Predicate<PathType> {
                @Override
                public boolean test(PathType pathType) {
                    return true;
                }

                @Override
                public boolean equals(Object obj) {
                    return obj instanceof AlwaysTrueFilter;
                }

                @Override
                public int hashCode() {
                    return AlwaysTrueFilter.class.hashCode();
                }

                @Override
                public String toString() {
                    return "AlwaysTrueFilter[]";
                }
            }

            private static final Predicate<PathType> DEFAULT_FILTER = new AlwaysTrueFilter();

            private final RequestType requestType;
            private final Project project;
            private final Artifact rootArtifact;
            private final DependencyCoordinates root;
            private final Collection<DependencyCoordinates> dependencies;
            private final Collection<DependencyCoordinates> managedDependencies;
            private final boolean verbose;
            private final PathScope pathScope;
            private final Predicate<PathType> pathTypeFilter;
            private final Version targetVersion;
            private final List<RemoteRepository> repositories;

            /**
             * Creates a request with the specified properties.
             *
             * @param session      {@link Session}
             * @param rootArtifact The root dependency whose transitive dependencies should be collected, may be {@code
             *                     null}.
             */
            @SuppressWarnings("checkstyle:ParameterNumber")
            DefaultDependencyResolverRequest(
                    @Nonnull Session session,
                    @Nullable RequestTrace trace,
                    @Nonnull RequestType requestType,
                    @Nullable Project project,
                    @Nullable Artifact rootArtifact,
                    @Nullable DependencyCoordinates root,
                    @Nonnull Collection<DependencyCoordinates> dependencies,
                    @Nonnull Collection<DependencyCoordinates> managedDependencies,
                    boolean verbose,
                    @Nullable PathScope pathScope,
                    @Nullable Predicate<PathType> pathTypeFilter,
                    @Nullable Version targetVersion,
                    @Nullable List<RemoteRepository> repositories) {
                super(session, trace);
                this.requestType = requireNonNull(requestType, "requestType cannot be null");
                this.project = project;
                this.rootArtifact = rootArtifact;
                this.root = root;
                this.dependencies = List.copyOf(requireNonNull(dependencies, "dependencies cannot be null"));
                this.managedDependencies =
                        List.copyOf(requireNonNull(managedDependencies, "managedDependencies cannot be null"));
                this.verbose = verbose;
                this.pathScope = requireNonNull(pathScope, "pathScope cannot be null");
                this.pathTypeFilter = (pathTypeFilter != null) ? pathTypeFilter : DEFAULT_FILTER;
                this.targetVersion = targetVersion;
                this.repositories = validate(repositories);
                if (verbose && requestType != RequestType.COLLECT) {
                    throw new IllegalArgumentException("verbose cannot only be true when collecting dependencies");
                }
            }

            @Nonnull
            @Override
            public RequestType getRequestType() {
                return requestType;
            }

            @Nonnull
            @Override
            public Optional<Project> getProject() {
                return Optional.ofNullable(project);
            }

            @Nonnull
            @Override
            public Optional<Artifact> getRootArtifact() {
                return Optional.ofNullable(rootArtifact);
            }

            @Nonnull
            @Override
            public Optional<DependencyCoordinates> getRoot() {
                return Optional.ofNullable(root);
            }

            @Nonnull
            @Override
            public Collection<DependencyCoordinates> getDependencies() {
                return dependencies;
            }

            @Nonnull
            @Override
            public Collection<DependencyCoordinates> getManagedDependencies() {
                return managedDependencies;
            }

            @Override
            public boolean getVerbose() {
                return verbose;
            }

            @Override
            public PathScope getPathScope() {
                return pathScope;
            }

            @Override
            public Predicate<PathType> getPathTypeFilter() {
                return pathTypeFilter;
            }

            @Override
            public Version getTargetVersion() {
                return targetVersion;
            }

            @Override
            public List<RemoteRepository> getRepositories() {
                return repositories;
            }

            @Override
            public boolean equals(Object o) {
                return o instanceof DefaultDependencyResolverRequest that
                        && verbose == that.verbose
                        && requestType == that.requestType
                        && Objects.equals(project, that.project)
                        && Objects.equals(rootArtifact, that.rootArtifact)
                        && Objects.equals(root, that.root)
                        && Objects.equals(dependencies, that.dependencies)
                        && Objects.equals(managedDependencies, that.managedDependencies)
                        && Objects.equals(pathScope, that.pathScope)
                        && Objects.equals(pathTypeFilter, that.pathTypeFilter)
                        && Objects.equals(targetVersion, that.targetVersion)
                        && Objects.equals(repositories, that.repositories);
            }

            @Override
            public int hashCode() {
                return Objects.hash(
                        requestType,
                        project,
                        rootArtifact,
                        root,
                        dependencies,
                        managedDependencies,
                        verbose,
                        pathScope,
                        pathTypeFilter,
                        targetVersion,
                        repositories);
            }

            @Override
            public String toString() {
                return "DependencyResolverRequest[" + "requestType="
                        + requestType + ", project="
                        + project + ", rootArtifact="
                        + rootArtifact + ", root="
                        + root + ", dependencies="
                        + dependencies + ", managedDependencies="
                        + managedDependencies + ", verbose="
                        + verbose + ", pathScope="
                        + pathScope + ", pathTypeFilter="
                        + pathTypeFilter + ", targetVersion="
                        + targetVersion + ", repositories="
                        + repositories + ']';
            }
        }
    }
}
