// 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:async';

import 'package:stream_channel/stream_channel.dart';

import 'channel_manager.dart';
import 'client.dart';
import 'parameters.dart';
import 'server.dart';
import 'utils.dart';

/// A JSON-RPC 2.0 client *and* server.
///
/// This supports bidirectional peer-to-peer communication with another JSON-RPC
/// 2.0 endpoint. It sends both requests and responses across the same
/// communication channel and expects to connect to a peer that does the same.
class Peer implements Client, Server {
  final ChannelManager _manager;

  /// The underlying client that handles request-sending and response-receiving
  /// logic.
  Client _client;

  /// The underlying server that handles request-receiving and response-sending
  /// logic.
  Server _server;

  /// A stream controller that forwards incoming messages to [_server] if
  /// they're requests.
  final _serverIncomingForwarder = new StreamController(sync: true);

  /// A stream controller that forwards incoming messages to [_client] if
  /// they're responses.
  final _clientIncomingForwarder = new StreamController(sync: true);

  Future get done => _manager.done;
  bool get isClosed => _manager.isClosed;

  /// Creates a [Peer] that communicates over [channel].
  ///
  /// Note that the peer won't begin listening to [channel] until [Peer.listen]
  /// is called.
  Peer(StreamChannel<String> channel)
      : this.withoutJson(
            jsonDocument.bind(channel).transform(respondToFormatExceptions));

  /// Creates a [Peer] that communicates using decoded messages over [channel].
  ///
  /// Unlike [new Peer], this doesn't read or write JSON strings. Instead, it
  /// reads and writes decoded maps or lists.
  ///
  /// Note that the peer won't begin listening to [channel] until
  /// [Peer.listen] is called.
  Peer.withoutJson(StreamChannel channel)
      : _manager = new ChannelManager("Peer", channel) {
    _server = new Server.withoutJson(
        new StreamChannel(_serverIncomingForwarder.stream, channel.sink));
    _client = new Client.withoutJson(
        new StreamChannel(_clientIncomingForwarder.stream, channel.sink));
  }

  // Client methods.

  Future sendRequest(String method, [parameters]) =>
      _client.sendRequest(method, parameters);

  void sendNotification(String method, [parameters]) =>
      _client.sendNotification(method, parameters);

  withBatch(callback()) => _client.withBatch(callback);

  // Server methods.

  void registerMethod(String name, Function callback) =>
      _server.registerMethod(name, callback);

  void registerFallback(callback(Parameters parameters)) =>
      _server.registerFallback(callback);

  // Shared methods.

  Future listen() {
    _client.listen();
    _server.listen();
    return _manager.listen((message) {
      if (message is Map) {
        if (message.containsKey('result') || message.containsKey('error')) {
          _clientIncomingForwarder.add(message);
        } else {
          _serverIncomingForwarder.add(message);
        }
      } else if (message is List &&
          message.isNotEmpty &&
          message.first is Map) {
        if (message.first.containsKey('result') ||
            message.first.containsKey('error')) {
          _clientIncomingForwarder.add(message);
        } else {
          _serverIncomingForwarder.add(message);
        }
      } else {
        // Non-Map and -List messages are ill-formed, so we pass them to the
        // server since it knows how to send error responses.
        _serverIncomingForwarder.add(message);
      }
    });
  }

  Future close() =>
      Future.wait([_client.close(), _server.close(), _manager.close()]);
}
