// Copyright 2024 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.common.options;

import static java.util.Comparator.comparing;

import java.lang.reflect.Type;
import java.util.Comparator;
import javax.annotation.Nullable;

/** Everything the {@link OptionsParser} needs to know about how an option is defined. */
public abstract class OptionDefinition implements Comparable<OptionDefinition> {

  /** An ordering relation for options that orders by the option name. */
  public static final Comparator<OptionDefinition> BY_OPTION_NAME =
      Comparator.comparing(OptionDefinition::getOptionName);

  /**
   * An ordering relation for options that first groups together options of the same category, then
   * sorts by name within the category.
   */
  public static final Comparator<OptionDefinition> BY_CATEGORY =
      comparing(OptionDefinition::getOptionCategory).thenComparing(BY_OPTION_NAME);

  /** Returns the declaring {@link OptionsBase} class that owns this option. */
  public abstract <C extends OptionsBase> Class<? extends C> getDeclaringClass(Class<C> baseClass);

  /**
   * Returns the raw value of the option. Use {@link #getValue} if possible to correctly handle
   * default values.
   */
  public abstract Object getRawValue(OptionsBase optionsBase);

  /** Returns the value of this option, taking default values into account. */
  public abstract Object getValue(OptionsBase optionsBase);

  /**
   * Returns the value of this option as a boolean. If the option is not boolean-typed, throws an
   * IllegalStateException.
   */
  public boolean getBooleanValue(OptionsBase optionsBase) {
    // Check for primitive boolean first, as it's more common.
    if (!getType().isAssignableFrom(Boolean.TYPE) && !getType().isAssignableFrom(Boolean.class)) {
      throw new IllegalStateException(
          "Option "
              + getOptionName()
              + " is not a boolean, has type "
              + getType().getCanonicalName());
    }
    return getValue(optionsBase).equals(Boolean.TRUE);
  }

  /** Sets the value for this option. */
  public abstract void setValue(OptionsBase optionsBase, Object value);

  /** Returns whether this option is deprecated. */
  public abstract boolean isDeprecated();

  /** Returns the name of this option. */
  public abstract String getOptionName();

  /** Returns a one-character abbreviation for this option, if any. */
  public abstract char getAbbreviation();

  /** Returns the help test for this option. */
  public abstract String getHelpText();

  /** Returns a short description of the expected type of this option. */
  public abstract String getValueTypeHelpText();

  /**
   * Returns the default value of this option, with no conversion performed. Should only be used by
   * the parser.
   */
  public abstract String getUnparsedDefaultValue();

  /**
   * Returns the deprecated option category.
   *
   * @deprecated Use {@link #getDocumentationCategory} instead
   */
  @Deprecated
  public abstract String getOptionCategory();

  /** Returns the option category. */
  public abstract OptionDocumentationCategory getDocumentationCategory();

  /** Returns data about the intended effects of this option. */
  public abstract OptionEffectTag[] getOptionEffectTags();

  /** Returns metadata about this option. */
  public abstract OptionMetadataTag[] getOptionMetadataTags();

  /** Returns a converter to use for this option. */
  @SuppressWarnings({"rawtypes"})
  public abstract Class<? extends Converter> getProvidedConverter();

  /** Returns whether this option allows multiple instances to be combined into a list. */
  public abstract boolean allowsMultiple();

  /** Returns any options which are added if this option is present. */
  public abstract String[] getOptionExpansion();

  /** Returns aditional options that need to be implicitly added for this option. */
  public abstract String[] getImplicitRequirements();

  /** Returns a deprecation warning for this option, if one is present. */
  public abstract String getDeprecationWarning();

  /** Returns the old name for this option, if one is present. */
  public abstract String getOldOptionName();

  /** Returns a warning to use with this option if the old name is specified. */
  public abstract boolean getOldNameWarning();

  /** The type of the optionDefinition. */
  public abstract Class<?> getType();

  /** Whether this field has type Void. */
  public boolean isVoidField() {
    return getType().equals(Void.class);
  }

  // TODO: blaze-configurability - try to remove special handling for defaults
  public abstract boolean isSpecialNullDefault();

  /** Returns whether the arg is an expansion option. */
  public boolean isExpansionOption() {
    return getOptionExpansion().length > 0;
  }

  /** Returns whether the arg is an expansion option. */
  public boolean hasImplicitRequirements() {
    return (getImplicitRequirements().length > 0);
  }

  /**
   * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option
   * that does use it, asserts that the type is a {@code List<T>} and returns its element type
   * {@code T}.
   */
  public abstract Type getFieldSingularType();

  /** Returns the {@link Converter} that will be used for this option. */
  public abstract Converter<?> getConverter();

  /**
   * Returns whether a field should be considered as boolean.
   *
   * <p>Can be used for usage help and controlling whether the "no" prefix is allowed.
   */
  public boolean usesBooleanValueSyntax() {
    return getType().equals(boolean.class)
        || getType().equals(TriState.class)
        || getConverter() instanceof BoolOrEnumConverter;
  }

  /**
   * Returns whether an option requires a value when instantiated, or instead can be present without
   * an explicit value.
   */
  public boolean requiresValue() {
    return !isVoidField() && !usesBooleanValueSyntax();
  }

  /** Returns the evaluated default value for this option. */
  @Nullable
  public abstract Object getDefaultValue(@Nullable Object conversionContext);
}
