// Copyright 2022 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.lib.bazel.bzlmod;

import static java.util.Objects.requireNonNull;

import com.google.auto.value.AutoBuilder;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashMap;
import java.util.Map;

/**
 * The result of running Bazel module inspection pre-processing, containing the un-pruned and
 * augmented wrappers of the Bazel module dependency graph (post-version-resolution).
 *
 * @param depGraph The (bidirectional) inspection dep graph, containing wrappers of the {@link
 *     Module}, augmented with references to dependants. The order is non-deterministic, inherited
 *     from the {@code completeDepGraph} of {@link BazelModuleResolutionValue}. For any KEY in the
 *     returned map, it's guaranteed that {@code depGraph[KEY].getKey() == KEY}.
 * @param modulesIndex Index of all module keys mentioned in the un-pruned dep graph (loaded or not)
 *     for easy lookup. It is a map from <i> module name </i> to the set of {@link ModuleKey}s that
 *     point to a version of that module.
 * @param extensionToRepoInternalNames A mapping from a module extension ID, to the list of
 *     "internal" names of the repos generated by that extension. The "internal" name is the name
 *     directly used by the extension when instantiating a repo rule.
 * @param moduleKeyToCanonicalNames A mapping from a module key to the canonical repository name of
 *     the module repository.
 */
public record BazelModuleInspectorValue(
    ImmutableMap<ModuleKey, AugmentedModule> depGraph,
    ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex,
    ImmutableSetMultimap<ModuleExtensionId, String> extensionToRepoInternalNames,
    ImmutableMap<ModuleKey, RepositoryName> moduleKeyToCanonicalNames,
    ImmutableList<ExternalDepsException> errors)
    implements SkyValue {
  public BazelModuleInspectorValue {
    requireNonNull(depGraph, "depGraph");
    requireNonNull(modulesIndex, "modulesIndex");
    requireNonNull(extensionToRepoInternalNames, "extensionToRepoInternalNames");
    requireNonNull(moduleKeyToCanonicalNames, "moduleKeyToCanonicalNames");
    requireNonNull(errors, "errors");
  }

  @SerializationConstant
  public static final SkyKey KEY = () -> SkyFunctions.BAZEL_MODULE_INSPECTION;

  public static BazelModuleInspectorValue create(
      ImmutableMap<ModuleKey, AugmentedModule> depGraph,
      ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex,
      ImmutableSetMultimap<ModuleExtensionId, String> extensionToRepoInternalNames,
      ImmutableMap<ModuleKey, RepositoryName> moduleKeyToCanonicalNames,
      ImmutableList<ExternalDepsException> errors) {
    return new BazelModuleInspectorValue(
        depGraph, modulesIndex, extensionToRepoInternalNames, moduleKeyToCanonicalNames, errors);
  }

  /**
   * A wrapper for {@link Module}, augmented with references to dependants (and also those who are
   * not used in the final dep graph).
   *
   * @param name Name of the module. Same as in {@link Module}.
   * @param version Version of the module. Same as in {@link Module}.
   * @param key {@link ModuleKey} of this module. Same as in {@link Module}
   * @param repoName The apparent name used by the module to refer to its own repository.
   * @param dependants The set of modules in the resolved dep graph that depend on this module
   *     <strong> after </strong> the module resolution.
   * @param originalDependants The set of modules in the complete dep graph that originally depended
   *     on this module *before* the module resolution (can contain unused nodes).
   * @param deps A bi-map from the repo names of the resolved dependencies of this module to their
   *     actual module keys. The inverse lookup is used to check how and why the dependency changed
   *     using the other maps.
   * @param unusedDeps A bi-map from the repo names of the unused dependencies of this module to
   *     their actual module keys. The inverse lookup is used to check how and why the dependency
   *     changed using the other maps.
   * @param depReasons A map from the repo name of the dependencies of this module to the rules that
   *     were used for their resolution (can be either the original dependency, changed by the
   *     Minimal-Version Selection algorithm or by an override rule.
   * @param loaded Flag that tell whether the module was loaded and added to the dependency graph.
   *     Modules overridden by {@code single_version_override} and {@link NonRegistryOverride} are
   *     not loaded so their {@code originalDeps} are (yet) unknown.
   */
  public record AugmentedModule(
      String name,
      Version version,
      ModuleKey key,
      String repoName,
      ImmutableSet<ModuleKey> dependants,
      ImmutableSet<ModuleKey> originalDependants,
      ImmutableBiMap<String, ModuleKey> deps,
      ImmutableBiMap<String, ModuleKey> unusedDeps,
      ImmutableMap<String, ResolutionReason> depReasons,
      boolean loaded) {
    public AugmentedModule {
      requireNonNull(name, "name");
      requireNonNull(version, "version");
      requireNonNull(key, "key");
      requireNonNull(repoName, "repoName");
      requireNonNull(dependants, "dependants");
      requireNonNull(originalDependants, "originalDependants");
      requireNonNull(deps, "deps");
      requireNonNull(unusedDeps, "unusedDeps");
      requireNonNull(depReasons, "depReasons");
    }

    /**
     * Shortcut for retrieving the sorted union of both used and unused deps based on the unused
     * flag.
     */
    public ImmutableSortedMap<ModuleKey, String> getAllDeps(boolean unused) {
      if (!unused) {
        return ImmutableSortedMap.copyOf(deps().inverse(), ModuleKey.LEXICOGRAPHIC_COMPARATOR);
      } else {
        Map<ModuleKey, String> map = new HashMap<>();
        map.putAll(deps().inverse());
        map.putAll(unusedDeps().inverse());
        return ImmutableSortedMap.copyOf(map, ModuleKey.LEXICOGRAPHIC_COMPARATOR);
      }
    }

    /** Flag for checking whether the module is present in the resolved dep graph. */
    public boolean isUsed() {
      return key().equals(ModuleKey.ROOT) || !dependants().isEmpty();
    }

    /** Returns a new {@link AugmentedModule.Builder} with {@code key} set. */
    public static AugmentedModule.Builder builder(ModuleKey key) {
      return new AutoBuilder_BazelModuleInspectorValue_AugmentedModule_Builder()
          .setName(key.name())
          .setVersion(key.version())
          .setRepoName(key.name())
          .setKey(key)
          .setLoaded(false);
    }

    /** Builder type for {@link AugmentedModule}. */
    @AutoBuilder
    public abstract static class Builder {
      public abstract AugmentedModule.Builder setName(String value);

      public abstract AugmentedModule.Builder setVersion(Version value);

      public abstract AugmentedModule.Builder setKey(ModuleKey value);

      public abstract AugmentedModule.Builder setRepoName(String value);

      public abstract AugmentedModule.Builder setLoaded(boolean value);

      abstract ImmutableSet.Builder<ModuleKey> originalDependantsBuilder();

      @CanIgnoreReturnValue
      public AugmentedModule.Builder addOriginalDependant(ModuleKey depKey) {
        originalDependantsBuilder().add(depKey);
        return this;
      }

      abstract ImmutableSet.Builder<ModuleKey> dependantsBuilder();

      @CanIgnoreReturnValue
      public AugmentedModule.Builder addDependant(ModuleKey depKey) {
        dependantsBuilder().add(depKey);
        return this;
      }

      abstract ImmutableBiMap.Builder<String, ModuleKey> depsBuilder();

      @CanIgnoreReturnValue
      public AugmentedModule.Builder addDep(String repoName, ModuleKey key) {
        depsBuilder().put(repoName, key);
        return this;
      }

      abstract ImmutableBiMap.Builder<String, ModuleKey> unusedDepsBuilder();

      @CanIgnoreReturnValue
      public AugmentedModule.Builder addUnusedDep(String repoName, ModuleKey key) {
        unusedDepsBuilder().put(repoName, key);
        return this;
      }

      abstract ImmutableMap.Builder<String, ResolutionReason> depReasonsBuilder();

      @CanIgnoreReturnValue
      public AugmentedModule.Builder addDepReason(String repoName, ResolutionReason reason) {
        depReasonsBuilder().put(repoName, reason);
        return this;
      }

      public abstract AugmentedModule build();
    }

    /** The reason why a final dependency of a module was resolved the way it was. */
    public enum ResolutionReason {
      /** The dependency is the original dependency defined in the MODULE.bazel file. */
      ORIGINAL(""),
      /** The dependency was replaced by the Minimal-Version Selection algorithm. */
      MINIMAL_VERSION_SELECTION("MVS"),
      /** The dependency was replaced by a {@code single_version_override} rule. */
      SINGLE_VERSION_OVERRIDE("SVO"),
      /** The dependency was replaced by a {@code multiple_version_override} rule. */
      MULTIPLE_VERSION_OVERRIDE("MVO"),
      /** The dependency was replaced by one of the {@link NonRegistryOverride} rules. */
      NON_REGISTRY_OVERRIDE("NRO");

      private final String label;

      ResolutionReason(String label) {
        this.label = label;
      }

      public String getLabel() {
        return label;
      }
    }
  }
}
