// 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.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * The implementation of the options parser. This is intentionally package
 * private for full flexibility. Use {@link OptionsParser} or {@link Options}
 * if you're a consumer.
 */
class OptionsParserImpl {

  /**
   * A bunch of default converters in case the user doesn't specify a
   * different one in the field annotation.
   */
  static final Map<Class<?>, Converter<?>> DEFAULT_CONVERTERS = Maps.newHashMap();

  static {
    DEFAULT_CONVERTERS.put(String.class, new Converter<String>() {
      @Override
      public String convert(String input) {
        return input;
      }
      @Override
      public String getTypeDescription() {
        return "a string";
      }});
    DEFAULT_CONVERTERS.put(int.class, new Converter<Integer>() {
      @Override
      public Integer convert(String input) throws OptionsParsingException {
        try {
          return Integer.decode(input);
        } catch (NumberFormatException e) {
          throw new OptionsParsingException("'" + input + "' is not an int");
        }
      }
      @Override
      public String getTypeDescription() {
        return "an integer";
      }});
    DEFAULT_CONVERTERS.put(double.class, new Converter<Double>() {
      @Override
      public Double convert(String input) throws OptionsParsingException {
        try {
          return Double.parseDouble(input);
        } catch (NumberFormatException e) {
          throw new OptionsParsingException("'" + input + "' is not a double");
        }
      }
      @Override
      public String getTypeDescription() {
        return "a double";
      }});
    DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter());
    DEFAULT_CONVERTERS.put(TriState.class, new Converter<TriState>() {
      @Override
      public TriState convert(String input) throws OptionsParsingException {
        if (input == null) {
          return TriState.AUTO;
        }
        input = input.toLowerCase();
        if (input.equals("auto")) {
          return TriState.AUTO;
        }
        if (input.equals("true") || input.equals("1") || input.equals("yes") ||
            input.equals("t") || input.equals("y")) {
          return TriState.YES;
        }
        if (input.equals("false") || input.equals("0") || input.equals("no") ||
            input.equals("f") || input.equals("n")) {
          return TriState.NO;
        }
        throw new OptionsParsingException("'" + input + "' is not a boolean");
      }
      @Override
      public String getTypeDescription() {
        return "a tri-state (auto, yes, no)";
      }});
    DEFAULT_CONVERTERS.put(Void.class, new Converter<Void>() {
      @Override
      public Void convert(String input) throws OptionsParsingException {
        if (input == null) {
          return null;  // expected input, return is unused so null is fine.
        }
        throw new OptionsParsingException("'" + input + "' unexpected");
      }
      @Override
      public String getTypeDescription() {
        return "";
      }});
    DEFAULT_CONVERTERS.put(long.class, new Converter<Long>() {
      @Override
      public Long convert(String input) throws OptionsParsingException {
        try {
          return Long.decode(input);
        } catch (NumberFormatException e) {
          throw new OptionsParsingException("'" + input + "' is not a long");
        }
      }
      @Override
      public String getTypeDescription() {
        return "a long integer";
      }});
  }

  /**
   * For every value, this class keeps track of its priority, its free-form source
   * description, whether it was set as an implicit dependency, and the value.
   */
  private static final class ParsedOptionEntry {
    private final Object value;
    private final OptionPriority priority;
    private final String source;
    private final String implicitDependant;
    private final String expandedFrom;
    private final boolean allowMultiple;

    ParsedOptionEntry(Object value,
        OptionPriority priority, String source, String implicitDependant, String expandedFrom,
        boolean allowMultiple) {
      this.value = value;
      this.priority = priority;
      this.source = source;
      this.implicitDependant = implicitDependant;
      this.expandedFrom = expandedFrom;
      this.allowMultiple = allowMultiple;
    }

    // Need to suppress unchecked warnings, because the "multiple occurrence"
    // options use unchecked ListMultimaps due to limitations of Java generics.
    @SuppressWarnings({"unchecked", "rawtypes"})
    Object getValue() {
      if (allowMultiple) {
        // Sort the results by option priority and return them in a new list.
        // The generic type of the list is not known at runtime, so we can't
        // use it here. It was already checked in the constructor, so this is
        // type-safe.
        List result = Lists.newArrayList();
        ListMultimap realValue = (ListMultimap) value;
        for (OptionPriority priority : OptionPriority.values()) {
          // If there is no mapping for this key, this check avoids object creation (because
          // ListMultimap has to return a new object on get) and also an unnecessary addAll call.
          if (realValue.containsKey(priority)) {
            result.addAll(realValue.get(priority));
          }
        }
        return result;
      }
      return value;
    }

    // Need to suppress unchecked warnings, because the "multiple occurrence"
    // options use unchecked ListMultimaps due to limitations of Java generics.
    @SuppressWarnings({"unchecked", "rawtypes"})
    void addValue(OptionPriority addedPriority, Object addedValue) {
      Preconditions.checkState(allowMultiple);
      ListMultimap optionValueList = (ListMultimap) value;
      if (addedValue instanceof List<?>) {
        for (Object element : (List<?>) addedValue) {
          optionValueList.put(addedPriority, element);
        }
      } else {
        optionValueList.put(addedPriority, addedValue);
      }
    }

    OptionValueDescription asOptionValueDescription(String fieldName) {
      return new OptionValueDescription(fieldName, getValue(), priority,
          source, implicitDependant, expandedFrom);
    }
  }

  private final OptionsData optionsData;

  /**
   * We store the results of parsing the arguments in here. It'll look like
   * <pre>
   *   Field("--host") -> "www.google.com"
   *   Field("--port") -> 80
   * </pre>
   * This map is modified by repeated calls to
   * {@link #parse(OptionPriority,Function,List)}.
   */
  private final Map<Field, ParsedOptionEntry> parsedValues = Maps.newHashMap();

  /**
   * We store the pre-parsed, explicit options for each priority in here.
   * We use partially preparsed options, which can be different from the original
   * representation, e.g. "--nofoo" becomes "--foo=0".
   */
  private final List<UnparsedOptionValueDescription> unparsedValues = Lists.newArrayList();

  /**
   * Unparsed values for use with the canonicalize command are stored separately from
   * unparsedValues so that invocation policy can modify the values for canonicalization (e.g.
   * override user-specified values with default values) without corrupting the data used to
   * represent the user's original invocation for {@link #asListOfExplicitOptions()} and
   * {@link #asListOfUnparsedOptions()}. A LinkedHashMultimap is used so that canonicalization
   * happens in the correct order and multiple values can be stored for flags that allow multiple
   * values.
   */
  private final Multimap<Field, UnparsedOptionValueDescription> canonicalizeValues
      = LinkedHashMultimap.create();

  private final List<String> warnings = Lists.newArrayList();

  private boolean allowSingleDashLongOptions = false;

  /**
   * Create a new parser object
   */
  OptionsParserImpl(OptionsData optionsData) {
    this.optionsData = optionsData;
  }

  /**
   * Indicates whether or not the parser will allow long options with a
   * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example.
   */
  void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
    this.allowSingleDashLongOptions = allowSingleDashLongOptions;
  }

  /**
   * The implementation of {@link OptionsBase#asMap}.
   */
  static Map<String, Object> optionsAsMap(OptionsBase optionsInstance) {
    Map<String, Object> map = Maps.newHashMap();
    for (Field field : OptionsParser.getAllAnnotatedFields(optionsInstance.getClass())) {
      try {
        String name = field.getAnnotation(Option.class).name();
        Object value = field.get(optionsInstance);
        map.put(name, value);
      } catch (IllegalAccessException e) {
        throw new IllegalStateException(e); // unreachable
      }
    }
    return map;
  }

  List<Field> getAnnotatedFieldsFor(Class<? extends OptionsBase> clazz) {
    return optionsData.getFieldsForClass(clazz);
  }

  /**
   * Implements {@link OptionsParser#asListOfUnparsedOptions()}.
   */
  List<UnparsedOptionValueDescription> asListOfUnparsedOptions() {
    List<UnparsedOptionValueDescription> result = Lists.newArrayList(unparsedValues);
    // It is vital that this sort is stable so that options on the same priority are not reordered.
    Collections.sort(result, new Comparator<UnparsedOptionValueDescription>() {
      @Override
      public int compare(UnparsedOptionValueDescription o1,
          UnparsedOptionValueDescription o2) {
        return o1.getPriority().compareTo(o2.getPriority());
      }
    });
    return result;
  }

  /**
   * Implements {@link OptionsParser#asListOfExplicitOptions()}.
   */
  List<UnparsedOptionValueDescription> asListOfExplicitOptions() {
    List<UnparsedOptionValueDescription> result = Lists.newArrayList(Iterables.filter(
      unparsedValues,
      new Predicate<UnparsedOptionValueDescription>() {
        @Override
        public boolean apply(UnparsedOptionValueDescription input) {
          return input.isExplicit();
        }
    }));
    // It is vital that this sort is stable so that options on the same priority are not reordered.
    Collections.sort(result, new Comparator<UnparsedOptionValueDescription>() {
      @Override
      public int compare(UnparsedOptionValueDescription o1,
          UnparsedOptionValueDescription o2) {
        return o1.getPriority().compareTo(o2.getPriority());
      }
    });
    return result;
  }

  /**
   * Implements {@link OptionsParser#canonicalize}.
   */
  List<String> asCanonicalizedList() {

    List<UnparsedOptionValueDescription> processed = Lists.newArrayList(
        canonicalizeValues.values());
    // Sort implicit requirement options to the end, keeping their existing order, and sort the
    // other options alphabetically.
    Collections.sort(processed, new Comparator<UnparsedOptionValueDescription>() {
      @Override
      public int compare(UnparsedOptionValueDescription o1, UnparsedOptionValueDescription o2) {
        if (o1.isImplicitRequirement()) {
          return o2.isImplicitRequirement() ? 0 : 1;
        }
        if (o2.isImplicitRequirement()) {
          return -1;
        }
        return o1.getName().compareTo(o2.getName());
      }
    });

    List<String> result = Lists.newArrayList();
    for (UnparsedOptionValueDescription value : processed) {

      // Ignore expansion options.
      if (value.isExpansion()) {
        continue;
      }

      result.add("--" + value.getName() + "=" + value.getUnparsedValue());
    }
    return result;
  }

  /**
   * Implements {@link OptionsParser#asListOfEffectiveOptions()}.
   */
  List<OptionValueDescription> asListOfEffectiveOptions() {
    List<OptionValueDescription> result = Lists.newArrayList();
    for (Map.Entry<String,Field> mapEntry : optionsData.getAllNamedFields()) {
      String fieldName = mapEntry.getKey();
      Field field = mapEntry.getValue();
      ParsedOptionEntry entry = parsedValues.get(field);
      if (entry == null) {
        Object value = optionsData.getDefaultValue(field);
        result.add(new OptionValueDescription(fieldName, value, OptionPriority.DEFAULT,
            null, null, null));
      } else {
        result.add(entry.asOptionValueDescription(fieldName));
      }
    }
    return result;
  }

  Collection<Class<?  extends OptionsBase>> getOptionsClasses() {
    return optionsData.getOptionsClasses();
  }

  private void maybeAddDeprecationWarning(Field field) {
    Option option = field.getAnnotation(Option.class);
    // Continue to support the old behavior for @Deprecated options.
    String warning = option.deprecationWarning();
    if (!warning.isEmpty() || (field.getAnnotation(Deprecated.class) != null)) {
      addDeprecationWarning(option.name(), warning);
    }
  }

  private void addDeprecationWarning(String optionName, String warning) {
    warnings.add("Option '" + optionName + "' is deprecated"
        + (warning.isEmpty() ? "" : ": " + warning));
  }

  // Warnings should not end with a '.' because the internal reporter adds one automatically.
  private void setValue(Field field, String name, Object value,
      OptionPriority priority, String source, String implicitDependant, String expandedFrom) {
    ParsedOptionEntry entry = parsedValues.get(field);
    if (entry != null) {
      // Override existing option if the new value has higher or equal priority.
      if (priority.compareTo(entry.priority) >= 0) {
        // Output warnings:
        if ((implicitDependant != null) && (entry.implicitDependant != null)) {
          if (!implicitDependant.equals(entry.implicitDependant)) {
            warnings.add("Option '" + name + "' is implicitly defined by both option '" +
                entry.implicitDependant + "' and option '" + implicitDependant + "'");
          }
        } else if ((implicitDependant != null) && priority.equals(entry.priority)) {
          warnings.add("Option '" + name + "' is implicitly defined by option '" +
              implicitDependant + "'; the implicitly set value overrides the previous one");
        } else if (entry.implicitDependant != null) {
          warnings.add("A new value for option '" + name + "' overrides a previous " +
              "implicit setting of that option by option '" + entry.implicitDependant + "'");
        } else if ((priority == entry.priority) &&
            ((entry.expandedFrom == null) && (expandedFrom != null))) {
          // Create a warning if an expansion option overrides an explicit option:
          warnings.add("The option '" + expandedFrom + "' was expanded and now overrides a "
              + "previous explicitly specified option '" + name + "'");
        }

        // Record the new value:
        parsedValues.put(field,
            new ParsedOptionEntry(value, priority, source, implicitDependant, expandedFrom, false));
      }
    } else {
      parsedValues.put(field,
          new ParsedOptionEntry(value, priority, source, implicitDependant, expandedFrom, false));
      maybeAddDeprecationWarning(field);
    }
  }

  private void addListValue(Field field, Object value, OptionPriority priority, String source,
      String implicitDependant, String expandedFrom) {
    ParsedOptionEntry entry = parsedValues.get(field);
    if (entry == null) {
      entry = new ParsedOptionEntry(ArrayListMultimap.create(), priority, source,
          implicitDependant, expandedFrom, true);
      parsedValues.put(field, entry);
      maybeAddDeprecationWarning(field);
    }
    entry.addValue(priority, value);
  }

  void clearValue(String optionName, Map<String, OptionValueDescription> clearedValues)
      throws OptionsParsingException {
    Field field = optionsData.getFieldFromName(optionName);
    if (field == null) {
      throw new IllegalArgumentException("No such option '" + optionName + "'");
    }
    Option option = field.getAnnotation(Option.class);
    clearValue(field, option, clearedValues);
  }

  private void clearValue(
      Field field, Option option, Map<String, OptionValueDescription> clearedValues)
      throws OptionsParsingException {

    ParsedOptionEntry removed = parsedValues.remove(field);
    if (removed != null) {
      clearedValues.put(option.name(), removed.asOptionValueDescription(option.name()));
    }

    canonicalizeValues.removeAll(field);

    // Recurse to remove any implicit or expansion flags that this flag may have added when
    // originally parsed.
    for (String[] args : new String[][] {option.implicitRequirements(), option.expansion()}) {
      Iterator<String> argsIterator = Iterators.forArray(args);
      while (argsIterator.hasNext()) {
        String arg = argsIterator.next();
        ParseOptionResult parseOptionResult = parseOption(arg, argsIterator);
        clearValue(parseOptionResult.field, parseOptionResult.option, clearedValues);
      }
    }
  }

  OptionValueDescription getOptionValueDescription(String name) {
    Field field = optionsData.getFieldFromName(name);
    if (field == null) {
      throw new IllegalArgumentException("No such option '" + name + "'");
    }
    ParsedOptionEntry entry = parsedValues.get(field);
    if (entry == null) {
      return null;
    }
    return entry.asOptionValueDescription(name);
  }

  OptionDescription getOptionDescription(String name) {
    Field field = optionsData.getFieldFromName(name);
    if (field == null) {
      return null;
    }

    Option optionAnnotation = field.getAnnotation(Option.class);
    return new OptionDescription(
        name,
        optionsData.getDefaultValue(field),
        optionsData.getConverter(field),
        optionAnnotation.allowMultiple());
  }

  boolean containsExplicitOption(String name) {
    Field field = optionsData.getFieldFromName(name);
    if (field == null) {
      throw new IllegalArgumentException("No such option '" + name + "'");
    }
    return parsedValues.get(field) != null;
  }

  /**
   * Parses the args, and returns what it doesn't parse. May be called multiple
   * times, and may be called recursively. In each call, there may be no
   * duplicates, but separate calls may contain intersecting sets of options; in
   * that case, the arg seen last takes precedence.
   */
  List<String> parse(OptionPriority priority, Function<? super String, String> sourceFunction,
      List<String> args) throws OptionsParsingException {
    return parse(priority, sourceFunction, null, null, args);
  }

  /**
   * Parses the args, and returns what it doesn't parse. May be called multiple
   * times, and may be called recursively. Calls may contain intersecting sets
   * of options; in that case, the arg seen last takes precedence.
   *
   * <p>The method uses the invariant that if an option has neither an implicit
   * dependent nor an expanded from value, then it must have been explicitly
   * set.
   */
  private List<String> parse(
      OptionPriority priority,
      Function<? super String, String> sourceFunction,
      String implicitDependent,
      String expandedFrom,
      List<String> args) throws OptionsParsingException {

    List<String> unparsedArgs = Lists.newArrayList();
    LinkedHashMap<String,List<String>> implicitRequirements = Maps.newLinkedHashMap();

    Iterator<String> argsIterator = args.iterator();
    while (argsIterator.hasNext()) {
      String arg = argsIterator.next();

      if (!arg.startsWith("-")) {
        unparsedArgs.add(arg);
        continue;  // not an option arg
      }

      if (arg.equals("--")) {  // "--" means all remaining args aren't options
        Iterators.addAll(unparsedArgs, argsIterator);
        break;
      }

      ParseOptionResult optionParseResult = parseOption(arg, argsIterator);
      Field field = optionParseResult.field;
      Option option = optionParseResult.option;
      String value = optionParseResult.value;

      final String originalName = option.name();

      if (option.wrapperOption()) {
        if (value.startsWith("-")) {

          List<String> unparsed = parse(
              priority,
              Functions.constant("Unwrapped from wrapper option --" + originalName),
              null, // implicitDependent
              null, // expandedFrom
              ImmutableList.of(value));

          if (!unparsed.isEmpty()) {
            throw new OptionsParsingException("Unparsed options remain after unwrapping " +
              arg + ": " + Joiner.on(' ').join(unparsed));
          }

          // Don't process implicitRequirements or expansions for wrapper options. In particular,
          // don't record this option in unparsedValues, so that only the wrapped option shows
          // up in canonicalized options.
          continue;

        } else {
          throw new OptionsParsingException("Invalid --" + originalName + " value format. "
              + "You may have meant --" + originalName + "=--" + value);
        }
      }

      if (implicitDependent == null) {
        // Log explicit options and expanded options in the order they are parsed (can be sorted
        // later). Also remember whether they were expanded or not. This information is needed to
        // correctly canonicalize flags.
        UnparsedOptionValueDescription unparsedOptionValueDescription =
            new UnparsedOptionValueDescription(
                originalName,
                field,
                value,
                priority,
                sourceFunction.apply(originalName),
                expandedFrom == null);
        unparsedValues.add(unparsedOptionValueDescription);
        if (option.allowMultiple()) {
          canonicalizeValues.put(field, unparsedOptionValueDescription);
        } else {
          canonicalizeValues.replaceValues(field, ImmutableList.of(unparsedOptionValueDescription));
        }
      }

      // Handle expansion options.
      if (option.expansion().length > 0) {
        Function<Object, String> expansionSourceFunction = Functions.<String>constant(
            "expanded from option --" + originalName + " from " +
            sourceFunction.apply(originalName));
        maybeAddDeprecationWarning(field);
        List<String> unparsed = parse(priority, expansionSourceFunction, null, originalName,
            ImmutableList.copyOf(option.expansion()));
        if (!unparsed.isEmpty()) {
          // Throw an assertion, because this indicates an error in the code that specified the
          // expansion for the current option.
          throw new AssertionError("Unparsed options remain after parsing expansion of " +
            arg + ": " + Joiner.on(' ').join(unparsed));
        }
      } else {
        Converter<?> converter = optionsData.getConverter(field);
        Object convertedValue;
        try {
          convertedValue = converter.convert(value);
        } catch (OptionsParsingException e) {
          // The converter doesn't know the option name, so we supply it here by
          // re-throwing:
          throw new OptionsParsingException("While parsing option " + arg
                                            + ": " + e.getMessage(), e);
        }

        // ...but allow duplicates of single-use options across separate calls to
        // parse(); latest wins:
        if (!option.allowMultiple()) {
          setValue(field, originalName, convertedValue,
              priority, sourceFunction.apply(originalName), implicitDependent, expandedFrom);
        } else {
          // But if it's a multiple-use option, then just accumulate the
          // values, in the order in which they were seen.
          // Note: The type of the list member is not known; Java introspection
          // only makes it available in String form via the signature string
          // for the field declaration.
          addListValue(field, convertedValue, priority, sourceFunction.apply(originalName),
              implicitDependent, expandedFrom);
        }
      }

      // Collect any implicit requirements.
      if (option.implicitRequirements().length > 0) {
        implicitRequirements.put(option.name(), Arrays.asList(option.implicitRequirements()));
      }
    }

    // Now parse any implicit requirements that were collected.
    // TODO(bazel-team): this should happen when the option is encountered.
    if (!implicitRequirements.isEmpty()) {
      for (Map.Entry<String,List<String>> entry : implicitRequirements.entrySet()) {
        Function<Object, String> requirementSourceFunction = Functions.<String>constant(
            "implicit requirement of option --" + entry.getKey() + " from " +
            sourceFunction.apply(entry.getKey()));

        List<String> unparsed = parse(priority, requirementSourceFunction, entry.getKey(), null,
            entry.getValue());
        if (!unparsed.isEmpty()) {
          // Throw an assertion, because this indicates an error in the code that specified in the
          // implicit requirements for the option(s).
          throw new AssertionError("Unparsed options remain after parsing implicit options: "
              + Joiner.on(' ').join(unparsed));
        }
      }
    }

    return unparsedArgs;
  }

  private static final class ParseOptionResult {
    final Field field;
    final Option option;
    final String value;

    ParseOptionResult(Field field, Option option, String value) {
      this.field = field;
      this.option = option;
      this.value = value;
    }
  }

  private ParseOptionResult parseOption(String arg, Iterator<String> nextArgs)
      throws OptionsParsingException {

    String value = null;
    Field field;
    boolean booleanValue = true;

    if (arg.length() == 2) { // -l  (may be nullary or unary)
      field = optionsData.getFieldForAbbrev(arg.charAt(1));
      booleanValue = true;

    } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l-  (boolean)
      field = optionsData.getFieldForAbbrev(arg.charAt(1));
      booleanValue = false;

    } else if (allowSingleDashLongOptions // -long_option
        || arg.startsWith("--")) { // or --long_option

      int equalsAt = arg.indexOf('=');
      int nameStartsAt = arg.startsWith("--") ? 2 : 1;
      String name =
          equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt);
      if (name.trim().isEmpty()) {
        throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
      }
      value = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
      field = optionsData.getFieldFromName(name);

      // look for a "no"-prefixed option name: "no<optionname>";
      // (Undocumented: we also allow --no_foo.  We're generous like that.)
      if (field == null && name.startsWith("no")) {
        name = name.substring(name.startsWith("no_") ? 3 : 2);
        field = optionsData.getFieldFromName(name);
        booleanValue = false;
        if (field != null) {
          // TODO(bazel-team): Add tests for these cases.
          if (!OptionsParserImpl.isBooleanField(field)) {
            throw new OptionsParsingException(
                "Illegal use of 'no' prefix on non-boolean option: " + arg, arg);
          }
          if (value != null) {
            throw new OptionsParsingException(
                "Unexpected value after boolean option: " + arg, arg);
          }
          // "no<optionname>" signifies a boolean option w/ false value
          value = "0";
        }
      }
    } else {
      throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
    }

    if (field == null) {
      throw new OptionsParsingException("Unrecognized option: " + arg, arg);
    }

    Option option = field.getAnnotation(Option.class);

    if (value == null) {
      // Special-case boolean to supply value based on presence of "no" prefix.
      if (OptionsParserImpl.isBooleanField(field)) {
        value = booleanValue ? "1" : "0";
      } else if (field.getType().equals(Void.class) && !option.wrapperOption()) {
        // This is expected, Void type options have no args (unless they're wrapper options).
      } else if (nextArgs.hasNext()) {
        value = nextArgs.next();  // "--flag value" form
      } else {
        throw new OptionsParsingException("Expected value after " + arg);
      }
    }

    return new ParseOptionResult(field, option, value);
  }

  /**
   * Gets the result of parsing the options.
   */
  <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) {
    // Create the instance:
    O optionsInstance;
    try {
      Constructor<O> constructor = optionsData.getConstructor(optionsClass);
      if (constructor == null) {
        return null;
      }
      optionsInstance = constructor.newInstance(new Object[0]);
    } catch (Exception e) {
      throw new IllegalStateException(e);
    }

    // Set the fields
    for (Field field : optionsData.getFieldsForClass(optionsClass)) {
      Object value;
      ParsedOptionEntry entry = parsedValues.get(field);
      if (entry == null) {
        value = optionsData.getDefaultValue(field);
      } else {
        value = entry.getValue();
      }
      try {
        field.set(optionsInstance, value);
      } catch (IllegalAccessException e) {
        throw new IllegalStateException(e);
      }
    }
    return optionsInstance;
  }

  List<String> getWarnings() {
    return ImmutableList.copyOf(warnings);
  }

  static String getDefaultOptionString(Field optionField) {
    Option annotation = optionField.getAnnotation(Option.class);
    return annotation.defaultValue();
  }

  static boolean isBooleanField(Field field) {
    return field.getType().equals(boolean.class)
        || field.getType().equals(TriState.class)
        || findConverter(field) instanceof BoolOrEnumConverter;
  }

  static boolean isVoidField(Field field) {
    return field.getType().equals(Void.class);
  }

  static boolean isSpecialNullDefault(String defaultValueString, Field optionField) {
    return defaultValueString.equals("null") && !optionField.getType().isPrimitive();
  }

  static Converter<?> findConverter(Field optionField) {
    Option annotation = optionField.getAnnotation(Option.class);
    if (annotation.converter() == Converter.class) {
      Type type;
      if (annotation.allowMultiple()) {
        // The OptionParserImpl already checked that the type is List<T> for some T;
        // here we extract the type T.
        type = ((ParameterizedType) optionField.getGenericType()).getActualTypeArguments()[0];
      } else {
        type = optionField.getType();
      }
      Converter<?> converter = DEFAULT_CONVERTERS.get(type);
      if (converter == null) {
        throw new AssertionError("No converter found for "
            + type + "; possible fix: add "
            + "converter=... to @Option annotation for "
            + optionField.getName());
      }
      return converter;
    }
    try {
      Class<?> converter = annotation.converter();
      Constructor<?> constructor = converter.getConstructor(new Class<?>[0]);
      return (Converter<?>) constructor.newInstance(new Object[0]);
    } catch (Exception e) {
      throw new AssertionError(e);
    }
  }
}