// 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.runtime;

import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.Converters.RangeConverter;
import com.google.devtools.common.options.EnumConverter;
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.HashSet;
import java.util.List;

/** Command-line UI options. */
@OptionsClass
public abstract class UiOptions extends OptionsBase {

  /** Enum to select whether color output is enabled or not. */
  public enum UseColor {
    YES,
    NO,
    AUTO
  }

  /** Enum to select whether curses output is enabled or not. */
  public enum UseCurses {
    YES,
    NO,
    AUTO
  }

  /** Converter for {@link EventKind} filters * */
  public static class EventFiltersConverter
      extends Converter.Contextless<EventFiltersConverter.EventKindFilters> {

    /** Container for an EventKind input filter. */
    public record EventKindFilters(
        ImmutableSet<EventKind> filteredEventKinds, ImmutableSet<EventKind> unfilteredEventKinds) {
      public EventKindFilters {
        requireNonNull(filteredEventKinds, "filteredEventKinds");
        requireNonNull(unfilteredEventKinds, "unfilteredEventKinds");
      }

      public static EventKindFilters from(
          ImmutableSet<EventKind> filtered, ImmutableSet<EventKind> unfiltered) {
        return new EventKindFilters(filtered, unfiltered);
      }
    }

    private final CommaSeparatedOptionListConverter commaSeparatedListConverter;
    private final EnumConverter<EventKind> eventKindConverter;

    public EventFiltersConverter() {
      this.commaSeparatedListConverter = new CommaSeparatedOptionListConverter();
      this.eventKindConverter = new EnumConverter<>(EventKind.class, "event kind") {};
    }

    @Override
    public EventKindFilters convert(String input) throws OptionsParsingException {
      if (input.isEmpty()) {
        // This method is not called to convert the default value
        // Empty list means that the user wants to filter all events
        return EventKindFilters.from(EventKind.ALL_EVENTS, ImmutableSet.of());
      }
      ImmutableList<String> filters =
          commaSeparatedListConverter.convert(input, /* conversionContext= */ null);

      HashSet<EventKind> filteredEventKinds = new HashSet<>();
      HashSet<EventKind> unfilteredEventKinds = new HashSet<>();

      for (String filter : filters) {
        if (!filter.startsWith("+") && !filter.startsWith("-")) {
          filteredEventKinds.addAll(EventKind.ALL_EVENTS);
          unfilteredEventKinds.clear();
        }
        if (!filter.isEmpty()) {
          EventKind kind =
              eventKindConverter.convert(
                  filter.replaceFirst("^[+-]", ""), /* conversionContext= */ null);
          if (filter.startsWith("-")) {
            filteredEventKinds.add(kind);
            unfilteredEventKinds.remove(kind);
          } else {
            unfilteredEventKinds.add(kind);
            filteredEventKinds.remove(kind);
          }
        }
      }
      return EventKindFilters.from(
          ImmutableSet.copyOf(filteredEventKinds), ImmutableSet.copyOf(unfilteredEventKinds));
    }

    @Override
    public String getTypeDescription() {
      return "Convert list of comma separated event kind to list of filters";
    }
  }

  /** Converter for {@link UseColor}. */
  public static class UseColorConverter extends EnumConverter<UseColor> {
    public UseColorConverter() {
      super(UseColor.class, "--color setting");
    }
  }

  /** Converter for {@link UseCurses}. */
  public static class UseCursesConverter extends EnumConverter<UseCurses> {
    public UseCursesConverter() {
      super(UseCurses.class, "--curses setting");
    }
  }

  @Option(
      name = "show_progress",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Display progress messages during a build.")
  public abstract boolean getShowProgress();

  public abstract void setShowProgress(boolean value);

  @Option(
      name = "show_progress_rate_limit",
      defaultValue = "0.2", // A nice middle ground; snappy but not too spammy in logs.
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Minimum number of seconds between progress messages in the output.")
  public abstract double getShowProgressRateLimit();

  public abstract void setShowProgressRateLimit(double value);

  @Option(
      name = "color",
      defaultValue = "auto",
      converter = UseColorConverter.class,
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Use terminal controls to colorize output.")
  public abstract UseColor getUseColorEnum();

  @Option(
      name = "curses",
      defaultValue = "auto",
      converter = UseCursesConverter.class,
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Use terminal cursor controls to minimize scrolling output.")
  public abstract UseCurses getUseCursesEnum();

  public abstract void setUseCursesEnum(UseCurses value);

  @Option(
      name = "terminal_columns",
      defaultValue = "80",
      metadataTags = {OptionMetadataTag.HIDDEN},
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "A system-generated parameter which specifies the terminal width in columns.")
  public abstract int getTerminalColumns();

  @Option(
      name = "isatty",
      defaultValue = "false",
      metadataTags = {OptionMetadataTag.HIDDEN},
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "A system-generated parameter which is used to notify the "
              + "server whether this client is running in a terminal. "
              + "If this is set to false, then '--color=auto' will be treated as '--color=no'. "
              + "If this is set to true, then '--color=auto' will be treated as '--color=yes'.")
  public abstract boolean getIsATty();

  // This lives here (as opposed to the more logical BuildRequest.Options)
  // because the client passes it to the server *always*.  We don't want the
  // client to have to figure out when it should or shouldn't to send it.
  @Option(
      name = "emacs",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "A system-generated parameter which is true iff EMACS=t or INSIDE_EMACS is set "
              + "in the environment of the client.  This option controls certain display "
              + "features.")
  public abstract boolean getRunningInEmacs();

  @Option(
      name = "show_timestamps",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Include timestamps in messages")
  public abstract boolean getShowTimestamp();

  @Option(
      name = "progress_in_terminal_title",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Show the command progress in the terminal title. "
              + "Useful to see what bazel is doing when having multiple terminal tabs.")
  public abstract boolean getProgressInTermTitle();

  @Option(
      name = "attempt_to_print_relative_paths",
      oldName = "experimental_ui_attempt_to_print_relative_paths",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      help =
          "When printing the location part of messages, attempt to use a path relative to the "
              + "workspace directory or one of the directories specified by --package_path.")
  public abstract boolean getAttemptToPrintRelativePaths();

  @Option(
      name = "experimental_ui_debug_all_events",
      defaultValue = "false",
      metadataTags = {OptionMetadataTag.HIDDEN},
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Report all events known to the Bazel UI.")
  public abstract boolean getExperimentalUiDebugAllEvents();

  @Option(
      name = "ui_event_filters",
      converter = EventFiltersConverter.class,
      defaultValue = "null",
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      help =
          "Specifies which events to show in the UI. It is possible to add or remove events "
              + "to the default ones using leading +/-, or override the default "
              + "set completely with direct assignment. The set of supported event kinds "
              + "include INFO, DEBUG, ERROR and more.",
      allowMultiple = true)
  public abstract List<EventFiltersConverter.EventKindFilters> getEventKindFilters();

  public abstract void setEventKindFilters(List<EventFiltersConverter.EventKindFilters> value);

  @Option(
      name = "ui_actions_shown",
      oldName = "experimental_ui_actions_shown",
      defaultValue = "8",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      help =
          "Number of concurrent actions shown in the detailed progress bar; each "
              + "action is shown on a separate line. The progress bar always shows "
              + "at least one one, all numbers less than 1 are mapped to 1.")
  public abstract int getUiActionsShown();

  public abstract void setUiActionsShown(int value);

  @Option(
      name = "experimental_ui_max_stdouterr_bytes",
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      defaultValue = "1048576",
      converter = MaxStdoutErrBytesConverter.class,
      help =
          "The maximum size of the stdout / stderr files that will be printed to the console. "
              + "-1 implies no limit.")
  public abstract int getMaxStdoutErrBytes();

  public boolean useColor() {
    return getUseColorEnum() == UseColor.YES || (getUseColorEnum() == UseColor.AUTO && getIsATty());
  }

  public boolean useCursorControl() {
    return getUseCursesEnum() == UseCurses.YES
        || (getUseCursesEnum() == UseCurses.AUTO && getIsATty());
  }

  public ImmutableSet<EventKind> getFilteredEventKinds() {
    HashSet<EventKind> filtered = new HashSet<>();
    for (EventFiltersConverter.EventKindFilters filters : getEventKindFilters()) {
      filtered.addAll(filters.filteredEventKinds());
      filtered.removeAll(filters.unfilteredEventKinds());
    }
    return ImmutableSet.copyOf(filtered);
  }

  /** A converter for --experimental_ui_max_stdouterr_bytes. */
  public static class MaxStdoutErrBytesConverter extends RangeConverter {

    /**
     * The maximum value of the flag must be limited to ensure conversions to UTF-8 do not trigger
     * integer overflows. In JDK9+, if the message buffer contains a byte whose high bit is set, a
     * UTF-8 decoding path is taken that allocates a new byte[] buffer twice as large as the message
     * byte[] buffer.
     */
    private static final int MAX_VALUE = (Integer.MAX_VALUE - 8) >> 1;

    public MaxStdoutErrBytesConverter() {
      super(-1, (Integer.MAX_VALUE - 8) >> 1);
    }

    @Override
    public Integer convert(String input) throws OptionsParsingException {
      Integer value = super.convert(input);
      return value >= 0 ? value : MAX_VALUE;
    }
  }
}
