// 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.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.docgen.annot.DocCategory;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.Nullable;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkValue;

/** The Starlark object optionally returned by a module extension's implementation function. */
@StarlarkBuiltin(
    name = "extension_metadata",
    category = DocCategory.BUILTIN,
    doc =
        "Return values of this type from a module extension's implementation function to "
            + "provide metadata about the repositories generated by the extension to Bazel.")
@AutoValue
public abstract class ModuleExtensionMetadata implements StarlarkValue {

  static final ModuleExtensionMetadata DEFAULT =
      create(
          /* explicitRootModuleDirectDeps= */ null,
          /* explicitRootModuleDirectDevDeps= */ null,
          UseAllRepos.NO,
          /* reproducible= */ false,
          /* facts= */ Facts.EMPTY);

  static final ModuleExtensionMetadata REPRODUCIBLE =
      create(
          /* explicitRootModuleDirectDeps= */ null,
          /* explicitRootModuleDirectDevDeps= */ null,
          UseAllRepos.NO,
          /* reproducible= */ true,
          /* facts= */ Facts.EMPTY);

  @Nullable
  abstract ImmutableSet<String> getExplicitRootModuleDirectDeps();

  @Nullable
  abstract ImmutableSet<String> getExplicitRootModuleDirectDevDeps();

  abstract UseAllRepos getUseAllRepos();

  abstract boolean getReproducible();

  abstract Facts getFacts();

  private static ModuleExtensionMetadata create(
      @Nullable Set<String> explicitRootModuleDirectDeps,
      @Nullable Set<String> explicitRootModuleDirectDevDeps,
      UseAllRepos useAllRepos,
      boolean reproducible,
      Facts facts) {
    return new AutoValue_ModuleExtensionMetadata(
        explicitRootModuleDirectDeps != null
            ? ImmutableSet.copyOf(explicitRootModuleDirectDeps)
            : null,
        explicitRootModuleDirectDevDeps != null
            ? ImmutableSet.copyOf(explicitRootModuleDirectDevDeps)
            : null,
        useAllRepos,
        reproducible,
        facts);
  }

  static ModuleExtensionMetadata create(
      Object rootModuleDirectDepsUnchecked,
      Object rootModuleDirectDevDepsUnchecked,
      boolean reproducible,
      Dict<String, Object> factsObj)
      throws EvalException {
    var facts = Facts.validateAndCreate(factsObj);

    if (rootModuleDirectDepsUnchecked == Starlark.NONE
        && rootModuleDirectDevDepsUnchecked == Starlark.NONE) {
      return create(null, null, UseAllRepos.NO, reproducible, facts);
    }

    // When root_module_direct_deps = "all", accept both root_module_direct_dev_deps = None and
    // root_module_direct_dev_deps = [], but not root_module_direct_dev_deps = ["some_repo"].
    if (rootModuleDirectDepsUnchecked.equals("all")
        && rootModuleDirectDevDepsUnchecked.equals(StarlarkList.immutableOf())) {
      return create(null, null, UseAllRepos.REGULAR, reproducible, facts);
    }

    if (rootModuleDirectDevDepsUnchecked.equals("all")
        && rootModuleDirectDepsUnchecked.equals(StarlarkList.immutableOf())) {
      return create(null, null, UseAllRepos.DEV, reproducible, facts);
    }

    if (rootModuleDirectDepsUnchecked.equals("all")
        || rootModuleDirectDevDepsUnchecked.equals("all")) {
      throw Starlark.errorf(
          "if one of root_module_direct_deps and root_module_direct_dev_deps is "
              + "\"all\", the other must be an empty list");
    }

    if (rootModuleDirectDepsUnchecked instanceof String
        || rootModuleDirectDevDepsUnchecked instanceof String) {
      throw Starlark.errorf(
          "root_module_direct_deps and root_module_direct_dev_deps must be "
              + "None, \"all\", or a list of strings");
    }
    if ((rootModuleDirectDepsUnchecked == Starlark.NONE)
        != (rootModuleDirectDevDepsUnchecked == Starlark.NONE)) {
      throw Starlark.errorf(
          "root_module_direct_deps and root_module_direct_dev_deps must both be "
              + "specified or both be unspecified");
    }

    Sequence<String> rootModuleDirectDeps =
        Sequence.cast(rootModuleDirectDepsUnchecked, String.class, "root_module_direct_deps");
    Sequence<String> rootModuleDirectDevDeps =
        Sequence.cast(
            rootModuleDirectDevDepsUnchecked, String.class, "root_module_direct_dev_deps");

    Set<String> explicitRootModuleDirectDeps = new LinkedHashSet<>();
    for (String dep : rootModuleDirectDeps) {
      try {
        RepositoryName.validateUserProvidedRepoName(dep);
      } catch (EvalException e) {
        throw Starlark.errorf("in root_module_direct_deps: %s", e.getMessage());
      }
      if (!explicitRootModuleDirectDeps.add(dep)) {
        throw Starlark.errorf("in root_module_direct_deps: duplicate entry '%s'", dep);
      }
    }

    Set<String> explicitRootModuleDirectDevDeps = new LinkedHashSet<>();
    for (String dep : rootModuleDirectDevDeps) {
      try {
        RepositoryName.validateUserProvidedRepoName(dep);
      } catch (EvalException e) {
        throw Starlark.errorf("in root_module_direct_dev_deps: %s", e.getMessage());
      }
      if (explicitRootModuleDirectDeps.contains(dep)) {
        throw Starlark.errorf(
            "in root_module_direct_dev_deps: entry '%s' is also in " + "root_module_direct_deps",
            dep);
      }
      if (!explicitRootModuleDirectDevDeps.add(dep)) {
        throw Starlark.errorf("in root_module_direct_dev_deps: duplicate entry '%s'", dep);
      }
    }

    return create(
        explicitRootModuleDirectDeps,
        explicitRootModuleDirectDevDeps,
        UseAllRepos.NO,
        reproducible,
        facts);
  }

  enum UseAllRepos {
    NO,
    REGULAR,
    DEV,
  }
}
