// 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:io';
import 'dart:math' as math;

import 'package:async/async.dart';

import '../util/io.dart';
import '../utils.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, and may return a [Future].
  void registerCommand(String name, String description, body()) {
    if (_commands.containsKey(name)) {
      throw new ArgumentError(
          'The console already has a command named "$name".');
    }

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

  /// Starts running the console.
  ///
  /// This prints the initial prompt and loops while waiting for user input.
  void start() {
    _running = true;
    invoke(() 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}");
    }
  }
}

/// An individual console command.
class _Command {
  /// The name of the command.
  final String name;

  /// The single-line description of the command.
  final String description;

  /// The callback to run when the command is invoked.
  final Function body;

  _Command(this.name, this.description, this.body);
}
