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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.Immutable;

/**
 * A selection of options data corresponding to a set of {@link OptionsBase} subclasses (options
 * classes). The data is collected using reflection, which can be expensive. Therefore this class
 * can be used internally to cache the results.
 *
 * <p>The data is isolated in the sense that it has not yet been processed to add
 * inter-option-dependent information -- namely, the results of evaluating expansion functions. The
 * {@link OptionsData} subclass stores this added information. The reason for the split is so that
 * we can avoid exposing to expansion functions the effects of evaluating other expansion functions,
 * to ensure that the order in which they run is not significant.
 *
 * <p>This class is immutable so long as the converters and default values associated with the
 * options are immutable.
 */
@Immutable
public class IsolatedOptionsData extends OpaqueOptionsData {

  /**
   * Cache for the options in an OptionsBase.
   *
   * <p>Mapping from options class to a list of all {@code OptionFields} in that class. The map
   * entries are unordered, but the fields in the lists are ordered alphabetically. This caches the
   * work of reflection done for the same {@code optionsBase} across multiple {@link OptionsData}
   * instances, and must be used through the thread safe {@link
   * #getAllOptionDefinitionsForClass(Class)}
   */
  private static final Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>>
      allOptionsFields = new HashMap<>();

  /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */
  public static synchronized ImmutableList<OptionDefinition> getAllOptionDefinitionsForClass(
      Class<? extends OptionsBase> optionsClass) {
    return allOptionsFields.computeIfAbsent(
        optionsClass,
        optionsBaseClass ->
            Arrays.stream(optionsBaseClass.getFields())
                .map(
                    field -> {
                      try {
                        return OptionDefinition.extractOptionDefinition(field);
                      } catch (NotAnOptionException e) {
                        // Ignore non-@Option annotated fields. Requiring all fields in the
                        // OptionsBase to be @Option-annotated requires a depot cleanup.
                        return null;
                      }
                    })
                .filter(Objects::nonNull)
                .sorted(OptionDefinition.BY_OPTION_NAME)
                .collect(ImmutableList.toImmutableList()));
  }

  /**
   * Mapping from each options class to its no-arg constructor. Entries appear in the same order
   * that they were passed to {@link #from(Collection)}.
   */
  private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;

  /**
   * Mapping from option name to {@code OptionDefinition}. Entries appear ordered first by their
   * options class (the order in which they were passed to {@link #from(Collection)}, and then in
   * alphabetic order within each options class.
   */
  private final ImmutableMap<String, OptionDefinition> nameToField;

  /**
   * For options that have an "OldName", this is a mapping from old name to its corresponding {@code
   * OptionDefinition}. Entries appear ordered first by their options class (the order in which they
   * were passed to {@link #from(Collection)}, and then in alphabetic order within each options
   * class.
   */
  private final ImmutableMap<String, OptionDefinition> oldNameToField;

  /** Mapping from option abbreviation to {@code OptionDefinition} (unordered). */
  private final ImmutableMap<Character, OptionDefinition> abbrevToField;


  /**
   * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes}
   * annotation (unordered).
   */
  private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes;

  private IsolatedOptionsData(
      Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
      Map<String, OptionDefinition> nameToField,
      Map<String, OptionDefinition> oldNameToField,
      Map<Character, OptionDefinition> abbrevToField,
      Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) {
    this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
    this.nameToField = ImmutableMap.copyOf(nameToField);
    this.oldNameToField = ImmutableMap.copyOf(oldNameToField);
    this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
    this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes);
  }

  protected IsolatedOptionsData(IsolatedOptionsData other) {
    this(
        other.optionsClasses,
        other.nameToField,
        other.oldNameToField,
        other.abbrevToField,
        other.usesOnlyCoreTypes);
  }

  /**
   * Returns all options classes indexed by this options data object, in the order they were passed
   * to {@link #from(Collection)}.
   */
  public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
    return optionsClasses.keySet();
  }

  @SuppressWarnings("unchecked") // The construction ensures that the case is always valid.
  public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
    return (Constructor<T>) optionsClasses.get(clazz);
  }

  /**
   * Returns the option in this parser by the provided name, or {@code null} if none is found. This
   * will match both the canonical name of an option, and any old name listed that we still accept.
   */
  public OptionDefinition getOptionDefinitionFromName(String name) {
    return nameToField.getOrDefault(name, oldNameToField.get(name));
  }

  /**
   * Returns all {@link OptionDefinition} objects loaded, mapped by their canonical names. Entries
   * appear ordered first by their options class (the order in which they were passed to {@link
   * #from(Collection)}, and then in alphabetic order within each options class.
   */
  public Iterable<Map.Entry<String, OptionDefinition>> getAllOptionDefinitions() {
    return nameToField.entrySet();
  }

  public OptionDefinition getFieldForAbbrev(char abbrev) {
    return abbrevToField.get(abbrev);
  }

  public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
    return usesOnlyCoreTypes.get(optionsClass);
  }

  /**
   * Generic method to check for collisions between the names we give options. Useful for checking
   * both single-character abbreviations and full names.
   */
  private static <A> void checkForCollisions(
      Map<A, OptionDefinition> aFieldMap, A optionName, String description)
      throws DuplicateOptionDeclarationException {
    if (aFieldMap.containsKey(optionName)) {
      throw new DuplicateOptionDeclarationException(
          "Duplicate option name, due to " + description + ": --" + optionName);
    }
  }

  /**
   * All options, even non-boolean ones, should check that they do not conflict with previously
   * loaded boolean options.
   */
  private static void checkForBooleanAliasCollisions(
      Map<String, String> booleanAliasMap, String optionName, String description)
      throws DuplicateOptionDeclarationException {
    if (booleanAliasMap.containsKey(optionName)) {
      throw new DuplicateOptionDeclarationException(
          "Duplicate option name, due to "
              + description
              + " --"
              + optionName
              + ", it conflicts with a negating alias for boolean flag --"
              + booleanAliasMap.get(optionName));
    }
  }

  /**
   * For an {@code option} of boolean type, this checks that the boolean alias does not conflict
   * with other names, and adds the boolean alias to a list so that future flags can find if they
   * conflict with a boolean alias..
   */
  private static void checkAndUpdateBooleanAliases(
      Map<String, OptionDefinition> nameToFieldMap,
      Map<String, OptionDefinition> oldNameToFieldMap,
      Map<String, String> booleanAliasMap,
      String optionName)
      throws DuplicateOptionDeclarationException {
    // Check that the negating alias does not conflict with existing flags.
    checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias");
    checkForCollisions(oldNameToFieldMap, "no" + optionName, "boolean option alias");

    // Record that the boolean option takes up additional namespace for its negating alias.
    booleanAliasMap.put("no" + optionName, optionName);
  }

  /**
   * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given
   * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking
   * on each option in isolation.
   */
  static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
    // Mind which fields have to preserve order.
    Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>();
    Map<String, OptionDefinition> nameToFieldBuilder = new LinkedHashMap<>();
    Map<String, OptionDefinition> oldNameToFieldBuilder = new LinkedHashMap<>();
    Map<Character, OptionDefinition> abbrevToFieldBuilder = new HashMap<>();

    // Maps the negated boolean flag aliases to the original option name.
    Map<String, String> booleanAliasMap = new HashMap<>();

    Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>();

    // Combine the option definitions for these options classes, and check that they do not
    // conflict. The options are individually checked for correctness at compile time in the
    // OptionProcessor.
    for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
      try {
        Constructor<? extends OptionsBase> constructor = parsedOptionsClass.getConstructor();
        constructorBuilder.put(parsedOptionsClass, constructor);
      } catch (NoSuchMethodException e) {
        throw new IllegalArgumentException(parsedOptionsClass
            + " lacks an accessible default constructor");
      }
      ImmutableList<OptionDefinition> optionDefinitions =
          getAllOptionDefinitionsForClass(parsedOptionsClass);

      for (OptionDefinition optionDefinition : optionDefinitions) {
        try {
          String optionName = optionDefinition.getOptionName();
          checkForCollisions(nameToFieldBuilder, optionName, "option name collision");
          checkForCollisions(
              oldNameToFieldBuilder,
              optionName,
              "option name collision with another option's old name");
          checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option");
          if (optionDefinition.usesBooleanValueSyntax()) {
            checkAndUpdateBooleanAliases(
                nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, optionName);
          }
          nameToFieldBuilder.put(optionName, optionDefinition);

          if (!optionDefinition.getOldOptionName().isEmpty()) {
            String oldName = optionDefinition.getOldOptionName();
            checkForCollisions(
                nameToFieldBuilder,
                oldName,
                "old option name collision with another option's canonical name");
            checkForCollisions(
                oldNameToFieldBuilder,
                oldName,
                "old option name collision with another old option name");
            checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name");
            // If boolean, repeat the alias dance for the old name.
            if (optionDefinition.usesBooleanValueSyntax()) {
              checkAndUpdateBooleanAliases(
                  nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, oldName);
            }
            // Now that we've checked for conflicts, confidently store the old name.
            oldNameToFieldBuilder.put(oldName, optionDefinition);
          }
          if (optionDefinition.getAbbreviation() != '\0') {
            checkForCollisions(
                abbrevToFieldBuilder, optionDefinition.getAbbreviation(), "option abbreviation");
            abbrevToFieldBuilder.put(optionDefinition.getAbbreviation(), optionDefinition);
          }
        } catch (DuplicateOptionDeclarationException e) {
          throw new ConstructionException(e);
        }
      }

      boolean usesOnlyCoreTypes = parsedOptionsClass.isAnnotationPresent(UsesOnlyCoreTypes.class);
      if (usesOnlyCoreTypes) {
        // Validate that @UsesOnlyCoreTypes was used correctly.
        for (OptionDefinition optionDefinition : optionDefinitions) {
          // The classes in coreTypes are all final. But even if they weren't, we only want to check
          // for exact matches; subclasses would not be considered core types.
          if (!UsesOnlyCoreTypes.CORE_TYPES.contains(optionDefinition.getType())) {
            throw new ConstructionException(
                "Options class '"
                    + parsedOptionsClass.getName()
                    + "' is marked as "
                    + "@UsesOnlyCoreTypes, but field '"
                    + optionDefinition.getField().getName()
                    + "' has type '"
                    + optionDefinition.getType().getName()
                    + "'");
          }
        }
      }
      usesOnlyCoreTypesBuilder.put(parsedOptionsClass, usesOnlyCoreTypes);
    }

    return new IsolatedOptionsData(
        constructorBuilder,
        nameToFieldBuilder,
        oldNameToFieldBuilder,
        abbrevToFieldBuilder,
        usesOnlyCoreTypesBuilder);
  }

}