// Copyright (c) 2021, 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 '../messages/codes.dart';
import 'resolve_input_uri.dart';

class CommandLineProblem {
  final Message message;

  CommandLineProblem(this.message);

  CommandLineProblem.deprecated(String message)
      : this(templateUnspecified.withArguments(message));

  @override
  String toString() => message.problemMessage;
}

class ParsedOptions {
  final Map<String, dynamic> options = <String, dynamic>{};
  final List<String> arguments = <String>[];
  final Map<String, String> defines = <String, String>{};

  @override
  String toString() => "ParsedArguments($options, $arguments)";

  /// Returns arguments stored as line separated text.
  static List<String> readOptionsFile(String optionsFile) {
    return optionsFile
        .split('\n')
        .map((String line) => line.trim())
        .where((line) => line.isNotEmpty)
        .toList();
  }

  /// Parses a list of command-line [arguments] into options and arguments.
  ///
  /// An /option/ is something that, normally, starts with `-` or `--` (one or
  /// two dashes). However, as a special case `/?` and `/h` are also recognized
  /// as options for increased compatibility with Windows. An option can have a
  /// value.
  ///
  /// An /argument/ is something that isn't an option, for example, a file name.
  ///
  /// The specification is a map of options to one of the following values:
  /// * the type literal `Uri`, representing an option value of type [Uri],
  /// * the type literal `int`, representing an option value of type [int],
  /// * the bool literal `false`, representing a boolean option that is turned
  ///   off by default,
  /// * the bool literal `true, representing a boolean option that is turned on
  ///   by default,
  /// * or the string literal `","`, representing a comma-separated list of
  ///   values.
  ///
  /// If [arguments] contains `"--"`, anything before is parsed as options, and
  /// arguments; anything following is treated as arguments (even if starting
  /// with, for example, a `-`).
  ///
  /// If an option isn't found in [options], an error is thrown.
  ///
  /// Boolean options do not require an option value, but an optional value can
  /// be provided using the forms `--option=value` where `value` can be `true`
  /// or `yes` to turn on the option, or `false` or `no` to turn it off.  If no
  /// option value is specified, a boolean option is turned on.
  ///
  /// All other options require an option value, either on the form `--option
  /// value` or `--option=value`.
  static ParsedOptions parse(List<String> arguments, List<Option>? options) {
    options ??= [];
    Map<String, ValueSpecification>? specification = {};
    void addSpec(String flag, ValueSpecification spec) {
      if (specification.containsKey(flag)) {
        throw new CommandLineProblem.deprecated("Duplicate option '${flag}'.");
      }
      specification[flag] = spec;
    }

    for (Option option in options) {
      addSpec(option.flag, option.spec);
      for (String alias in option.aliases) {
        addSpec(alias, new AliasValue(option.flag));
      }
    }
    ParsedOptions result = new ParsedOptions();
    int index = arguments.indexOf("--");
    Iterable<String> nonOptions = const <String>[];
    Iterator<String> iterator = arguments.iterator;
    if (index != -1) {
      nonOptions = arguments.skip(index + 1);
      iterator = arguments.take(index).iterator;
    }
    while (iterator.moveNext()) {
      String argument = iterator.current;
      if (argument.startsWith("-") || argument == "/?" || argument == "/h") {
        String? value;
        if (argument.startsWith("-D")) {
          value = argument.substring("-D".length);
          argument = "-D";
        } else {
          index = argument.indexOf("=");
          if (index != -1) {
            value = argument.substring(index + 1);
            argument = argument.substring(0, index);
          }
        }
        ValueSpecification? valueSpecification = specification[argument];
        if (valueSpecification == null) {
          throw new CommandLineProblem.deprecated(
              "Unknown option '$argument'.");
        }
        String canonicalArgument = argument;
        if (valueSpecification.alias != null) {
          canonicalArgument = valueSpecification.alias as String;
          valueSpecification = specification[valueSpecification.alias];
        }
        if (valueSpecification == null) {
          throw new CommandLineProblem.deprecated(
              "Unknown option alias '$canonicalArgument'.");
        }
        final bool requiresValue = valueSpecification.requiresValue;
        if (requiresValue && value == null) {
          if (!iterator.moveNext()) {
            throw new CommandLineProblem(
                templateFastaCLIArgumentRequired.withArguments(argument));
          }
          value = iterator.current;
        }
        valueSpecification.processValue(
            result, canonicalArgument, argument, value);
      } else {
        result.arguments.add(argument);
      }
    }
    specification.forEach((String key, ValueSpecification value) {
      if (value.defaultValue != null) {
        result.options[key] ??= value.defaultValue;
      }
    });
    result.arguments.addAll(nonOptions);
    return result;
  }
}

abstract class ValueSpecification<T> {
  const ValueSpecification();

  String? get alias => null;

  T? get defaultValue => null;

  bool get requiresValue => true;

  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value);
}

class AliasValue<T> extends ValueSpecification<T> {
  @override
  final String alias;

  const AliasValue(this.alias);

  @override
  bool get requiresValue =>
      throw new UnsupportedError("AliasValue.requiresValue");

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    throw new UnsupportedError("AliasValue.processValue");
  }
}

class UriValue extends ValueSpecification<Uri?> {
  const UriValue();

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    if (result.options.containsKey(canonicalArgument)) {
      throw new CommandLineProblem.deprecated(
          "Multiple values for '$argument': "
          "'${result.options[canonicalArgument]}' and '$value'.");
    }
    // TODO(ahe): resolve Uris lazily, so that schemes provided by
    // other flags can be used for parsed command-line arguments too.
    result.options[canonicalArgument] = resolveInputUri(value!);
  }
}

class StringValue extends ValueSpecification<String?> {
  @override
  final String? defaultValue;

  const StringValue({this.defaultValue});

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    if (result.options.containsKey(canonicalArgument)) {
      throw new CommandLineProblem.deprecated(
          "Multiple values for '$argument': "
          "'${result.options[canonicalArgument]}' and '$value'.");
    }
    result.options[canonicalArgument] = value!;
  }
}

class BoolValue extends ValueSpecification<bool?> {
  @override
  final bool? defaultValue;

  const BoolValue(this.defaultValue);

  @override
  bool get requiresValue => false;

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    if (result.options.containsKey(canonicalArgument)) {
      throw new CommandLineProblem.deprecated(
          "Multiple values for '$argument': "
          "'${result.options[canonicalArgument]}' and '$value'.");
    }
    bool parsedValue;
    if (value == null || value == "true" || value == "yes") {
      parsedValue = true;
    } else if (value == "false" || value == "no") {
      parsedValue = false;
    } else {
      throw new CommandLineProblem.deprecated(
          "Value for '$argument' is '$value', "
          "but expected one of: 'true', 'false', 'yes', or 'no'.");
    }
    result.options[canonicalArgument] = parsedValue;
  }
}

class IntValue extends ValueSpecification<int?> {
  @override
  final int? defaultValue;
  final int? noArgValue;

  const IntValue({this.defaultValue, this.noArgValue});

  @override
  bool get requiresValue => noArgValue == null;

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    if (result.options.containsKey(canonicalArgument)) {
      throw new CommandLineProblem.deprecated(
          "Multiple values for '$argument': "
          "'${result.options[canonicalArgument]}' and '$value'.");
    }
    int? parsedValue = noArgValue;
    if (value != null) {
      parsedValue = int.tryParse(value);
    }
    if (parsedValue == null) {
      throw new CommandLineProblem.deprecated(
          "Value for '$argument', '$value', isn't an int.");
    }
    result.options[canonicalArgument] = parsedValue;
  }
}

class DefineValue extends ValueSpecification<Map<String, String>> {
  const DefineValue();

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    int index = value!.indexOf('=');
    String name;
    String expression;
    if (index != -1) {
      name = value.substring(0, index);
      expression = value.substring(index + 1);
    } else {
      name = value;
      expression = value;
    }
    result.defines[name] = expression;
  }
}

class StringListValue extends ValueSpecification<List<String>?> {
  const StringListValue();

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    List<String> values = result.options[canonicalArgument] ??= <String>[];
    values.addAll(value!.split(","));
  }
}

class UriListValue extends ValueSpecification<List<Uri>?> {
  const UriListValue();

  @override
  void processValue(ParsedOptions result, String canonicalArgument,
      String argument, String? value) {
    List<Uri> values = result.options[canonicalArgument] ??= <Uri>[];
    values.addAll(value!.split(",").map(resolveInputUri));
  }
}

class Option<T> {
  final String flag;

  final ValueSpecification<T?> spec;

  final bool isDefines;

  final List<String> aliases;

  const Option(this.flag, this.spec,
      {this.isDefines = false, this.aliases = const []});

  T read(ParsedOptions parsedOptions) {
    if (isDefines) {
      return parsedOptions.defines as T;
    }
    return parsedOptions.options[flag] as T;
  }

  List<String> remove(List<String> arguments) {
    arguments.remove(flag);
    for (String alias in aliases) {
      arguments.remove(alias);
    }
    return arguments;
  }
}
