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

import 'dart:collection';

/// 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 abbreviation, String help,
    String valueHelp, List<String> allowed, Map<String, String> allowedHelp,
    defaultValue, Function callback, OptionType type,
    {bool negatable, bool splitCommas, bool hide: false}) {
  return new Option._(name, abbreviation, help, valueHelp, allowed, allowedHelp,
      defaultValue, callback, type, negatable: negatable,
      splitCommas: splitCommas, hide: hide);
}

/// A command-line option. Includes both flags and options which take a value.
class Option {
  final String name;
  final String abbreviation;
  final List<String> allowed;
  final defaultValue;
  final Function callback;
  final String help;
  final String valueHelp;
  final Map<String, String> allowedHelp;
  final OptionType type;
  final bool negatable;
  final bool splitCommas;
  final bool hide;

  /// 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.abbreviation, this.help, this.valueHelp,
      List<String> allowed, Map<String, String> allowedHelp, this.defaultValue,
      this.callback, OptionType type, {this.negatable, bool splitCommas,
      this.hide: false})
      : this.allowed = allowed == null
          ? null
          : new UnmodifiableListView(allowed),
        this.allowedHelp = allowedHelp == null
            ? null
            : new UnmodifiableMapView(allowedHelp),
        this.type = type,
        // If the user doesn't specify [splitCommas], it defaults to true for
        // multiple options.
        this.splitCommas = splitCommas == null
            ? type == OptionType.MULTIPLE
            : splitCommas {
    if (name.isEmpty) {
      throw new ArgumentError('Name cannot be empty.');
    } else if (name.startsWith('-')) {
      throw new ArgumentError('Name $name cannot start with "-".');
    }

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

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

      if (_invalidChars.hasMatch(abbreviation)) {
        throw new 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 [defaultValue] if set or `null`
  /// otherwise. For multiple-valued options, it will be an empty list or a
  /// list containing [defaultValue] if set.
  dynamic getOrDefault(value) {
    if (value != null) return value;

    if (!isMultiple) return defaultValue;
    if (defaultValue != null) return [defaultValue];
    return [];
  }

  static final _invalidChars = new 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 = const 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 = const 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 = const OptionType._("OptionType.MULTIPLE");

  final String name;

  const OptionType._(this.name);
}
