// Copyright 2015 The Chromium Authors. 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:convert';
import 'dart:io';

import 'package:logging/logging.dart';

import 'flutter_command.dart';
import 'start.dart';
import 'stop.dart';

const String protocolVersion = '0.0.1';

/// A @domain annotation.
const String domain = 'domain';

/// A domain @command annotation.
const String command = 'command';

final Logger _logging = new Logger('flutter_tools.daemon');

// TODO: Create a `device` domain in order to list devices and fire events when
// devices are added or removed.

// TODO: Is this the best name? Server? Daemon?

/// A server process command. This command will start up a long-lived server.
/// It reads JSON-RPC based commands from stdin, executes them, and returns
/// JSON-RPC based responses and events to stdout.
///
/// It can be shutdown with a `daemon.shutdown` command (or by killing the
/// process).
class DaemonCommand extends FlutterCommand {
  final String name = 'daemon';
  final String description =
      'Run a persistent, JSON-RPC based server to communicate with devices.';
  final String usageFooter =
      '\nThis command is intended to be used by tooling environments that need '
      'a programatic interface into launching Flutter applications.';

  @override
  Future<int> runInProject() async {
    print('Starting device daemon...');

    Stream<Map> commandStream = stdin
      .transform(UTF8.decoder)
      .transform(const LineSplitter())
      .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
      .map((String line) {
        line = line.substring(1, line.length - 1);
        return JSON.decode(line);
      });

    await downloadApplicationPackagesAndConnectToDevices();

    Daemon daemon = new Daemon(commandStream, (Map command) {
      stdout.writeln('[${JSON.encode(command)}]');
    }, daemonCommand: this);

    return await daemon.onExit;
  }
}

typedef void DispatchComand(Map command);

typedef Future<dynamic> CommandHandler(dynamic args);

class Daemon {
  final DispatchComand sendCommand;
  final DaemonCommand daemonCommand;

  final Completer<int> _onExitCompleter = new Completer();
  final Map<String, Domain> _domains = {};

  Daemon(Stream<Map> commandStream, this.sendCommand, {this.daemonCommand}) {
    // Set up domains.
    _registerDomain(new DaemonDomain(this));
    _registerDomain(new AppDomain(this));

    // Start listening.
    commandStream.listen(
      (Map command) => _handleCommand(command),
      onDone: () => _onExitCompleter.complete(0)
    );
  }

  void _registerDomain(Domain domain) {
    _domains[domain.name] = domain;
  }

  Future<int> get onExit => _onExitCompleter.future;

  void _handleCommand(Map command) {
    // {id, event, params}
    var id = command['id'];

    if (id == null) {
      _logging.severe('no id for command: $command');
      return;
    }

    try {
      String event = command['event'];
      if (event.indexOf('.') == -1)
        throw 'command not understood: $event';

      String prefix = event.substring(0, event.indexOf('.'));
      String name = event.substring(event.indexOf('.') + 1);
      if (_domains[prefix] == null)
        throw 'no domain for command: $command';

      _domains[prefix].handleEvent(name, id, command['params']);
    } catch (error, trace) {
      _send({'id': id, 'error': _toJsonable(error)});
      _logging.warning('error handling ${command['event']}', error, trace);
    }
  }

  void _send(Map map) => sendCommand(map);

  void shutdown() {
    if (!_onExitCompleter.isCompleted)
      _onExitCompleter.complete(0);
  }
}

abstract class Domain {
  final Daemon daemon;
  final String name;
  final Map<String, CommandHandler> _handlers = {};

  Domain(this.daemon, this.name);

  void registerHandler(String name, CommandHandler handler) {
    _handlers[name] = handler;
  }

  String toString() => name;

  void handleEvent(String name, dynamic id, dynamic args) {
    new Future.sync(() {
      if (_handlers.containsKey(name))
        return _handlers[name](args);
      throw 'command not understood: $name';
    }).then((result) {
      if (result == null) {
        _send({'id': id});
      } else {
        _send({'id': id, 'result': _toJsonable(result)});
      }
    }).catchError((error, trace) {
      _send({'id': id, 'error': _toJsonable(error)});
      _logging.warning('error handling $name', error, trace);
    });
  }

  void _send(Map map) => daemon._send(map);
}

/// This domain responds to methods like [version] and [shutdown].
@domain
class DaemonDomain extends Domain {
  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
    registerHandler('version', version);
    registerHandler('shutdown', shutdown);
  }

  @command
  Future<dynamic> version(dynamic args) {
    return new Future.value(protocolVersion);
  }

  @command
  Future<dynamic> shutdown(dynamic args) {
    Timer.run(() => daemon.shutdown());
    return new Future.value();
  }
}

/// This domain responds to methods like [start] and [stopAll].
///
/// It'll be extended to fire events for when applications start, stop, and
/// log data.
@domain
class AppDomain extends Domain {
  AppDomain(Daemon daemon) : super(daemon, 'app') {
    registerHandler('start', start);
    registerHandler('stopAll', stopAll);
  }

  @command
  Future<dynamic> start(dynamic args) {
    // TODO: Add the ability to pass args: target, http, checked
    StartCommand startComand = new StartCommand();
    startComand.inheritFromParent(daemon.daemonCommand);
    return startComand.runInProject().then((_) => null);
  }

  @command
  Future<bool> stopAll(dynamic args) {
    StopCommand stopCommand = new StopCommand();
    stopCommand.inheritFromParent(daemon.daemonCommand);
    return stopCommand.stop();
  }
}

dynamic _toJsonable(dynamic obj) {
  if (obj is String || obj is int || obj is bool || obj is Map || obj is List || obj == null)
    return obj;
  return '$obj';
}
