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

import org.apache.maven.api.Artifact;
import org.apache.maven.api.DependencyCoordinate;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
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 org.apache.maven.api.services.BaseRequest.nonNull;

/**
 * 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
 * @see DependencyCollector#collect(DependencyCollectorRequest)
 */
@Experimental
@Immutable
public interface DependencyCollectorRequest {

    @Nonnull
    Session getSession();

    Optional<Project> getProject();

    @Nonnull
    Optional<Artifact> getRootArtifact();

    @Nonnull
    Optional<DependencyCoordinate> getRoot();

    @Nonnull
    Collection<DependencyCoordinate> getDependencies();

    @Nonnull
    Collection<DependencyCoordinate> getManagedDependencies();

    boolean getVerbose();

    @Nonnull
    static DependencyCollectorRequest build(@Nonnull Session session, Artifact root) {
        return builder()
                .session(nonNull(session, "session cannot be null"))
                .rootArtifact(nonNull(root, "root cannot be null"))
                .build();
    }

    @Nonnull
    static DependencyCollectorRequest build(@Nonnull Session session, @Nonnull DependencyCoordinate root) {
        return builder()
                .session(nonNull(session, "session cannot be null"))
                .root(nonNull(root, "root cannot be null"))
                .build();
    }

    @Nonnull
    static DependencyCollectorRequest build(@Nonnull Session session, @Nonnull Project project) {
        return builder()
                .session(nonNull(session, "session cannot be null"))
                .project(nonNull(project, "project cannot be null"))
                .build();
    }

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

    @NotThreadSafe
    class DependencyCollectorRequestBuilder {

        Session session;
        Project project;
        Artifact rootArtifact;
        DependencyCoordinate root;
        List<DependencyCoordinate> dependencies = Collections.emptyList();
        List<DependencyCoordinate> managedDependencies = Collections.emptyList();
        boolean verbose;

        DependencyCollectorRequestBuilder() {}

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

        @Nonnull
        public DependencyCollectorRequestBuilder 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(DependencyCoordinate)}: 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(DependencyCoordinate)} 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 DependencyCollectorRequestBuilder rootArtifact(@Nullable Artifact rootArtifact) {
            this.rootArtifact = rootArtifact;
            return this;
        }

        /**
         * @param root The root dependency
         * @return this request for chaining, never {@code null}
         */
        @Nonnull
        public DependencyCollectorRequestBuilder root(@Nonnull DependencyCoordinate 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 DependencyCollectorRequestBuilder dependencies(@Nullable List<DependencyCoordinate> 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 DependencyCollectorRequestBuilder dependency(@Nullable DependencyCoordinate 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 DependencyCollectorRequestBuilder managedDependencies(
                @Nullable List<DependencyCoordinate> 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 DependencyCollectorRequestBuilder managedDependency(@Nullable DependencyCoordinate 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 DependencyCollectorRequestBuilder verbose(boolean verbose) {
            this.verbose = verbose;
            return this;
        }

        @Nonnull
        public DependencyCollectorRequest build() {
            return new DefaultDependencyCollectorRequest(
                    session, project, rootArtifact, root, dependencies, managedDependencies, verbose);
        }

        static class DefaultDependencyCollectorRequest extends BaseRequest implements DependencyCollectorRequest {
            private final Project project;
            private final Artifact rootArtifact;
            private final DependencyCoordinate root;
            private final Collection<DependencyCoordinate> dependencies;
            private final Collection<DependencyCoordinate> managedDependencies;
            private final boolean verbose;

            /**
             * 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}.
             */
            DefaultDependencyCollectorRequest(
                    @Nonnull Session session,
                    @Nullable Project project,
                    @Nullable Artifact rootArtifact,
                    @Nullable DependencyCoordinate root,
                    @Nonnull Collection<DependencyCoordinate> dependencies,
                    @Nonnull Collection<DependencyCoordinate> managedDependencies,
                    boolean verbose) {
                super(session);
                this.project = project;
                this.rootArtifact = rootArtifact;
                this.root = root;
                this.dependencies = unmodifiable(nonNull(dependencies, "dependencies cannot be null"));
                this.managedDependencies =
                        unmodifiable(nonNull(managedDependencies, "managedDependencies cannot be null"));
                this.verbose = verbose;
            }

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

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

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

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

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

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

            @Nonnull
            @Override
            public String toString() {
                return getRoot() + " -> " + getDependencies();
            }
        }
    }
}
