// Copyright 2021 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 com.google.common.collect.ImmutableMap;
import com.google.devtools.build.docgen.annot.DocCategory;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
import com.google.devtools.build.lib.bazel.repository.starlark.StarlarkBaseExternalContext;
import com.google.devtools.build.lib.runtime.ProcessWrapper;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutor;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import java.util.Map;
import javax.annotation.Nullable;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.ParamType;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.NoneType;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkSemantics;

/** The Starlark object passed to the implementation function of module extensions. */
@StarlarkBuiltin(
    name = "module_ctx",
    category = DocCategory.BUILTIN,
    doc =
        "The context of the module extension containing helper functions and information about"
            + " pertinent tags across the dependency graph. You get a module_ctx object as an"
            + " argument to the <code>implementation</code> function when you create a module"
            + " extension.")
public class ModuleExtensionContext extends StarlarkBaseExternalContext {
  private final ModuleExtensionId extensionId;
  private final StarlarkList<StarlarkBazelModule> modules;
  private final Facts facts;
  private final boolean rootModuleHasNonDevDependency;

  protected ModuleExtensionContext(
      Path workingDirectory,
      BlazeDirectories directories,
      Environment env,
      Map<String, String> envVariables,
      DownloadManager downloadManager,
      double timeoutScaling,
      @Nullable ProcessWrapper processWrapper,
      StarlarkSemantics starlarkSemantics,
      @Nullable RepositoryRemoteExecutor remoteExecutor,
      ModuleExtensionId extensionId,
      StarlarkList<StarlarkBazelModule> modules,
      Facts facts,
      boolean rootModuleHasNonDevDependency) {
    super(
        workingDirectory,
        directories,
        env,
        envVariables,
        downloadManager,
        timeoutScaling,
        processWrapper,
        starlarkSemantics,
        ModuleExtensionEvaluationProgress.moduleExtensionEvaluationContextString(extensionId),
        remoteExecutor,
        /* allowWatchingPathsOutsideWorkspace= */ false);
    this.extensionId = extensionId;
    this.modules = modules;
    this.facts = facts;
    this.rootModuleHasNonDevDependency = rootModuleHasNonDevDependency;
  }

  @Override
  protected boolean shouldDeleteWorkingDirectoryOnClose(boolean successful) {
    // The contents of the working directory are purely ephemeral, only the repos instantiated by
    // the extension are considered its results.
    return true;
  }

  @Override
  protected boolean isRemotable() {
    // Maybe we can some day support remote execution, but not today.
    return false;
  }

  @Override
  protected ImmutableMap<String, String> getRemoteExecProperties() {
    return ImmutableMap.of();
  }

  @StarlarkMethod(
      name = "modules",
      structField = true,
      doc =
          "A list of all the Bazel modules in the external dependency graph that use this module "
              + "extension, each of which is a <a href=\"../builtins/bazel_module.html\">"
              + "bazel_module</a> object that exposes all the tags it specified for this extension."
              + " The iteration order of this dictionary is guaranteed to be the same as"
              + " breadth-first search starting from the root module.")
  public StarlarkList<StarlarkBazelModule> getModules() {
    return modules;
  }

  @StarlarkMethod(
      name = "facts",
      structField = true,
      doc =
          """
          The JSON-like dict returned by a previous execution of this extension in the `facts`
          parameter of [`extension_metadata`](../builtins/module_ctx#extension_metadata) or else
          `{}`.
          This is useful for extensions that want to preserve universally true facts such as the
          hashes of artifacts in an immutable repository.
          Note that the returned value may have been created by a different version of the
          extension, which may have used a different schema.
          """)
  public Facts getFacts() {
    return facts;
  }

  @StarlarkMethod(
      name = "is_dev_dependency",
      doc =
          "Returns whether the given tag was specified on the result of a <a "
              + "href=\"../globals/module.html#use_extension\">use_extension</a> call with "
              + "<code>devDependency = True</code>.",
      parameters = {
        @Param(
            name = "tag",
            doc =
                "A tag obtained from <a"
                    + " href=\"../builtins/bazel_module.html#tags\">bazel_module.tags</a>.",
            allowedTypes = {@ParamType(type = TypeCheckedTag.class)})
      })
  public boolean isDevDependency(TypeCheckedTag tag) {
    return tag.isDevDependency();
  }

  @StarlarkMethod(
      name = "is_isolated",
      doc =
          "Whether this particular usage of the extension had <code>isolate = True</code> "
              + "specified and is thus isolated from all other usages."
              + "<p>This field is currently experimental and only available with the flag "
              + "<code>--experimental_isolated_extension_usages</code>.",
      structField = true,
      enableOnlyWithFlag = "-experimental_isolated_extension_usages")
  public boolean isIsolated() {
    return extensionId.isolationKey().isPresent();
  }

  @StarlarkMethod(
      name = "root_module_has_non_dev_dependency",
      doc = "Whether the root module uses this extension as a non-dev dependency.",
      structField = true)
  public boolean rootModuleHasNonDevDependency() {
    return rootModuleHasNonDevDependency;
  }

  @StarlarkMethod(
      name = "extension_metadata",
      doc =
          "Constructs an opaque object that can be returned from the module extension's"
              + " implementation function to provide metadata about the repositories generated by"
              + " the extension to Bazel.",
      parameters = {
        @Param(
            name = "root_module_direct_deps",
            doc =
                "The names of the repositories that the extension considers to be direct"
                    + " dependencies of the root module. If the root module imports additional"
                    + " repositories or does not import all of these repositories via <a"
                    + " href=\"../globals/module.html#use_repo\"><code>use_repo</code></a>, Bazel"
                    + " will print a warning when the extension is evaluated, instructing the user"
                    + " to run <code>bazel mod tidy</code> to fix the <code>use_repo</code> calls"
                    + " automatically. <p>If one of <code>root_module_direct_deps</code> and"
                    + " will print a warning and a fixup command when the extension is"
                    + " evaluated.<p>If one of <code>root_module_direct_deps</code> and"
                    + " <code>root_module_direct_dev_deps</code> is specified, the other has to be"
                    + " as well. The lists specified by these two parameters must be"
                    + " disjoint.<p>Exactly one of <code>root_module_direct_deps</code> and"
                    + " <code>root_module_direct_dev_deps</code> can be set to the special value"
                    + " <code>\"all\"</code>, which is treated as if a list with the names of"
                    + " all repositories generated by the extension was specified as the value.",
            positional = false,
            named = true,
            defaultValue = "None",
            allowedTypes = {
              @ParamType(type = Sequence.class, generic1 = String.class),
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class)
            }),
        @Param(
            name = "root_module_direct_dev_deps",
            doc =
                "The names of the repositories that the extension considers to be direct dev"
                    + " dependencies of the root module. If the root module imports additional"
                    + " repositories or does not import all of these repositories via <a"
                    + " href=\"../globals/module.html#use_repo\"><code>use_repo</code></a> on an"
                    + " extension proxy created with <code><a"
                    + " href=\"../globals/module.html#use_extension\">use_extension</a>(...,"
                    + " dev_dependency = True)</code>, Bazel will print a warning when the "
                    + " extension is evaluated, instructing the user to run"
                    + " <code>bazel mod tidy</code> to fix the <code>use_repo</code> calls"
                    + " automatically. <p>If one of <code>root_module_direct_deps</code> and"
                    + " <code>root_module_direct_dev_deps</code> is specified, the other has to be"
                    + " as well. The lists specified by these two parameters must be"
                    + " disjoint.<p>Exactly one of <code>root_module_direct_deps</code> and"
                    + " <code>root_module_direct_dev_deps</code> can be set to the special value"
                    + " <code>\"all\"</code>, which is treated as if a list with the names of"
                    + " all repositories generated by the extension was specified as the value.",
            positional = false,
            named = true,
            defaultValue = "None",
            allowedTypes = {
              @ParamType(type = Sequence.class, generic1 = String.class),
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class)
            }),
        @Param(
            name = "reproducible",
            doc =
                "States that this module extension ensures complete reproducibility, thereby it "
                    + "should not be stored in the lockfile.",
            positional = false,
            named = true,
            defaultValue = "False",
            allowedTypes = {
              @ParamType(type = Boolean.class),
            }),
        @Param(
            name = "facts",
            doc =
                """
                A JSON-like dict that is made available to future executions of this extension via
                the `module_ctx.facts` property.
                This is useful for extensions that want to preserve universally true facts such as
                the hashes of artifacts in an immutable repository.

                Bazel may shallowly merge multiple facts dicts returned by different versions of the
                extension in order to resolve merge conflicts on the MODULE.bazel.lock file, as if
                by applying the `dict.update()` method or the `|` operator in Starlark. Extensions
                should use facts for key-value storage only and ensure that the key uniquely
                determines the value, although perhaps only via additional information and network
                access. An extension can opt out of this merging by providing a dict with a single,
                fixed top-level key and an arbitrary value.

                Note that the value provided here may be read back by a different version of the
                extension, so either include a version number or use a schema that is unlikely to
                result in ambiguities.
                """,
            positional = false,
            named = true,
            defaultValue = "{}",
            allowedTypes = {
              @ParamType(type = Dict.class, generic1 = String.class),
            }),
      })
  public ModuleExtensionMetadata extensionMetadata(
      Object rootModuleDirectDepsUnchecked,
      Object rootModuleDirectDevDepsUnchecked,
      boolean reproducible,
      Object facts)
      throws EvalException {
    return ModuleExtensionMetadata.create(
        rootModuleDirectDepsUnchecked,
        rootModuleDirectDevDepsUnchecked,
        reproducible,
        Dict.cast(facts, String.class, Object.class, "facts"));
  }
}
