// Copyright (c) 2022, 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:typed_data';

import 'message_grouper.dart';
import 'byte_data_serializer.dart';

/// Channel for exchanging requests and responses over [Socket].
class RequestChannel {
  final Socket _socket;
  final Map<int, Completer<Object?>> _requests = {};
  final Map<String, Future<Object?> Function(Object?)> _requestHandlers = {};

  int _nextRequestId = 0;

  RequestChannel(this._socket) {
    final MessageGrouper messageGrouper = new MessageGrouper(_socket);
    messageGrouper.messageStream.map((bytes) {
      final ByteDataDeserializer deserializer = new ByteDataDeserializer(
        new ByteData.sublistView(bytes),
      );
      return deserializer.expectAny();
    }).listen(_processMessage, cancelOnError: true, onDone: () {
      _socket.destroy();
    });
  }

  /// Registers the [handler] for the [method].
  void add(String method, Future<Object?> Function(Object?) handler) {
    _requestHandlers[method] = handler;
  }

  /// Sends a request with the given [method] and [argument], when it is
  /// handled on the other side by the handler registered with [add], the
  /// result is returned from the [Future].
  ///
  /// If an exception happens on the other side, the returned future completes
  /// with a [RequestChannelException].
  ///
  /// The other side may do callbacks.
  Future<T> sendRequest<T>(String method, Object? argument) async {
    final int requestId = _nextRequestId++;
    final Completer<Object?> completer = new Completer<Object?>();
    _requests[requestId] = completer;
    _writeObject({
      'requestId': requestId,
      'method': method,
      'argument': argument,
    });

    return await completer.future as T;
  }

  void _processMessage(Object? message) {
    if (message is Map<Object?, Object?>) {
      final Object? requestId = message['requestId'];
      if (requestId != null) {
        final Object? method = message['method'];
        final Object? argument = message['argument'];
        if (method != null) {
          final Future<Object?> Function(Object?)? handler =
              _requestHandlers[method];
          if (handler != null) {
            handler(argument).then((result) {
              _writeObject({
                'responseId': requestId,
                'result': result,
              });
            }, onError: (e, stackTrace) {
              _writeObject({
                'responseId': requestId,
                'exception': {
                  'message': '$e',
                  'stackTrace': '$stackTrace',
                },
              });
            });
          } else {
            _writeObject({
              'responseId': requestId,
              'exception': {
                'message': 'No handler for: $method',
                'stackTrace': '${StackTrace.current}',
              },
            });
          }
          return;
        }
      }

      final Object? responseId = message['responseId'];
      if (responseId != null) {
        final Completer<Object?>? completer = _requests.remove(responseId);
        if (completer == null) {
          return;
        }

        final Object? exception = message['exception'];
        if (exception is Map) {
          completer.completeError(
            new RequestChannelException(
              exception['message'] as String,
              stackTrace: exception['stackTrace'] as String,
            ),
          );
          return;
        }

        final Object? result = message['result'];
        completer.complete(result);
        return;
      }
    }

    throw new RequestChannelException('Unexpected message: $message');
  }

  /// Sends [bytes] for [MessageGrouper].
  void _writeBytes(List<int> bytes) {
    final ByteData lengthByteData = new ByteData(4)..setUint32(0, bytes.length);
    _socket.add(lengthByteData.buffer.asUint8List());
    _socket.add(bytes);
  }

  /// Serializes and sends the [object].
  void _writeObject(Object object) {
    final ByteDataSerializer serializer = new ByteDataSerializer();
    serializer.addAny(object);
    _writeBytes(serializer.result);
  }
}

/// Message channel failure or unexpected message.
class RequestChannelException {
  final String message;
  final String? stackTrace;

  RequestChannelException(this.message, {this.stackTrace});

  @override
  String toString() => 'RequestChannelException: $message'
      '${stackTrace == null ? '' : '\n$stackTrace'}';
}
