// Copyright 2014 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.pkgcache;


import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.packages.RuleVisibility;
import com.google.devtools.build.lib.util.ResourceConverter;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsClass;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.List;
import java.util.Objects;

/** Options for configuring Packages -- loading and default behaviors. */
@OptionsClass
public abstract class PackageOptions extends OptionsBase {

  /** Converter for the {@code --default_visibility} option. */
  public static class DefaultVisibilityConverter extends Converter.Contextless<RuleVisibility> {
    @Override
    public RuleVisibility convert(String input) throws OptionsParsingException {
      return switch (input) {
        case "public" -> RuleVisibility.PUBLIC;
        case "private" -> RuleVisibility.PRIVATE;
        default ->
            throw new OptionsParsingException(
                "Not a valid default visibility: '" + input + "' (should be 'public' or 'private'");
      };
    }

    @Override
    public String getTypeDescription() {
      return "default visibility";
    }
  }

  /** Converter for globbing threads. */
  public static class ParallelismConverter extends ResourceConverter.IntegerConverter {
    public ParallelismConverter() throws OptionsParsingException {
      super(/* auto= */ HOST_CPUS_SUPPLIER, /* minValue= */ 1, /* maxValue= */ Integer.MAX_VALUE);
    }
  }

  @Option(
      name = "package_path",
      defaultValue = "%workspace%",
      converter = Converters.ColonSeparatedOptionListConverter.class,
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "A colon-separated list of where to look for packages. "
              + "Elements beginning with '%workspace%' are relative to the enclosing "
              + "workspace. If omitted or empty, the default is the output of "
              + "'bazel info default-package-path'.")
  public abstract List<String> getPackagePath();

  public abstract void setPackagePath(List<String> value);

  @Option(
      name = "show_loading_progress",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "If enabled, causes Bazel to print \"Loading package:\" messages.")
  public abstract boolean getShowLoadingProgress();

  public abstract void setShowLoadingProgress(boolean value);

  @Option(
      name = "deleted_packages",
      allowMultiple = true,
      defaultValue = "null",
      converter = CommaSeparatedPackageNameListConverter.class,
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "A comma-separated list of names of packages which the "
              + "build system will consider non-existent, even if they are "
              + "visible somewhere on the package path.\n"
              + "Use this option when deleting a subpackage 'x/y' of an "
              + "existing package 'x'.  For example, after deleting x/y/BUILD "
              + "in your client, the build system may complain if it "
              + "encounters a label '//x:y/z' if that is still provided by another "
              + "package_path entry.  Specifying --deleted_packages x/y avoids this "
              + "problem.")
  public abstract List<PackageIdentifier> getDeletedPackages();

  @Option(
      name = "default_visibility",
      defaultValue = "private",
      converter = DefaultVisibilityConverter.class,
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Default visibility for packages that don't set it explicitly ('public' or 'private').")
  public abstract RuleVisibility getDefaultVisibility();

  public abstract void setDefaultVisibility(RuleVisibility value);

  @Option(
      name = "incompatible_enforce_config_setting_visibility",
      // TODO(b/179944632): set this and --incompatible_config_setting_private_default_visibility
      // to true, then make these no-ops, then remove.
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
      effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
      metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE},
      help =
          "If true, enforce config_setting visibility restrictions. If false, every "
              + "config_setting is visible to every target. See "
              + "https://github.com/bazelbuild/bazel/issues/12932.")
  public abstract boolean getEnforceConfigSettingVisibility();

  @Option(
      name = "incompatible_config_setting_private_default_visibility",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
      effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
      metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE},
      help =
          "If incompatible_enforce_config_setting_visibility=false, this is a noop. Else, if this"
              + " flag is false, any config_setting without an explicit visibility attribute is"
              + " //visibility:public. If this flag is true, config_setting follows the same"
              + " visibility logic as all other rules. See"
              + " https://github.com/bazelbuild/bazel/issues/12933.")
  public abstract boolean getConfigSettingPrivateDefaultVisibility();

  @Option(
      name = "legacy_globbing_threads",
      defaultValue = "100",
      converter = ParallelismConverter.class,
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Number of threads to use for glob evaluation. Takes "
              + ResourceConverter.FLAG_SYNTAX
              + ". \"auto\" means to use a reasonable value derived from the machine's hardware"
              + " profile (e.g. the number of processors).")
  public abstract int getGlobbingThreads();

  public abstract void setGlobbingThreads(int value);

  @Option(
      name = "experimental_max_directories_to_eagerly_visit_in_globbing",
      defaultValue = "-1",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "If non-negative, the first time a glob is evaluated in a package, the subdirectories of"
              + " the package will be traversed in order to warm filesystem caches and compensate"
              + " for lack of parallelism in globbing. At most this many directories will be"
              + " visited.")
  public abstract int getMaxDirectoriesToEagerlyVisitInGlobbing();

  @Option(
      name = "fetch",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Allows the command to fetch external dependencies. If set to false, the command will"
              + " utilize any cached version of the dependency, and if none exists, the command"
              + " will result in failure.")
  public abstract boolean getFetch();

  @Option(
      name = "experimental_check_output_files",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Check for modifications made to the output files of a build. Consider setting "
              + "this flag to false if you don't expect these files to change outside of bazel "
              + "since it will speed up subsequent runs as they won't have to check a "
              + "previous run's cache.")
  public abstract boolean getCheckOutputFiles();

  public abstract void setCheckOutputFiles(boolean value);

  @Option(
      name = "experimental_check_external_other_files",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Check for modifications made to the non-output, non-repo external files, e.g. host"
              + " files.")
  public abstract boolean getCheckExternalOtherFiles();

  public abstract void setCheckExternalOtherFiles(boolean value);

  // TODO(https://github.com/bazelbuild/bazel/issues/25539) - at present, lazy macro expansion is
  // incompatible with non-finalizer use of native.existing_rules(). Once we can load all packages
  // in lazy macro expansion mode, we might evolve this option to be an allowlist/denylist for
  // performance reasons - which would mean supporting negation or package_group()-style
  // subpackage patterns.
  @Option(
      name = "experimental_lazy_macro_expansion_packages",
      defaultValue = "",
      converter = LazyMacroExpansionPackages.OptionConverter.class,
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.LOSES_INCREMENTAL_STATE},
      metadataTags = {OptionMetadataTag.EXPERIMENTAL},
      help = "List of packages in which symbolic macro are expanded only if necessary.")
  public abstract LazyMacroExpansionPackages getLazyMacroExpansionPackages();

  public abstract void setLazyMacroExpansionPackages(LazyMacroExpansionPackages value);

  /** A converter from strings containing comma-separated names of packages to lists of strings. */
  public static class CommaSeparatedPackageNameListConverter
      extends Converter.Contextless<List<PackageIdentifier>> {

    private static final Splitter COMMA_SPLITTER = Splitter.on(',');

    @Override
    public List<PackageIdentifier> convert(String input) throws OptionsParsingException {
      if (Strings.isNullOrEmpty(input)) {
        return ImmutableList.of();
      }
      ImmutableList.Builder<PackageIdentifier> list = ImmutableList.builder();
      for (String s : COMMA_SPLITTER.split(input)) {
        try {
          list.add(PackageIdentifier.parse(s));
        } catch (LabelSyntaxException e) {
          throw new OptionsParsingException(e.getMessage());
        }
      }
      return list.build();
    }

    @Override
    public String getTypeDescription() {
      return "comma-separated list of package names";
    }
  }

  public ImmutableSet<PackageIdentifier> getDeletedPackagesOrEmptySet() {
    if (getDeletedPackages() == null) {
      return ImmutableSet.of();
    }
    return ImmutableSet.copyOf(getDeletedPackages());
  }

  /**
   * The set of packages in which symbolic macros are to be expanded lazily. Used by Skyframe and by
   * PackageLoader.
   */
  public interface LazyMacroExpansionPackages {
    /**
     * A {@link LazyMacroExpansionPackages} indicating that no packages should have symbolic macros
     * expanded lazily.
     */
    public static final LazyMacroExpansionPackages NONE =
        new LazyMacroExpansionPackagesSet(ImmutableSet.of());

    /**
     * A {@link LazyMacroExpansionPackages} indicating that all packages should have symbolic macros
     * expanded lazily.
     */
    public static final LazyMacroExpansionPackages ALL =
        new LazyMacroExpansionPackages() {
          @Override
          public boolean contains(PackageIdentifier packageId) {
            return true;
          }
        };

    /** Returns true if symbolic macros in the given package should be expanded lazily. */
    boolean contains(PackageIdentifier packageId);

    /** {@link Converter} for {@link LazyMacroExpansionPackages}. */
    public static class OptionConverter extends Converter.Contextless<LazyMacroExpansionPackages> {

      private static final CommaSeparatedNonEmptyOptionListConverter stringConverter =
          new CommaSeparatedNonEmptyOptionListConverter();

      @Override
      public LazyMacroExpansionPackages convert(String input) throws OptionsParsingException {
        ImmutableList<String> strings = stringConverter.convert(input);
        if (strings.isEmpty()) {
          return LazyMacroExpansionPackages.NONE;
        } else if (strings.contains("*")) {
          return LazyMacroExpansionPackages.ALL;
        } else {
          ImmutableSet.Builder<PackageIdentifier> packageIds = ImmutableSet.builder();
          for (String s : strings) {
            try {
              packageIds.add(PackageIdentifier.parse(s));
            } catch (LabelSyntaxException e) {
              throw new OptionsParsingException(e.getMessage());
            }
          }
          return new LazyMacroExpansionPackagesSet(packageIds.build());
        }
      }

      @Override
      public String getTypeDescription() {
        return "comma-separated list of package names; or '*' to indicate all packages";
      }
    }
  }

  private static final class LazyMacroExpansionPackagesSet implements LazyMacroExpansionPackages {
    private final ImmutableSet<PackageIdentifier> packageIds;

    private LazyMacroExpansionPackagesSet(ImmutableSet<PackageIdentifier> packageIds) {
      this.packageIds = packageIds;
    }

    @Override
    public boolean contains(PackageIdentifier packageId) {
      return packageIds.contains(packageId);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      return o instanceof LazyMacroExpansionPackagesSet other
          && Objects.equals(packageIds, other.packageIds);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(packageIds);
    }

    @Override
    public String toString() {
      return String.format("LazyMacroExpansionPackages[%s]", packageIds);
    }
  }
}
