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

import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';

/**
 * Instances of the class [WebSocketClientChannel] implement a
 * [ClientCommunicationChannel] that uses a [WebSocket] to communicate with
 * servers.
 */
class WebSocketClientChannel implements ClientCommunicationChannel {
  /**
   * The socket being wrapped.
   */
  final WebSocket socket;

  @override
  Stream<Response> responseStream;

  @override
  Stream<Notification> notificationStream;

  /**
   * Initialize a new [WebSocket] wrapper for the given [socket].
   */
  WebSocketClientChannel(this.socket) {
    Stream jsonStream = socket
        .where((data) => data is String)
        .transform(new JsonStreamDecoder())
        .where((json) => json is Map)
        .asBroadcastStream();
    responseStream = jsonStream
        .where((json) => json[Notification.EVENT] == null)
        .transform(new ResponseConverter())
        .asBroadcastStream();
    notificationStream = jsonStream
        .where((json) => json[Notification.EVENT] != null)
        .transform(new NotificationConverter())
        .asBroadcastStream();
  }

  @override
  Future close() {
    return socket.close();
  }

  @override
  Future<Response> sendRequest(Request request) async {
    String id = request.id;
    socket.add(JSON.encode(request.toJson()));
    return await responseStream
        .firstWhere((Response response) => response.id == id);
  }
}

/**
 * Instances of the class [WebSocketServerChannel] implement a
 * [ServerCommunicationChannel] that uses a [WebSocket] to communicate with
 * clients.
 */
class WebSocketServerChannel implements ServerCommunicationChannel {
  /**
   * The socket being wrapped.
   */
  final WebSocket socket;

  /**
   * The instrumentation service that is to be used by this analysis server.
   */
  final InstrumentationService instrumentationService;

  /**
   * Initialize a newly create [WebSocket] wrapper to wrap the given [socket].
   */
  WebSocketServerChannel(this.socket, this.instrumentationService);

  @override
  void close() {
    socket.close(WebSocketStatus.NORMAL_CLOSURE);
  }

  @override
  void listen(void onRequest(Request request),
      {Function onError, void onDone()}) {
    socket.listen((data) => readRequest(data, onRequest),
        onError: onError, onDone: onDone);
  }

  /**
   * Read a request from the given [data] and use the given function to handle
   * the request.
   */
  void readRequest(Object data, void onRequest(Request request)) {
    if (data is String) {
      instrumentationService.logRequest(data);
      // Parse the string as a JSON descriptor and process the resulting
      // structure as a request.
      ServerPerformanceStatistics.serverChannel.makeCurrentWhile(() {
        Request request = new Request.fromString(data);
        if (request == null) {
          sendResponse(new Response.invalidRequestFormat());
          return;
        }
        onRequest(request);
      });
    } else if (data is List<int>) {
      // TODO(brianwilkerson) Implement a more efficient protocol.
      sendResponse(new Response.invalidRequestFormat());
    } else {
      sendResponse(new Response.invalidRequestFormat());
    }
  }

  @override
  void sendNotification(Notification notification) {
    ServerPerformanceStatistics.serverChannel.makeCurrentWhile(() {
      String jsonEncoding = JSON.encode(notification.toJson());
      socket.add(jsonEncoding);
      instrumentationService.logNotification(jsonEncoding);
    });
  }

  @override
  void sendResponse(Response response) {
    ServerPerformanceStatistics.serverChannel.makeCurrentWhile(() {
      String jsonEncoding = JSON.encode(response.toJson());
      socket.add(jsonEncoding);
      instrumentationService.logResponse(jsonEncoding);
    });
  }
}
