// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.workspace.maven;


import static java.util.stream.Collectors.toList;

import com.google.common.collect.Lists;
import java.util.List;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.resolution.VersionRangeRequest;
import org.eclipse.aether.resolution.VersionRangeResolutionException;
import org.eclipse.aether.resolution.VersionRangeResult;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.filter.DependencyFilterUtils;
import org.eclipse.aether.util.graph.manager.ClassicDependencyManager;
import org.eclipse.aether.version.Version;

/**
 * Facade around aether. This class is used to make various requests to the aether subsystem.
 */
public class Aether {

  static final String MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2/";

  RepositorySystem repositorySystem;

  RepositorySystemSession repositorySystemSession;

  List<RemoteRepository> remoteRepositories;

  private Aether(RepositorySystem repositorySystem,
      RepositorySystemSession session, List<RemoteRepository> remoteRepositories) {
    this.repositorySystem = repositorySystem;
    this.repositorySystemSession = session;
    this.remoteRepositories = remoteRepositories;
  }

  public void addRemoteRepository(RemoteRepository repo) {
    this.remoteRepositories.add(repo);
  }

  public List<RemoteRepository> getRemoteRepositories() {
    return this.remoteRepositories;
  }

  /** Given an artifacts requests a version range for it. */
  List<String> requestVersionRange(Artifact artifact) throws VersionRangeResolutionException {
    VersionRangeRequest rangeRequest = new VersionRangeRequest(artifact, remoteRepositories, null);
    VersionRangeResult result = repositorySystem.resolveVersionRange(repositorySystemSession, rangeRequest);
    return result.getVersions().stream().map(Version::toString).collect(toList());
  }

  /**
   * Creates a CollectRequest, i.e. a request to collect transitive dependencies and to build
   * a dependency graph from them. It accomplishes this through a list of direct dependencies.
   */
  CollectRequest createCollectRequest(
      List<Dependency> directDependencies, List<Dependency> managedDependencies) {
    return new CollectRequest(directDependencies, managedDependencies, remoteRepositories);
  }

  /**
   * Creates a DependencyRequest, i.e. a request to resolve transitive dependencies, from
   * a collect request.
   */
  //TODO(petros): add some means to add exclusions.
  DependencyRequest createDependencyRequest(CollectRequest collectRequest, DependencyFilter filter) {
    return new DependencyRequest(collectRequest, filter);
  }

  DependencyRequest createDependencyRequest(CollectRequest collectRequest) {
    DependencyFilter compileFilter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE);
    return createDependencyRequest(collectRequest, compileFilter);
  }

  /**
   * Given a dependency request, this makes the call to aether to transitively resolve the
   * dependencies.
   */
  DependencyResult requestDependencyResolution(DependencyRequest dependencyRequest)
      throws DependencyResolutionException {
    return repositorySystem.resolveDependencies(repositorySystemSession, dependencyRequest);
  }

  /** TODO(petros): this is a hack until I replace the existing Resolver. */
  static Aether defaultOption() {
    return new Aether.Builder().build();
  }

  static Aether.Builder builder() {
    return new Aether.Builder();
  }

  /** Builder class for convenience and flexibility. */
  static class Builder {
    private List<RemoteRepository> remoteRepositories;
    private RepositorySystem repositorySystem;
    private RepositorySystemSession repositorySystemSession;

    Builder() {
      remoteRepositories = Lists.newArrayList(Utilities.mavenCentralRepository());
      repositorySystem = Utilities.newRepositorySystem();
      repositorySystemSession = Utilities.newRepositorySession(repositorySystem);
    }

    Builder remoteRepos(List<RemoteRepository> remoteRepositories) {
      this.remoteRepositories = remoteRepositories;
      return this;
    }

    /**
     * Provide Repository System Sessions and RepositorySystems together since
     * Repository sessions are associated to a particular to a Repository System.
     */
    Builder systemSession(RepositorySystemSession session, RepositorySystem system) {
      this.repositorySystemSession = session;
      this.repositorySystem = system;
      return this;
    }

    Aether build() {
      return new Aether(repositorySystem, repositorySystemSession, remoteRepositories);
    }
  }

  /**
   * A set of utility methods for creating repository systems, repository system sessions and
   * remote repositories.
   */
  static class Utilities {

    /* Creates a new aether repository system. */
    static RepositorySystem newRepositorySystem() {
      DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
      locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
      locator.addService(TransporterFactory.class, FileTransporterFactory.class);
      locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
      return locator.getService(RepositorySystem.class);
    }

    /**
     * Aether and its components are designed to be stateless. All configurations and state
     * are provided through the RepositorySystemSession.
     *
     * TODO(petros): Separate this out into its own class.
     * This is the most intricate element of Aether.
     * There are various settings that are useful to us.
     * Specifically, these are the (1) LocalRepositoryManager, (2) DependencyManager,
     * (3) DependencyGraphTransformer, (4) TransferListener, (5) ProxySelector
     */
    static RepositorySystemSession newRepositorySession(RepositorySystem system) {
      DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();

      // TODO(petros): Decide on where to cache things.
      LocalRepository localRepository = new LocalRepository("target/local-repo");
      session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepository));

      session.setDependencyManager(new ClassicDependencyManager());
      return session;
    }

    /** Creates an instance of the maven central repository */
    static RemoteRepository mavenCentralRepository() {
      return new RemoteRepository.Builder( "central", "default", MAVEN_CENTRAL_URL).build();
    }
  }

}