// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Creates a new [Option].
///
/// Since [Option] doesn't have a public constructor, this lets `ArgParser`
/// get to it. This function isn't exported to the public API of the package.
Option newOption(
    String name,
    String? abbr,
    String? help,
    String? valueHelp,
    Iterable<String>? allowed,
    Map<String, String>? allowedHelp,
    Object? defaultsTo,
    Function? callback,
    OptionType type,
    {bool? negatable,
    bool? splitCommas,
    bool mandatory = false,
    bool hide = false,
    bool hideNegatedUsage = false,
    List<String> aliases = const []}) {
  return Option._(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
      callback, type,
      negatable: negatable,
      splitCommas: splitCommas,
      mandatory: mandatory,
      hide: hide,
      hideNegatedUsage: hideNegatedUsage,
      aliases: aliases);
}

/// A command-line option.
///
/// This represents both boolean flags and options which take a value.
class Option {
  /// The name of the option that the user passes as an argument.
  final String name;

  /// A single-character string that can be used as a shorthand for this option.
  ///
  /// For example, `abbr: "a"` will allow the user to pass `-a value` or
  /// `-avalue`.
  final String? abbr;

  /// A description of this option.
  final String? help;

  /// A name for the value this option takes.
  final String? valueHelp;

  /// A list of valid values for this option.
  final List<String>? allowed;

  /// A map from values in [allowed] to documentation for those values.
  final Map<String, String>? allowedHelp;

  /// The value this option will have if the user doesn't explicitly pass it.
  final dynamic defaultsTo;

  /// Whether this flag's value can be set to `false`.
  ///
  /// For example, if [name] is `flag`, the user can pass `--no-flag` to set its
  /// value to `false`.
  ///
  /// This is `null` unless [type] is [OptionType.flag].
  final bool? negatable;

  /// Whether to document that this flag is [negatable].
  ///
  /// This is `null` unless [type] is [OptionType.flag].
  final bool? hideNegatedUsage;

  /// The callback to invoke with the option's value when the option is parsed.
  final Function? callback;

  /// Whether this is a flag, a single value option, or a multi-value option.
  final OptionType type;

  /// Whether multiple values may be passed by writing `--option a,b` in
  /// addition to `--option a --option b`.
  final bool splitCommas;

  /// Whether this option must be provided for correct usage.
  final bool mandatory;

  /// Whether this option should be hidden from usage documentation.
  final bool hide;

  /// All aliases for [name].
  final List<String> aliases;

  /// Whether the option is boolean-valued flag.
  bool get isFlag => type == OptionType.flag;

  /// Whether the option takes a single value.
  bool get isSingle => type == OptionType.single;

  /// Whether the option allows multiple values.
  bool get isMultiple => type == OptionType.multiple;

  Option._(
      this.name,
      this.abbr,
      this.help,
      this.valueHelp,
      Iterable<String>? allowed,
      Map<String, String>? allowedHelp,
      this.defaultsTo,
      this.callback,
      this.type,
      {this.negatable,
      bool? splitCommas,
      this.mandatory = false,
      this.hide = false,
      this.hideNegatedUsage,
      this.aliases = const []})
      : allowed = allowed == null ? null : List.unmodifiable(allowed),
        allowedHelp =
            allowedHelp == null ? null : Map.unmodifiable(allowedHelp),
        // If the user doesn't specify [splitCommas], it defaults to true for
        // multiple options.
        splitCommas = splitCommas ?? type == OptionType.multiple {
    if (name.isEmpty) {
      throw ArgumentError('Name cannot be empty.');
    } else if (name.startsWith('-')) {
      throw ArgumentError('Name $name cannot start with "-".');
    }

    // Ensure name does not contain any invalid characters.
    if (_invalidChars.hasMatch(name)) {
      throw ArgumentError('Name "$name" contains invalid characters.');
    }

    var abbr = this.abbr;
    if (abbr != null) {
      if (abbr.length != 1) {
        throw ArgumentError('Abbreviation must be null or have length 1.');
      } else if (abbr == '-') {
        throw ArgumentError('Abbreviation cannot be "-".');
      }

      if (_invalidChars.hasMatch(abbr)) {
        throw ArgumentError('Abbreviation is an invalid character.');
      }
    }
  }

  /// Returns [value] if non-`null`, otherwise returns the default value for
  /// this option.
  ///
  /// For single-valued options, it will be [defaultsTo] if set or `null`
  /// otherwise. For multiple-valued options, it will be an empty list or a
  /// list containing [defaultsTo] if set.
  dynamic valueOrDefault(Object? value) {
    if (value != null) return value;
    if (isMultiple) return defaultsTo ?? <String>[];
    return defaultsTo;
  }

  @Deprecated('Use valueOrDefault instead.')
  dynamic getOrDefault(Object? value) => valueOrDefault(value);

  static final _invalidChars = RegExp(r'''[ \t\r\n"'\\/]''');
}

/// What kinds of values an option accepts.
class OptionType {
  /// An option that can only be `true` or `false`.
  ///
  /// The presence of the option name itself in the argument list means `true`.
  static const flag = OptionType._('OptionType.flag');

  /// An option that takes a single value.
  ///
  /// Examples:
  ///
  ///     --mode debug
  ///     -mdebug
  ///     --mode=debug
  ///
  /// If the option is passed more than once, the last one wins.
  static const single = OptionType._('OptionType.single');

  /// An option that allows multiple values.
  ///
  /// Example:
  ///
  ///     --output text --output xml
  ///
  /// In the parsed `ArgResults`, a multiple-valued option will always return
  /// a list, even if one or no values were passed.
  static const multiple = OptionType._('OptionType.multiple');

  final String name;

  const OptionType._(this.name);
}
