// Copyright (c) 2015, 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:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:stream_channel/stream_channel.dart';
import 'package:web_socket_channel/io.dart';

import 'src/exceptions.dart';
import 'src/flag.dart';
import 'src/isolate.dart';
import 'src/service_version.dart';
import 'src/stream_manager.dart';
import 'src/utils.dart';
import 'src/v1_compatibility.dart';
import 'src/vm.dart';

export 'src/bound_field.dart' hide newVMBoundField;
export 'src/breakpoint.dart' hide newVMBreakpoint;
export 'src/class.dart' hide newVMClassRef;
export 'src/code.dart' hide newVMCodeRef;
export 'src/context.dart' hide newVMContextRef;
export 'src/error.dart' hide newVMError, newVMErrorRef;
export 'src/exceptions.dart';
export 'src/field.dart' hide newVMFieldRef;
export 'src/flag.dart' hide newVMFlagList;
export 'src/frame.dart' hide newVMFrame;
export 'src/function.dart' hide newVMFunctionRef;
export 'src/instance.dart'
    hide
        newVMInstanceRef,
        newVMInstance,
        newVMTypeLikeInstanceRef,
        newVMTypeInstanceRef,
        newVMInstanceRefOrSentinel;
export 'src/isolate.dart' hide newVMIsolateRef;
export 'src/library.dart' hide newVMLibraryRef;
export 'src/message.dart' hide newVMMessage;
export 'src/object.dart';
export 'src/pause_event.dart' hide newVMPauseEvent;
export 'src/script.dart'
    hide newVMScriptRef, newVMScriptToken, newVMScriptTokenFromPosition;
export 'src/sentinel.dart' hide newVMSentinel;
export 'src/service_version.dart' hide newVMServiceVersion;
export 'src/source_location.dart'
    hide newVMSourceLocation, newVMSourceLocationFromPosition;
export 'src/source_report.dart' hide newSourceReport;
export 'src/stack.dart' hide newVMStack;
export 'src/type_arguments.dart' hide newVMTypeArgumentsRef;
export 'src/unresolved_source_location.dart' hide newVMUnresolvedSourceLocation;
export 'src/vm.dart' hide newVM;

/// A client for the [Dart VM service protocol][service api].
///
/// [service api]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
///
/// Connect to a VM service endpoint using [connect], and use [getVM] to load
/// information about the VM itself.
///
/// The client supports VM service versions 1.x (which first shipped with Dart
/// 1.11), 2.x (which first shipped with Dart 1.12), and 3.x (which first
/// shipped with Dart 1.13). Some functionality may be unavailable in older VM
/// service versions; those places will be clearly documented. You can check the
/// version of the VM service you're connected to using [getVersion].
///
/// Because it takes an extra RPC call to verify compatibility with the protocol
/// version, the client doesn't do so by default. Users who want to be sure
/// they're talking to a supported protocol version can call [validateVersion].
class VMServiceClient {
  /// The underlying JSON-RPC peer used to communicate with the VM service.
  final rpc.Peer _peer;

  /// The streams shared among the entire service protocol client.
  final StreamManager _streams;

  /// A broadcast stream that emits every isolate as it starts.
  Stream<VMIsolateRef> get onIsolateStart => _onIsolateStart;
  Stream<VMIsolateRef> _onIsolateStart;

  /// A broadcast stream that emits every isolate as it becomes runnable.
  ///
  /// These isolates are guaranteed to return a [VMRunnableIsolate] from
  /// [VMIsolateRef.load].
  ///
  /// This is only supported on the VM service protocol version 3.0 and greater.
  Stream<VMIsolateRef> get onIsolateRunnable => _onIsolateRunnable;
  Stream<VMIsolateRef> _onIsolateRunnable;

  /// A future that fires when the underlying connection has been closed.
  ///
  /// Any connection-level errors will also be emitted through this future.
  final Future done;

  /// Connects to the VM service protocol at [url].
  ///
  /// [url] may be a `ws://` or a `http://` URL. If it's `ws://`, it's
  /// interpreted as the URL to connect to directly. If it's `http://`, it's
  /// interpreted as the URL for the Dart observatory, and the corresponding
  /// WebSocket URL is determined based on that. It may be either a [String] or
  /// a [Uri].
  ///
  /// If this encounters a connection error, [done] will complete with a
  /// [WebSocketChannelException].
  factory VMServiceClient.connect(url) {
    if (url is! Uri && url is! String) {
      throw new ArgumentError.value(url, "url", "must be a String or a Uri");
    }

    var uri = url is String ? Uri.parse(url) : url;
    if (uri.scheme == 'http') uri = uri.replace(scheme: 'ws', path: '/ws');

    // TODO(nweiz): Just use [WebSocketChannel.connect] when cross-platform
    // libraries work.
    return new VMServiceClient(new IOWebSocketChannel.connect(uri).cast());
  }

  /// Creates a client that reads incoming messages from a [channel] which
  /// contains JSON-encoded [String] instances.
  ///
  /// This is useful when using the client over a pre-existing connection. To
  /// establish a connection from scratch, use [VMServiceClient.connect].
  factory VMServiceClient(StreamChannel<String> channel) =>
      new VMServiceClient.withoutJson(jsonDocument.bind(channel));

  /// Creates a client that reads incoming messages from a [channel] which
  /// contains decoded JSON maps and lists.
  ///
  /// This is useful when using the client over a pre-existing connection. To
  /// establish a connection from scratch, use [VMServiceClient.connect].
  factory VMServiceClient.withoutJson(StreamChannel channel) =>
      new VMServiceClient._(new rpc.Peer.withoutJson(
          channel.transformStream(v1CompatibilityTransformer)));

  VMServiceClient._(rpc.Peer peer)
      : _peer = peer,
        _streams = new StreamManager(peer),
        done = peer.listen() {
    _onIsolateStart = transform(_streams.isolate, (json, sink) {
      if (json["kind"] != "IsolateStart") return;
      sink.add(newVMIsolateRef(_peer, _streams, json["isolate"]));
    });

    _onIsolateRunnable = transform(_streams.isolate, (json, sink) {
      if (json["kind"] != "IsolateRunnable") return;
      sink.add(newVMIsolateRef(_peer, _streams, json["isolate"]));
    });
  }

  /// Checks the VM service protocol version and throws a
  /// [VMUnsupportedVersionException] if it's not a supported version.
  ///
  /// Because it's possible the VM service protocol doesn't speak JSON-RPC 2.0
  /// at all, by default this will also throw a [VMUnsupportedVersionException]
  /// if a reply isn't received within two seconds. This timeout can be
  /// controlled with [timeout], or `null` can be passed to use no timeout.
  Future validateVersion({Duration timeout: const Duration(seconds: 2)}) {
    var future = _peer.sendRequest("getVersion", {}).then((json) {
      var version;
      try {
        version = newVMServiceVersion(json);
      } catch (_) {
        throw new VMUnsupportedVersionException();
      }

      if (version.major < 2 || version.major > 3) {
        throw new VMUnsupportedVersionException(version);
      }
    });

    if (timeout == null) return future;

    return future.timeout(timeout, onTimeout: () {
      throw new VMUnsupportedVersionException();
    });
  }

  /// Closes the underlying connection to the VM service.
  ///
  /// Returns a [Future] that fires once the connection has been closed.
  Future close() => _peer.close();

  /// Returns a list of flags that were passed to the VM.
  ///
  /// As of VM service version 3.0, this only includes VM-internal flags.
  Future<List<VMFlag>> getFlags() async =>
      newVMFlagList(await _peer.sendRequest("getFlagList", {}));

  /// Returns the version of the VM service protocol that this client is
  /// communicating with.
  ///
  /// Note that this is distinct from the version of Dart, which is accessible
  /// via [VM.version]. Note also that this doesn't necessarily accurately
  /// reflect the available APIs; for example, VM service versions 3.1, 3.2, and
  /// 3.3 all reported themseves as version 3.0.
  Future<VMServiceVersion> getVersion() async =>
      newVMServiceVersion(await _peer.sendRequest("getVersion", {}));

  /// Returns information about the Dart VM.
  Future<VM> getVM() async =>
      newVM(_peer, _streams, await _peer.sendRequest("getVM", {}));
}
