// Copyright 2016 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' show BASE64;
import 'dart:io';

import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:web_socket_channel/io.dart';

class Observatory {
  Observatory._(this.peer, this.port) {
    peer.registerMethod('streamNotify', (rpc.Parameters event) {
      _handleStreamNotify(event.asMap);
    });

    onIsolateEvent.listen((Event event) {
      if (event.kind == 'IsolateStart') {
        _addIsolate(event.isolate);
      } else if (event.kind == 'IsolateExit') {
        String removedId = event.isolate.id;
        isolates.removeWhere((IsolateRef ref) => ref.id == removedId);
      }
    });
  }

  static Future<Observatory> connect(int port) async {
    Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: port, path: 'ws');
    WebSocket ws = await WebSocket.connect(uri.toString());
    rpc.Peer peer = new rpc.Peer(new IOWebSocketChannel(ws));
    peer.listen();
    return new Observatory._(peer, port);
  }

  final rpc.Peer peer;
  final int port;

  List<IsolateRef> isolates = <IsolateRef>[];
  Completer<IsolateRef> _waitFirstIsolateCompleter;

  Map<String, StreamController<Event>> _eventControllers = <String, StreamController<Event>>{};

  Set<String> _listeningFor = new Set<String>();

  bool get isClosed => peer.isClosed;
  Future<Null> get done => peer.done;

  String get firstIsolateId => isolates.isEmpty ? null : isolates.first.id;

  // Events

  Stream<Event> get onExtensionEvent => onEvent('Extension');
  // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded
  Stream<Event> get onIsolateEvent => onEvent('Isolate');
  Stream<Event> get onTimelineEvent => onEvent('Timeline');

  // Listen for a specific event name.
  Stream<Event> onEvent(String streamId) {
    streamListen(streamId);
    return _getEventController(streamId).stream;
  }

  StreamController<Event> _getEventController(String eventName) {
    StreamController<Event> controller = _eventControllers[eventName];
    if (controller == null) {
      controller = new StreamController<Event>.broadcast();
      _eventControllers[eventName] = controller;
    }
    return controller;
  }

  void _handleStreamNotify(Map<String, dynamic> data) {
    Event event = new Event(data['event']);
    _getEventController(data['streamId']).add(event);
  }

  Future<Null> populateIsolateInfo() async {
    // Calling this has the side effect of populating the isolate information.
    await waitFirstIsolate;
  }

  Future<IsolateRef> get waitFirstIsolate async {
    if (isolates.isNotEmpty)
      return isolates.first;

    _waitFirstIsolateCompleter ??= new Completer<IsolateRef>();

    getVM().then((VM vm) {
      for (IsolateRef isolate in vm.isolates)
        _addIsolate(isolate);
    });

    return _waitFirstIsolateCompleter.future;
  }

  // Requests

  Future<Response> sendRequest(String method, [Map<String, dynamic> args]) {
    return peer.sendRequest(method, args).then((dynamic result) => new Response(result));
  }

  Future<Null> streamListen(String streamId) async {
    if (!_listeningFor.contains(streamId)) {
      _listeningFor.add(streamId);
      sendRequest('streamListen', <String, dynamic>{ 'streamId': streamId });
    }
  }

  Future<VM> getVM() {
    return peer.sendRequest('getVM').then((dynamic result) {
      return new VM(result);
    });
  }

  Future<Response> isolateReload(String isolateId) {
    return sendRequest('isolateReload', <String, dynamic>{
      'isolateId': isolateId
    });
  }

  Future<Response> clearVMTimeline() => sendRequest('_clearVMTimeline');

  Future<Response> setVMTimelineFlags(List<String> recordedStreams) {
    assert(recordedStreams != null);

    return sendRequest('_setVMTimelineFlags', <String, dynamic> {
      'recordedStreams': recordedStreams
    });
  }

  Future<Response> getVMTimeline() => sendRequest('_getVMTimeline');

  // DevFS / VM virtual file system methods

  /// Create a new file system.
  ///
  /// When you create a file system you provide a fsName parameter. Given the
  /// [fsName] parameter you can build all the URIs you need with the following
  /// format:
  ///
  ///     dart-devfs://$fsName/$path
  Future<Response> createDevFS(String fsName) {
    return sendRequest('_createDevFS', <String, dynamic> { 'fsName': fsName });
  }

  /// List the available file systems.
  Future<List<String>> listDevFS() {
    return sendRequest('_listDevFS').then((Response response) {
      return response.response['fsNames'];
    });
  }

  // Write one file into a file system.
  Future<Response> writeDevFSFile(String fsName, {
    String path,
    List<int> fileContents
  }) {
    assert(path != null);
    assert(fileContents != null);

    return sendRequest('_writeDevFSFile', <String, dynamic> {
      'fsName': fsName,
      'path': path,
      'fileContents': BASE64.encode(fileContents)
    });
  }

  // Write multiple files into a file system.
  Future<Response> writeDevFSFiles(String fsName, {
    List<DevFSFile> files
  }) {
    assert(files != null);

    return sendRequest('_writeDevFSFiles', <String, dynamic> {
      'fsName': fsName,
      'files': files.map((DevFSFile file) => file.toJson()).toList()
    });
  }

  // Read one file from a file system.
  Future<List<int>> readDevFSFile() {
    return sendRequest('_readDevFSFile').then((Response response) {
      return BASE64.decode(response.response['fileContents']);
    });
  }

  /// The complete list of a file system.
  Future<List<String>> listDevFSFiles(String fsName) {
    return sendRequest('_listDevFSFiles', <String, dynamic> {
      'fsName': fsName
    }).then((Response response) {
      return response.response['files'];
    });
  }

  /// Delete an existing file system.
  Future<Response> deleteDevFS(String fsName) {
    return sendRequest('_deleteDevFS', <String, dynamic> { 'fsName': fsName });
  }

  // Flutter extension methods.

  Future<Response> flutterExit(String isolateId) {
    return peer.sendRequest('ext.flutter.exit', <String, dynamic>{
      'isolateId': isolateId
    }).then((dynamic result) => new Response(result));
  }

  void _addIsolate(IsolateRef isolate) {
    if (!isolates.contains(isolate)) {
      isolates.add(isolate);

      if (_waitFirstIsolateCompleter != null) {
        _waitFirstIsolateCompleter.complete(isolate);
        _waitFirstIsolateCompleter = null;
      }
    }
  }
}

abstract class DevFSFile {
  DevFSFile(this.path);

  final String path;

  List<int> getContents();

  List<String> toJson() => <String>[path, BASE64.encode(getContents())];
}

class ByteDevFSFile extends DevFSFile {
  ByteDevFSFile(String path, this.contents): super(path);

  final List<int> contents;

  @override
  List<int> getContents() => contents;
}

class Response {
  Response(this.response);

  final Map<String, dynamic> response;

  String get type => response['type'];

  dynamic operator[](String key) => response[key];

  @override
  String toString() => response.toString();
}

class VM extends Response {
  VM(Map<String, dynamic> response) : super(response);

  List<IsolateRef> get isolates => response['isolates'].map((dynamic ref) => new IsolateRef(ref)).toList();
}

class Event extends Response {
  Event(Map<String, dynamic> response) : super(response);

  String get kind => response['kind'];
  IsolateRef get isolate => new IsolateRef.from(response['isolate']);

  /// Only valid for [kind] == `Extension`.
  String get extensionKind => response['extensionKind'];
}

class IsolateRef extends Response {
  IsolateRef(Map<String, dynamic> response) : super(response);
  factory IsolateRef.from(dynamic ref) => ref == null ? null : new IsolateRef(ref);

  String get id => response['id'];

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! IsolateRef)
      return false;
    final IsolateRef typedOther = other;
    return id == typedOther.id;
  }

  @override
  int get hashCode => id.hashCode;
}
