// Copyright (c) 2016, 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:async';
import 'dart:io';
import 'dart:math' as math;

import 'package:async/async.dart';

import '../util/io.dart';

/// An interactive console for taking user commands.
class Console {
  /// The registered commands.
  final _commands = <String, _Command>{};

  /// The pending next line of standard input, if we're waiting on one.
  CancelableOperation? _nextLine;

  /// Whether the console is currently running.
  bool _running = false;

  /// The terminal escape for red text, or the empty string if this is Windows
  /// or not outputting to a terminal.
  final String _red;

  /// The terminal escape for bold text, or the empty string if this is
  /// Windows or not outputting to a terminal.
  final String _bold;

  /// The terminal escape for removing test coloring, or the empty string if
  /// this is Windows or not outputting to a terminal.
  final String _noColor;

  /// Creates a new [Console].
  ///
  /// If [color] is true, this uses Unix terminal colors.
  Console({bool color = true})
      : _red = color ? '\u001b[31m' : '',
        _bold = color ? '\u001b[1m' : '',
        _noColor = color ? '\u001b[0m' : '' {
    registerCommand('help', 'Displays this help information.', _displayHelp);
  }

  /// Registers a command to be run whenever the user types [name].
  ///
  /// The [description] should be a one-line description of the command to print
  /// in the help output. The [body] callback will be called when the user types
  /// the command.
  void registerCommand(
      String name, String description, FutureOr<void> Function() body) {
    if (_commands.containsKey(name)) {
      throw ArgumentError('The console already has a command named "$name".');
    }

    _commands[name] = (name: name, description: description, body: body);
  }

  /// Starts running the console.
  ///
  /// This prints the initial prompt and loops while waiting for user input.
  void start() {
    _running = true;
    unawaited(() async {
      while (_running) {
        stdout.write('> ');
        _nextLine = stdinLines.cancelable((queue) => queue.next);
        var commandName = await _nextLine!.value;
        _nextLine = null;

        var command = _commands[commandName];
        if (command == null) {
          stderr.writeln(
              '${_red}Unknown command $_bold$commandName$_noColor$_red.'
              '$_noColor');
        } else {
          await command.body();
        }
      }
    }());
  }

  /// Stops the console running.
  void stop() {
    _running = false;
    if (_nextLine != null) {
      stdout.writeln();
      _nextLine!.cancel();
    }
  }

  /// Displays the help info for the console commands.
  void _displayHelp() {
    var maxCommandLength =
        _commands.values.map((command) => command.name.length).reduce(math.max);

    for (var command in _commands.values) {
      var name = command.name.padRight(maxCommandLength + 4);
      print('$_bold$name$_noColor${command.description}');
    }
  }
}

typedef _Command = ({
  String name,
  String description,
  FutureOr<void> Function() body,
});
