// Copyright (c) 2023, 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 'generate_dart_common.dart';

class VmServiceApi extends Api {
  static const _clientHeaderCode = r'''
// 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.

// This is a generated file. To regenerate, run `dart tool/generate.dart`.

/// A library to access the VM Service API.
///
/// The main entry-point for this library is the [VmService] class.
library;

// ignore_for_file: overridden_fields

import 'dart:async';
import 'dart:convert'
    show base64, jsonDecode, JsonDecoder, jsonEncode, utf8, Utf8Decoder;
import 'dart:typed_data';

export 'snapshot_graph.dart' show HeapSnapshotClass,
                                  HeapSnapshotExternalProperty,
                                  HeapSnapshotField,
                                  HeapSnapshotGraph,
                                  HeapSnapshotObject,
                                  HeapSnapshotObjectLengthData,
                                  HeapSnapshotObjectNoData,
                                  HeapSnapshotObjectNullData;
''';

  static const _implCode = r'''

  /// Call an arbitrary service protocol method. This allows clients to call
  /// methods not explicitly exposed by this library.
  Future<Response> callMethod(String method, {
    String? isolateId,
    Map<String, dynamic>? args
  }) {
    return callServiceExtension(method, isolateId: isolateId, args: args);
  }

  /// Invoke a specific service protocol extension method.
  ///
  /// See https://api.dart.dev/dart-developer/dart-developer-library.html.
  Future<Response> callServiceExtension(String method, {
    String? isolateId,
    Map<String, dynamic>? args
  }) {
    if (args == null && isolateId == null) {
      return _call(method);
    } else if (args == null) {
      return _call(method, {'isolateId': isolateId!});
    } else {
      args = Map.from(args);
      if (isolateId != null) {
        args['isolateId'] = isolateId;
      }
      return _call(method, args);
    }
  }

  Future<void> dispose() async {
    if (_disposed) {
      return;
    }
    _disposed = true;
    await _streamSub.cancel();
    _outstandingRequests.forEach((id, request) {
      request.completeError(RPCError(
        request.method,
        RPCErrorKind.kServerError.code,
        'Service connection disposed',
      ));
    });
    _outstandingRequests.clear();
    final handler = _disposeHandler;
    if (handler != null) {
      await handler();
    }
    assert(!_onDoneCompleter.isCompleted);
    _onDoneCompleter.complete();
  }

  /// When overridden, this method wraps [future] with logic.
  ///
  /// [wrapFuture] is called by [_call], which is the method that each VM
  /// service endpoint eventually goes through.
  ///
  /// This method should be overridden if subclasses of [VmService] need to do
  /// anything special upon calling the VM service, like tracking futures or
  /// logging requests.
  Future<T> wrapFuture<T>(String name, Future<T> future) {
    return future;
  }

  Future<T> _call<T>(String method, [Map args = const {}]) {
    if (_disposed) {
      throw RPCError(
        method,
        RPCErrorKind.kServerError.code,
        'Service connection disposed',
      );
    }
    return wrapFuture<T>(
      method,
      () {
        final request = _OutstandingRequest<T>(method);
        _outstandingRequests[request.id] = request;
        Map m = {
          'jsonrpc': '2.0',
          'id': request.id,
          'method': method,
          'params': args,
        };
        String message = jsonEncode(m);
        _onSend.add(message);
        _writeMessage(message);
        return request.future;
      }(),
    );
  }

  /// Register a service for invocation.
  void registerServiceCallback(String service, ServiceCallback cb) {
    if (_services.containsKey(service)) {
      throw Exception('Service \'$service\' already registered');
    }
    _services[service] = cb;
  }

  void _processMessage(dynamic message) {
    // Expect a String, an int[], or a ByteData.
    if (message is String) {
      _processMessageStr(message);
    } else if (message is Uint8List) {
      _processMessageByteData(ByteData.view(
          message.buffer, message.offsetInBytes, message.lengthInBytes));
    } else if (message is List<int>) {
      final list = Uint8List.fromList(message);
      _processMessageByteData(ByteData.view(list.buffer));
    } else if (message is ByteData) {
      _processMessageByteData(message);
    } else {
      _log.warning('unknown message type: ${message.runtimeType}');
    }
  }

  void _processMessageByteData(ByteData bytes) {
    final int metaOffset = 4;
    final int dataOffset = bytes.getUint32(0, Endian.little);
    final metaLength = dataOffset - metaOffset;
    final dataLength = bytes.lengthInBytes - dataOffset;
    final decoder = (const Utf8Decoder()).fuse(const JsonDecoder());
    final map = decoder.convert(Uint8List.view(
        bytes.buffer, bytes.offsetInBytes + metaOffset, metaLength)) as dynamic;
    final data = ByteData.view(
        bytes.buffer, bytes.offsetInBytes + dataOffset, dataLength);
    if (map['method'] == 'streamNotify') {
      final streamId = map['params']['streamId'];
      final event = map['params']['event'];
      event['data'] = data;
      _getEventController(streamId)
          .add(createServiceObject(event, const ['Event'])! as Event);
    }
  }

  void _processMessageStr(String message) {
    try {
      _onReceive.add(message);
      final json = jsonDecode(message)!;
      if (json.containsKey('method')) {
        if (json.containsKey('id')) {
          _processRequest(json);
        } else {
          _processNotification(json);
        }
      } else if (json.containsKey('id') &&
          (json.containsKey('result') || json.containsKey('error'))) {
        _processResponse(json);
      }
      else {
      _log.severe('unknown message type: $message');
      }
    } catch (e, s) {
      _log.severe('unable to decode message: $message, $e\n$s');
      return;
    }
  }

  void _processResponse(Map<String, dynamic> json) {
    final request = _outstandingRequests.remove(json['id']);
    if (request == null) {
      _log.severe('unmatched request response: ${jsonEncode(json)}');
    } else if (json['error'] != null) {
      request.completeError(RPCError.parse(request.method, json['error']));
    } else {
      final result = json['result'] as Map<String, dynamic>;
      final type = result['type'];
      if (type == 'Sentinel') {
        request.completeError(SentinelException.parse(request.method, result));
      } else if (_typeFactories[type] == null) {
        request.complete(Response.parse(result));
      } else {
        final returnTypes = _methodReturnTypes[request.method] ?? <String>[];
        request.complete(createServiceObject(result, returnTypes));
      }
    }
  }

  Future _processRequest(Map<String, dynamic> json) async {
    final result = await _routeRequest(json['method'], json['params'] ?? <String, dynamic>{});
    if (_disposed) {
      // The service has disappeared. Don't try to send the response.
      return;
    }
    result['id'] = json['id'];
    result['jsonrpc'] = '2.0';
    String message = jsonEncode(result);
    _onSend.add(message);
    _writeMessage(message);
  }

  Future _processNotification(Map<String, dynamic> json) async {
    final method = json['method'];
    final params = json['params'] ?? <String, dynamic>{};
    if (method == 'streamNotify') {
      final streamId = params['streamId'];
      _getEventController(streamId).add(createServiceObject(params['event'], const ['Event'])! as Event);
    } else {
      await _routeRequest(method, params);
    }
  }

  Future<Map> _routeRequest(String method, Map<String, dynamic> params) async {
    final service = _services[method];
    if (service == null) {
      final error = RPCError(method, RPCErrorKind.kMethodNotFound.code,
          'method not found \'$method\'');
      return {'error': error.toMap()};
    }

    try {
      return await service(params);
    } catch (e, st) {
      RPCError error = RPCError.withDetails(
        method,
        RPCErrorKind.kServerError.code,
        '$e',
        details: '$st',
      );
      return {'error': error.toMap()};
    }
  }
''';

  static const _rpcError = r'''

typedef DisposeHandler = Future Function();

// These error codes must be kept in sync with those in vm/json_stream.h and
// vmservice.dart.
enum RPCErrorKind {
  /// Application specific error code.
  kServerError(code: -32000, message: 'Application error'),

  /// Service connection disposed.
  ///
  /// This may indicate the connection was closed while a request was in-flight.
  kConnectionDisposed(code: -32010, message: 'Service connection disposed'),

  /// The JSON sent is not a valid Request object.
  kInvalidRequest(code: -32600, message: 'Invalid request object'),

  /// The method does not exist or is not available.
  kMethodNotFound(code: -32601, message: 'Method not found'),

  /// Invalid method parameter(s), such as a mismatched type.
  kInvalidParams(code: -32602, message: 'Invalid method parameters'),

  /// Internal JSON-RPC error.
  kInternalError(code: -32603, message: 'Internal JSON-RPC error'),

  /// The requested feature is disabled.
  kFeatureDisabled(code: 100, message: 'Feature is disabled'),

  /// The VM must be paused when performing this operation.
  kVmMustBePaused(code: 101, message: 'The VM must be paused'),

  /// Unable to add a breakpoint at the specified line or function.
  kCannotAddBreakpoint(code: 102,
    message: 'Unable to add breakpoint at specified line or function'),

  /// The stream has already been subscribed to.
  kStreamAlreadySubscribed(code: 103, message: 'Stream already subscribed'),

  /// The stream has not been subscribed to.
  kStreamNotSubscribed(code: 104, message: 'Stream not subscribed'),

  /// Isolate must first be runnable.
  kIsolateMustBeRunnable(code: 105, message: 'Isolate must be runnable'),

  /// Isolate must first be paused.
  kIsolateMustBePaused(code: 106, message: 'Isolate must be paused'),

  /// The isolate could not be resumed.
  kIsolateCannotBeResumed(code: 107, message: 'The isolate could not be resumed'),

  /// The isolate is currently reloading.
  kIsolateIsReloading(code: 108, message: 'The isolate is currently reloading'),

  /// The isolate could not be reloaded due to an unhandled exception.
  kIsolateCannotReload(code: 109, message: 'The isolate could not be reloaded'),

  /// The isolate reload resulted in no changes being applied.
  kIsolateNoReloadChangesApplied(code: 110, message: 'No reload changes applied'),

  /// The service has already been registered.
  kServiceAlreadyRegistered(code: 111, message: 'Service already registered'),

  /// The service no longer exists.
  kServiceDisappeared(code: 112, message: 'Service has disappeared'),

  /// There was an error in the expression compiler.
  kExpressionCompilationError(
      code: 113, message: 'Expression compilation error'),

  /// The timeline related request could not be completed due to the current configuration.
  kInvalidTimelineRequest(code: 114,
      message: 'Invalid timeline request for the current timeline configuration'),

  /// Information about the microtasks queued in the specified isolate cannot be
  /// retrieved.
  kCannotGetQueuedMicrotasks(code: 115, message: 'Cannot get queued microtasks'),

  /// The custom stream does not exist.
  kCustomStreamDoesNotExist(code: 130, message: 'Custom stream does not exist'),

  /// The core stream is not allowed.
  kCoreStreamNotAllowed(code: 131, message: 'Core streams are not allowed');

  const RPCErrorKind({required this.code, required this.message});

  final int code;

  final String message;

  static final _codeToErrorMap =
      RPCErrorKind.values.fold(<int, RPCErrorKind>{}, (map, error) {
    map[error.code] = error;
    return map;
  });

  static RPCErrorKind? fromCode(int code) {
    return _codeToErrorMap[code];
  }
}

class RPCError implements Exception {
  static RPCError parse(String callingMethod, dynamic json) {
    return RPCError(callingMethod, json['code'], json['message'], json['data']);
  }

  final String? callingMethod;
  final int code;
  final String message;
  final Map? data;

  RPCError(this.callingMethod, this.code, [message, this.data])
      : message =
            message ?? RPCErrorKind.fromCode(code)?.message ?? 'Unknown error';

  RPCError.withDetails(this.callingMethod, this.code, this.message,
      {Object? details})
      : data = details == null ? null : <String, dynamic>{} {
    if (details != null) {
      data!['details'] = details;
    }
  }

  String? get details => data == null ? null : data!['details'];

  /// Return a map representation of this error suitable for conversion to
  /// json.
  Map<String, dynamic> toMap() => <String, Object?>{
      'code': code,
      'message': message,
      if (data != null) 'data': data,
    };

  @override
  String toString() {
    if (details == null) {
      return '$callingMethod: ($code) $message';
    } else {
      return '$callingMethod: ($code) $message\n$details';
    }
  }
}

/// Thrown when an RPC response is a [Sentinel].
class SentinelException implements Exception {
  final String callingMethod;
  final Sentinel sentinel;

  SentinelException.parse(this.callingMethod, Map<String, dynamic> data) :
    sentinel = Sentinel.parse(data)!;

  @override
  String toString() => '$sentinel from $callingMethod()';
}

/// An `ExtensionData` is an arbitrary map that can have any contents.
class ExtensionData {
  static ExtensionData? parse(Map<String, dynamic>? json) =>
      json == null ? null : ExtensionData._fromJson(json);

  final Map<String, dynamic> data;

  ExtensionData() : data = <String, dynamic>{};

  ExtensionData._fromJson(this.data);

  @override
  String toString() => '[ExtensionData $data]';
}

/// A logging handler you can pass to a [VmService] instance in order to get
/// notifications of non-fatal service protocol warnings and errors.
abstract class Log {
  /// Log a warning level message.
  void warning(String message);

  /// Log an error level message.
  void severe(String message);
}

class _NullLog implements Log {
  @override
  void warning(String message) {}
  @override
  void severe(String message) {}
}
''';

  @override
  void generate(DartGenerator gen) {
    gen.out(_clientHeaderCode);
    gen.writeln("const String vmServiceVersion = '$serviceVersion';");
    gen.writeln();
    gen.writeln('''
/// @optional
const String optional = 'optional';

/// Decode a string in Base64 encoding into the equivalent non-encoded string.
/// This is useful for handling the results of the Stdout or Stderr events.
String decodeBase64(String str) => utf8.decode(base64.decode(str));

// Returns true if a response is the Dart `null` instance.
bool _isNullInstance(Map json) => ((json['type'] == '@Instance') &&
                                  (json['kind'] == 'Null'));

Object? createServiceObject(dynamic json, List<String> expectedTypes) {
  if (json == null) return null;

  if (json is List) {
    return json.map((e) => createServiceObject(e, expectedTypes)).toList();
  } else if (json is Map<String, dynamic>) {
    String? type = json['type'];

    // Not a Response type.
    if (type == null) {
      // If there's only one expected type, we'll just use that type.
      if (expectedTypes.length == 1) {
        type = expectedTypes.first;
      } else {
        return Response.parse(json);
      }
    } else if (_isNullInstance(json) && (!expectedTypes.contains('InstanceRef'))) {
      // Replace null instances with null when we don't expect an instance to
      // be returned.
      return null;
    }
    final typeFactory = _typeFactories[type];
    if (typeFactory == null) {
      return null;
    } else {
      return typeFactory(json);
    }
  } else {
    // Handle simple types.
    return json;
  }
}

dynamic _createSpecificObject(
    dynamic json, dynamic Function(Map<String, dynamic> map) creator) {
  if (json == null) return null;

  if (json is List) {
    return json.map((e) => creator(e)).toList();
  } else if (json is Map) {
    return creator({
      for (String key in json.keys)
        key: json[key],
    });
  } else {
    // Handle simple types.
    return json;
  }
}

Future<T> extensionCallHelper<T>(VmService service, String method, Map<String, dynamic> args) {
  return service._call(method, args);
}

typedef ServiceCallback = Future<Map<String, dynamic>> Function(
    Map<String, dynamic> params);

void addTypeFactory(String name, Function factory) {
  if (_typeFactories.containsKey(name)) {
    throw StateError('Factory already registered for \$name');
  }
  _typeFactories[name] = factory;
}

''');
    gen.writeln();
    gen.writeln('final _typeFactories = <String, Function>{');
    for (var type in types) {
      gen.writeln("'${type!.rawName}': ${type.name}.parse,");
    }
    gen.writeln('};');
    gen.writeln();

    gen.writeln('final _methodReturnTypes = <String, List<String>>{');
    for (var method in methods) {
      String returnTypes = typeRefListToString(method.returnType.types);
      gen.writeln("'${method.name}' : $returnTypes,");
    }
    gen.writeln('};');
    gen.writeln();
    gen.write('''
class _OutstandingRequest<T> {
  _OutstandingRequest(this.method);
  static int _idCounter = 0;
  final id = '\${_idCounter++}';
  final String method;
  final _stackTrace = StackTrace.current;
  final _completer = Completer<T>();

  Future<T> get future => _completer.future;

  void complete(T value) => _completer.complete(value);
  void completeError(Object error) =>
      _completer.completeError(error, _stackTrace);
}
''');
    gen.writeln();
    gen.writeln('''
typedef VmServiceFactory<T extends VmService> = T Function({
  required Stream<dynamic> /*String|List<int>*/ inStream,
  required void Function(String message) writeMessage,
  Log? log,
  DisposeHandler? disposeHandler,
  Future? streamClosed,
  String? wsUri,
});
''');

    // The client side service implementation.
    gen.writeStatement('class VmService {');
    gen.writeStatement('late final StreamSubscription _streamSub;');
    gen.writeStatement('late final Function _writeMessage;');
    gen.writeStatement(
        'final _outstandingRequests = <String, _OutstandingRequest>{};');
    gen.writeStatement('final _services = <String, ServiceCallback>{};');
    gen.writeStatement('late final Log _log;');
    gen.write('''

  /// The web socket URI pointing to the target VM service instance.
  final String? wsUri;

  Stream<String> get onSend => _onSend.stream;
  final _onSend = StreamController<String>.broadcast(sync: true);

  Stream<String> get onReceive => _onReceive.stream;
  final _onReceive = StreamController<String>.broadcast(sync: true);

  Future<void> get onDone => _onDoneCompleter.future;
  final _onDoneCompleter = Completer<void>();

  bool _disposed = false;

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

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

  late final DisposeHandler? _disposeHandler;

  VmService(
    Stream<dynamic> /*String|List<int>*/ inStream,
    void Function(String message) writeMessage, {
    Log? log,
    DisposeHandler? disposeHandler,
    Future? streamClosed,
    this.wsUri,
  }) {
    _streamSub = inStream.listen(
      _processMessage,
      onDone: () async => await dispose(),
    );
    _writeMessage = writeMessage;
    _log = log ?? _NullLog();
    _disposeHandler = disposeHandler;
    streamClosed?.then((_) async => await dispose());
  }

  static VmService defaultFactory({
    required Stream<dynamic> /*String|List<int>*/ inStream,
    required void Function(String message) writeMessage,
    Log? log,
    DisposeHandler? disposeHandler,
    Future? streamClosed,
    String? wsUri,
  }) {
    return VmService(
      inStream,
      writeMessage,
      log: log,
      disposeHandler: disposeHandler,
      streamClosed: streamClosed,
      wsUri: wsUri,
    );
  }

  Stream<Event> onEvent(String streamId) => _getEventController(streamId).stream;
''');

    // streamCategories
    for (var s in streamCategories) {
      s.generate(gen);
    }

    gen.writeln();
    for (var m in methods) {
      m.generate(gen);
    }
    gen.out(_implCode);
    gen.writeStatement('}');
    gen.writeln();
    gen.out(_rpcError);
    gen.writeln();
    gen.writeln('// enums');
    for (var e in enums) {
      if (e.name == 'EventKind') {
        _generateEventStream(gen);
      }
      e.generate(gen);
    }
    gen.writeln();
    gen.writeln('// types');
    types.where((t) => !t!.skip).forEach((t) => t!.generate(gen));
  }

  void setDefaultValue(String typeName, String fieldName, String defaultValue) {
    types
        .firstWhere((t) => t!.name == typeName)!
        .fields
        .firstWhere((f) => f.name == fieldName)
        .defaultValue = defaultValue;
  }

  void _generateEventStream(DartGenerator gen) {
    gen.writeln();
    gen.writeDocs('An enum of available event streams.');
    gen.writeln('abstract class EventStreams {');
    gen.writeln();

    for (var c in streamCategories) {
      gen.writeln("static const String k${c.name} = '${c.name}';");
    }

    gen.writeln('}');
  }
}
