// Copyright 2014 The Flutter 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 'package:meta/meta.dart' show visibleForTesting;
import 'package:vm_service/vm_service.dart' as vm_service;

import 'base/common.dart';
import 'base/context.dart';
import 'base/io.dart' as io;
import 'base/logger.dart';
import 'base/utils.dart';
import 'convert.dart';
import 'device.dart';
import 'version.dart';

const String kGetSkSLsMethod = '_flutter.getSkSLs';
const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath';
const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks';
const String kRunInViewMethod = '_flutter.runInView';
const String kListViewsMethod = '_flutter.listViews';
const String kScreenshotSkpMethod = '_flutter.screenshotSkp';
const String kScreenshotMethod = '_flutter.screenshot';
const String kRenderFrameWithRasterStatsMethod = '_flutter.renderFrameWithRasterStats';
const String kReloadAssetFonts = '_flutter.reloadAssetFonts';

/// The error response code from an unrecoverable compilation failure.
const int kIsolateReloadBarred = 1005;

/// Override `WebSocketConnector` in [context] to use a different constructor
/// for [WebSocket]s (used by tests).
typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression, required Logger logger});

typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event);

WebSocketConnector _openChannel = _defaultOpenChannel;

/// A testing only override of the WebSocket connector.
///
/// Provide a `null` value to restore the original connector.
@visibleForTesting
set openChannelForTesting(WebSocketConnector? connector) {
  _openChannel = connector ?? _defaultOpenChannel;
}

/// The error codes for the JSON-RPC standard, including VM service specific
/// error codes.
///
/// See also: https://www.jsonrpc.org/specification#error_object
abstract class RPCErrorCodes {
  /// The method does not exist or is not available.
  static const int kMethodNotFound = -32601;

  /// Invalid method parameter(s), such as a mismatched type.
  static const int kInvalidParams = -32602;

  /// Internal JSON-RPC error.
  static const int kInternalError = -32603;

  /// Application specific error codes.
  static const int kServerError = -32000;

  /// Non-standard JSON-RPC error codes:

  /// The VM service or extension service has disappeared.
  static const int kServiceDisappeared = 112;
}

/// A function that reacts to the invocation of the 'reloadSources' service.
///
/// The VM Service Protocol allows clients to register custom services that
/// can be invoked by other clients through the service protocol itself.
///
/// Clients like Observatory use external 'reloadSources' services,
/// when available, instead of the VM internal one. This allows these clients to
/// invoke Flutter HotReload when connected to a Flutter Application started in
/// hot mode.
///
/// See: https://github.com/dart-lang/sdk/issues/30023
typedef ReloadSources = Future<void> Function(
  String isolateId, {
  bool force,
  bool pause,
});

typedef Restart = Future<void> Function({ bool pause });

typedef CompileExpression = Future<String> Function(
  String isolateId,
  String expression,
  List<String> definitions,
  List<String> typeDefinitions,
  String libraryUri,
  String? klass,
  bool isStatic,
);

/// A method that pulls an SkSL shader from the device and writes it to a file.
///
/// The name of the file returned as a result.
typedef GetSkSLMethod = Future<String?> Function();

Future<io.WebSocket> _defaultOpenChannel(String url, {
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
  required Logger logger,
}) async {
  Duration delay = const Duration(milliseconds: 100);
  int attempts = 0;
  io.WebSocket? socket;

  Future<void> handleError(Object? e) async {
    void Function(String) printVisibleTrace = logger.printTrace;
    if (attempts == 10) {
      logger.printStatus('Connecting to the VM Service is taking longer than expected...');
    } else if (attempts == 20) {
      logger.printStatus('Still attempting to connect to the VM Service...');
      logger.printStatus(
        'If you do NOT see the Flutter application running, it might have '
        'crashed. The device logs (e.g. from adb or XCode) might have more '
        'details.');
      logger.printStatus(
        'If you do see the Flutter application running on the device, try '
        're-running with --host-vmservice-port to use a specific port known to '
        'be available.');
    } else if (attempts % 50 == 0) {
      printVisibleTrace = logger.printStatus;
    }

    printVisibleTrace('Exception attempting to connect to the VM Service: $e');
    printVisibleTrace('This was attempt #$attempts. Will retry in $delay.');

    // Delay next attempt.
    await Future<void>.delayed(delay);

    // Back off exponentially, up to 1600ms per attempt.
    if (delay < const Duration(seconds: 1)) {
      delay *= 2;
    }
  }

  final WebSocketConnector constructor = context.get<WebSocketConnector>() ?? (String url, {
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
    Logger? logger,
  }) => io.WebSocket.connect(url, compression: compression);

  while (socket == null) {
    attempts += 1;
    try {
      socket = await constructor(url, compression: compression, logger: logger);
    } on io.WebSocketException catch (e) {
      await handleError(e);
    } on io.SocketException catch (e) {
      await handleError(e);
    }
  }
  return socket;
}

/// Override `VMServiceConnector` in [context] to return a different VMService
/// from [VMService.connect] (used by tests).
typedef VMServiceConnector = Future<FlutterVmService> Function(Uri httpUri, {
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
  io.CompressionOptions compression,
  Device? device,
  required Logger logger,
});

/// Set up the VM Service client by attaching services for each of the provided
/// callbacks.
///
/// All parameters besides [vmService] may be null.
Future<vm_service.VmService> setUpVmService(
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  Device? device,
  GetSkSLMethod? skSLMethod,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
  vm_service.VmService vmService
) async {
  // Each service registration requires a request to the attached VM service. Since the
  // order of these requests does not matter, store each future in a list and await
  // all at the end of this method.
  final List<Future<vm_service.Success?>> registrationRequests = <Future<vm_service.Success?>>[];
  if (reloadSources != null) {
    vmService.registerServiceCallback('reloadSources', (Map<String, Object?> params) async {
      final String isolateId = _validateRpcStringParam('reloadSources', params, 'isolateId');
      final bool force = _validateRpcBoolParam('reloadSources', params, 'force');
      final bool pause = _validateRpcBoolParam('reloadSources', params, 'pause');

      await reloadSources(isolateId, force: force, pause: pause);

      return <String, Object>{
        'result': <String, Object>{
          'type': 'Success',
        },
      };
    });
    registrationRequests.add(vmService.registerService('reloadSources', 'Flutter Tools'));
  }

  if (restart != null) {
    vmService.registerServiceCallback('hotRestart', (Map<String, Object?> params) async {
      final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause');
      await restart(pause: pause);
      return <String, Object>{
        'result': <String, Object>{
          'type': 'Success',
        },
      };
    });
    registrationRequests.add(vmService.registerService('hotRestart', 'Flutter Tools'));
  }

  vmService.registerServiceCallback('flutterVersion', (Map<String, Object?> params) async {
    final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion();
    final Map<String, Object> versionJson = version.toJson();
    versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort;
    versionJson['engineRevisionShort'] = version.engineRevisionShort;
    return <String, Object>{
      'result': <String, Object>{
        'type': 'Success',
        ...versionJson,
      },
    };
  });
  registrationRequests.add(vmService.registerService('flutterVersion', 'Flutter Tools'));

  if (compileExpression != null) {
    vmService.registerServiceCallback('compileExpression', (Map<String, Object?> params) async {
      final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId');
      final String expression = _validateRpcStringParam('compileExpression', params, 'expression');
      final List<String> definitions = List<String>.from(params['definitions']! as List<Object?>);
      final List<String> typeDefinitions = List<String>.from(params['typeDefinitions']! as List<Object?>);
      final String libraryUri = params['libraryUri']! as String;
      final String? klass = params['klass'] as String?;
      final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic');

      final String kernelBytesBase64 = await compileExpression(isolateId,
          expression, definitions, typeDefinitions, libraryUri, klass,
          isStatic);
      return <String, Object>{
        'type': 'Success',
        'result': <String, String>{'kernelBytes': kernelBytesBase64},
      };
    });
    registrationRequests.add(vmService.registerService('compileExpression', 'Flutter Tools'));
  }
  if (device != null) {
    vmService.registerServiceCallback('flutterMemoryInfo', (Map<String, Object?> params) async {
      final MemoryInfo result = await device.queryMemoryInfo();
      return <String, Object>{
        'result': <String, Object>{
          'type': 'Success',
          ...result.toJson(),
        },
      };
    });
    registrationRequests.add(vmService.registerService('flutterMemoryInfo', 'Flutter Tools'));
  }
  if (skSLMethod != null) {
    vmService.registerServiceCallback('flutterGetSkSL', (Map<String, Object?> params) async {
      final String? filename = await skSLMethod();
      if (filename == null) {
        return <String, Object>{
          'result': <String, Object>{
            'type': 'Success',
          },
        };
      }
      return <String, Object>{
        'result': <String, Object>{
          'type': 'Success',
          'filename': filename,
        },
      };
    });
    registrationRequests.add(vmService.registerService('flutterGetSkSL', 'Flutter Tools'));
  }
  if (printStructuredErrorLogMethod != null) {
    vmService.onExtensionEvent.listen(printStructuredErrorLogMethod);
    // It is safe to ignore this error because we expect an error to be
    // thrown if we're already subscribed.
    registrationRequests.add(vmService
      .streamListen(vm_service.EventStreams.kExtension)
      .then<vm_service.Success?>((vm_service.Success success) => success)
      .catchError((Object? error) => null, test: (Object? error) => error is vm_service.RPCError)
    );
  }

  try {
    await Future.wait(registrationRequests);
  } on vm_service.RPCError catch (e) {
    throwToolExit('Failed to register service methods on attached VM Service: $e');
  }
  return vmService;
}

/// Connect to a Dart VM Service at [httpUri].
///
/// If the [reloadSources] parameter is not null, the 'reloadSources' service
/// will be registered. The VM Service Protocol allows clients to register
/// custom services that can be invoked by other clients through the service
/// protocol itself.
///
/// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217
Future<FlutterVmService> connectToVmService(
  Uri httpUri, {
    ReloadSources? reloadSources,
    Restart? restart,
    CompileExpression? compileExpression,
    GetSkSLMethod? getSkSLMethod,
    PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
    Device? device,
    required Logger logger,
  }) async {
  final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect;
  return connector(httpUri,
    reloadSources: reloadSources,
    restart: restart,
    compileExpression: compileExpression,
    compression: compression,
    device: device,
    getSkSLMethod: getSkSLMethod,
    printStructuredErrorLogMethod: printStructuredErrorLogMethod,
    logger: logger,
  );
}

Future<vm_service.VmService> createVmServiceDelegate(
  Uri wsUri, {
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
  required Logger logger,
}) async {
  final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger);
  return vm_service.VmService(
    channel,
    channel.add,
    disposeHandler: () async {
      await channel.close();
    },
  );
}

Future<FlutterVmService> _connect(
  Uri httpUri, {
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
  Device? device,
  required Logger logger,
}) async {
  final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws'));
  final vm_service.VmService delegateService = await createVmServiceDelegate(
    wsUri, compression: compression, logger: logger,
  );

  final vm_service.VmService service = await setUpVmService(
    reloadSources,
    restart,
    compileExpression,
    device,
    getSkSLMethod,
    printStructuredErrorLogMethod,
    delegateService,
  );

  // This call is to ensure we are able to establish a connection instead of
  // keeping on trucking and failing farther down the process.
  await delegateService.getVersion();
  return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri);
}

String _validateRpcStringParam(String methodName, Map<String, Object?> params, String paramName) {
  final Object? value = params[paramName];
  if (value is! String || value.isEmpty) {
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
  return value;
}

bool _validateRpcBoolParam(String methodName, Map<String, Object?> params, String paramName) {
  final Object? value = params[paramName];
  if (value != null && value is! bool) {
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
  return (value as bool?) ?? false;
}

/// Peered to an Android/iOS FlutterView widget on a device.
class FlutterView {
  FlutterView({
    required this.id,
    required this.uiIsolate,
  });

  factory FlutterView.parse(Map<String, Object?> json) {
    final Map<String, Object?>? rawIsolate = json['isolate'] as Map<String, Object?>?;
    vm_service.IsolateRef? isolate;
    if (rawIsolate != null) {
      rawIsolate['number'] = rawIsolate['number']?.toString();
      isolate = vm_service.IsolateRef.parse(rawIsolate);
    }
    return FlutterView(
      id: json['id']! as String,
      uiIsolate: isolate,
    );
  }

  final vm_service.IsolateRef? uiIsolate;
  final String id;

  bool get hasIsolate => uiIsolate != null;

  @override
  String toString() => id;

  Map<String, Object?> toJson() {
    return <String, Object?>{
      'id': id,
      'isolate': uiIsolate?.toJson(),
    };
  }
}

/// Flutter specific VM Service functionality.
class FlutterVmService {
  FlutterVmService(
    this.service, {
    this.wsAddress,
    this.httpAddress,
  });

  final vm_service.VmService service;
  final Uri? wsAddress;
  final Uri? httpAddress;

  Future<vm_service.Response?> callMethodWrapper(
    String method, {
    String? isolateId,
    Map<String, Object?>? args
  }) async {
    try {
      return await service.callMethod(method, isolateId: isolateId, args: args);
    } on vm_service.RPCError catch (e) {
      // If the service disappears mid-request the tool is unable to recover
      // and should begin to shutdown due to the service connection closing.
      // Swallow the exception here and let the shutdown logic elsewhere deal
      // with cleaning up.
      if (e.code == RPCErrorCodes.kServiceDisappeared) {
        return null;
      }
      rethrow;
    }
  }

  /// Set the asset directory for the an attached Flutter view.
  Future<void> setAssetDirectory({
    required Uri assetsDirectory,
    required String? viewId,
    required String? uiIsolateId,
    required bool windows,
  }) async {
    await callMethodWrapper(kSetAssetBundlePathMethod,
      isolateId: uiIsolateId,
      args: <String, Object?>{
        'viewId': viewId,
        'assetDirectory': assetsDirectory.toFilePath(windows: windows),
      });
  }

  /// Retrieve the cached SkSL shaders from an attached Flutter view.
  ///
  /// This method will only return data if `--cache-sksl` was provided as a
  /// flutter run argument, and only then on physical devices.
  Future<Map<String, Object?>?> getSkSLs({
    required String viewId,
  }) async {
    final vm_service.Response? response = await callMethodWrapper(
      kGetSkSLsMethod,
      args: <String, String>{
        'viewId': viewId,
      },
    );
    if (response == null) {
      return null;
    }
    return response.json?['SkSLs'] as Map<String, Object?>?;
  }

  /// Flush all tasks on the UI thread for an attached Flutter view.
  ///
  /// This method is currently used only for benchmarking.
  Future<void> flushUIThreadTasks({
    required String uiIsolateId,
  }) async {
    await callMethodWrapper(
      kFlushUIThreadTasksMethod,
      args: <String, String>{
        'isolateId': uiIsolateId,
      },
    );
  }

  /// Launch the Dart isolate with entrypoint [main] in the Flutter engine [viewId]
  /// with [assetsDirectory] as the devFS.
  ///
  /// This method is used by the tool to hot restart an already running Flutter
  /// engine.
  Future<void> runInView({
    required String viewId,
    required Uri main,
    required Uri assetsDirectory,
  }) async {
    try {
      await service.streamListen(vm_service.EventStreams.kIsolate);
    } on vm_service.RPCError {
      // Do nothing, since the tool is already subscribed.
    }
    final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) {
      return event.kind == vm_service.EventKind.kIsolateRunnable;
    });
    await callMethodWrapper(
      kRunInViewMethod,
      args: <String, Object>{
        'viewId': viewId,
        'mainScript': main.toString(),
        'assetDirectory': assetsDirectory.toString(),
      },
    );
    await onRunnable;
  }

  /// Renders the last frame with additional raster tracing enabled.
  ///
  /// When a frame is rendered using this method it will incur additional cost
  /// for rasterization which is not reflective of how long the frame takes in
  /// production. This is primarily intended to be used to identify the layers
  /// that result in the most raster perf degradation.
  Future<Map<String, Object?>?> renderFrameWithRasterStats({
    required String? viewId,
    required String? uiIsolateId,
  }) async {
    final vm_service.Response? response = await callMethodWrapper(
      kRenderFrameWithRasterStatsMethod,
      isolateId: uiIsolateId,
      args: <String, String?>{
        'viewId': viewId,
      },
    );
    return response?.json;
  }

  Future<String> flutterDebugDumpApp({
    required String isolateId,
  }) async {
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugDumpApp',
      isolateId: isolateId,
    );
    return response?['data']?.toString() ?? '';
  }

  Future<String> flutterDebugDumpRenderTree({
    required String isolateId,
  }) async {
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugDumpRenderTree',
      isolateId: isolateId,
      args: <String, Object>{}
    );
    return response?['data']?.toString() ?? '';
  }

  Future<String> flutterDebugDumpLayerTree({
    required String isolateId,
  }) async {
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugDumpLayerTree',
      isolateId: isolateId,
    );
    return response?['data']?.toString() ?? '';
  }

  Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({
    required String isolateId,
  }) async {
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugDumpSemanticsTreeInTraversalOrder',
      isolateId: isolateId,
    );
    return response?['data']?.toString() ?? '';
  }

  Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({
    required String isolateId,
  }) async {
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder',
      isolateId: isolateId,
    );
    if (response != null) {
      return response['data']?.toString() ?? '';
    }
    return '';
  }

  Future<Map<String, Object?>?> _flutterToggle(String name, {
    required String isolateId,
  }) async {
    Map<String, Object?>? state = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.$name',
      isolateId: isolateId,
    );
    if (state != null && state.containsKey('enabled') && state['enabled'] is String) {
      state = await invokeFlutterExtensionRpcRaw(
        'ext.flutter.$name',
        isolateId: isolateId,
        args: <String, Object>{
          'enabled': state['enabled'] == 'true' ? 'false' : 'true',
        },
      );
    }

    return state;
  }

  Future<Map<String, Object?>?> flutterToggleDebugPaintSizeEnabled({
    required String isolateId,
  }) => _flutterToggle('debugPaint', isolateId: isolateId);

  Future<Map<String, Object?>?> flutterTogglePerformanceOverlayOverride({
    required String isolateId,
  }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId);

  Future<Map<String, Object?>?> flutterToggleWidgetInspector({
    required String isolateId,
  }) => _flutterToggle('inspector.show', isolateId: isolateId);

  Future<Map<String, Object?>?> flutterToggleInvertOversizedImages({
    required String isolateId,
  }) => _flutterToggle('invertOversizedImages', isolateId: isolateId);

  Future<Map<String, Object?>?> flutterToggleProfileWidgetBuilds({
    required String isolateId,
  }) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId);

  Future<Map<String, Object?>?> flutterDebugAllowBanner(bool show, {
    required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugAllowBanner',
      isolateId: isolateId,
      args: <String, Object>{'enabled': show ? 'true' : 'false'},
    );
  }

  Future<Map<String, Object?>?> flutterReassemble({
    required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.reassemble',
      isolateId: isolateId,
    );
  }

  Future<Map<String, Object?>?> flutterFastReassemble({
   required String isolateId,
   required String className,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.fastReassemble',
      isolateId: isolateId,
      args: <String, Object>{
        'className': className,
      },
    );
  }

  Future<bool> flutterAlreadyPaintedFirstUsefulFrame({
    required String isolateId,
  }) async {
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.didSendFirstFrameRasterizedEvent',
      isolateId: isolateId,
    );
    // result might be null when the service extension is not initialized
    return result?['enabled'] == 'true';
  }

  Future<Map<String, Object?>?> uiWindowScheduleFrame({
    required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.scheduleFrame',
      isolateId: isolateId,
    );
  }

  Future<Map<String, Object?>?> flutterEvictAsset(String assetPath, {
   required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.evict',
      isolateId: isolateId,
      args: <String, Object?>{
        'value': assetPath,
      },
    );
  }

  Future<Map<String, Object?>?> flutterEvictShader(String assetPath, {
   required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.reinitializeShader',
      isolateId: isolateId,
      args: <String, Object?>{
        'assetKey': assetPath,
      },
    );
  }

  Future<Map<String, Object?>?> flutterEvictScene(String assetPath, {
   required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.reinitializeScene',
      isolateId: isolateId,
      args: <String, Object?>{
        'assetKey': assetPath,
      },
    );
  }


  /// Exit the application by calling [exit] from `dart:io`.
  ///
  /// This method is only supported by certain embedders. This is
  /// described by [Device.supportsFlutterExit].
  Future<bool> flutterExit({
    required String isolateId,
  }) async {
    try {
      final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
        'ext.flutter.exit',
        isolateId: isolateId,
      );
      // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError
      // with a missing method code. This can happen when attempting to quit a Flutter app
      // that never registered the methods in the bindings.
      if (result == null) {
        return false;
      }
    } on vm_service.SentinelException {
      // Do nothing on sentinel, the isolate already exited.
    } on vm_service.RPCError {
      // Do nothing on RPCError, the isolate already exited.
    }
    return true;
  }

  /// Return the current platform override for the flutter view running with
  /// the main isolate [isolateId].
  ///
  /// If a non-null value is provided for [platform], the platform override
  /// is updated with this value.
  Future<String> flutterPlatformOverride({
    String? platform,
    required String isolateId,
  }) async {
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.platformOverride',
      isolateId: isolateId,
      args: platform != null
        ? <String, Object>{'value': platform}
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
      return result['value']! as String;
    }
    return 'unknown';
  }

  /// Return the current brightness value for the flutter view running with
  /// the main isolate [isolateId].
  ///
  /// If a non-null value is provided for [brightness], the brightness override
  /// is updated with this value.
  Future<Brightness?> flutterBrightnessOverride({
    Brightness? brightness,
    required String isolateId,
  }) async {
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.brightnessOverride',
      isolateId: isolateId,
      args: brightness != null
        ? <String, String>{'value': brightness.toString()}
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
      return result['value'] == 'Brightness.light'
        ? Brightness.light
        : Brightness.dark;
    }
    return null;
  }

  Future<vm_service.Response?> _checkedCallServiceExtension(
    String method, {
    Map<String, Object?>? args,
  }) async {
    try {
      return await service.callServiceExtension(method, args: args);
    } on vm_service.RPCError catch (err) {
      // If an application is not using the framework or the VM service
      // disappears while handling a request, return null.
      if ((err.code == RPCErrorCodes.kMethodNotFound)
          || (err.code == RPCErrorCodes.kServiceDisappeared)) {
        return null;
      }
      rethrow;
    }
  }

  /// Invoke a flutter extension method, if the flutter extension is not
  /// available, returns null.
  Future<Map<String, Object?>?> invokeFlutterExtensionRpcRaw(
    String method, {
    required String isolateId,
    Map<String, Object?>? args,
  }) async {
    final vm_service.Response? response = await _checkedCallServiceExtension(
      method,
      args: <String, Object?>{
        'isolateId': isolateId,
        ...?args,
      },
    );
    return response?.json;
  }

  /// List all [FlutterView]s attached to the current VM.
  ///
  /// If this returns an empty list, it will poll forever unless [returnEarly]
  /// is set to true.
  ///
  /// By default, the poll duration is 50 milliseconds.
  Future<List<FlutterView>> getFlutterViews({
    bool returnEarly = false,
    Duration delay = const Duration(milliseconds: 50),
  }) async {
    while (true) {
      final vm_service.Response? response = await callMethodWrapper(
        kListViewsMethod,
      );
      if (response == null) {
        // The service may have disappeared mid-request.
        // Return an empty list now, and let the shutdown logic elsewhere deal
        // with cleaning up.
        return <FlutterView>[];
      }
      final List<Object?>? rawViews = response.json?['views'] as List<Object?>?;
      final List<FlutterView> views = <FlutterView>[
        if (rawViews != null)
          for (final Map<String, Object?> rawView in rawViews.whereType<Map<String, Object?>>())
            FlutterView.parse(rawView),
      ];
      if (views.isNotEmpty || returnEarly) {
        return views;
      }
      await Future<void>.delayed(delay);
    }
  }

  /// Tell the provided flutter view that the font manifest has been updated
  /// and asset fonts should be reloaded.
  Future<void> reloadAssetFonts({
    required String isolateId,
    required String viewId,
  }) async {
    await callMethodWrapper(
      kReloadAssetFonts,
      isolateId: isolateId, args: <String, Object?>{
        'viewId': viewId,
      },
    );
  }

  /// Waits for a signal from the VM service that [extensionName] is registered.
  ///
  /// Looks at the list of loaded extensions for first Flutter view, as well as
  /// the stream of added extensions to avoid races.
  ///
  /// If [webIsolate] is true, this uses the VM Service isolate list instead of
  /// the `_flutter.listViews` method, which is not implemented by DWDS.
  ///
  /// Throws a [VmServiceDisappearedException] should the VM Service disappear
  /// while making calls to it.
  Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async {
    try {
      await service.streamListen(vm_service.EventStreams.kIsolate);
    } on vm_service.RPCError {
      // Do nothing, since the tool is already subscribed.
    }

    final Completer<vm_service.IsolateRef> extensionAdded = Completer<vm_service.IsolateRef>();
    late final StreamSubscription<vm_service.Event> isolateEvents;
    isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) {
      if (event.kind == vm_service.EventKind.kServiceExtensionAdded
          && event.extensionRPC == extensionName) {
        isolateEvents.cancel();
        extensionAdded.complete(event.isolate);
      }
    });

    try {
      final List<vm_service.IsolateRef> refs = await _getIsolateRefs();
      for (final vm_service.IsolateRef ref in refs) {
        final vm_service.Isolate? isolate = await getIsolateOrNull(ref.id!);
        if (isolate != null && (isolate.extensionRPCs?.contains(extensionName) ?? false)) {
          return ref;
        }
      }
      return await extensionAdded.future;
    } finally {
      await isolateEvents.cancel();
      try {
        await service.streamCancel(vm_service.EventStreams.kIsolate);
      } on vm_service.RPCError {
        // It's ok for cleanup to fail, such as when the service disappears.
      }
    }
  }

  Future<List<vm_service.IsolateRef>> _getIsolateRefs() async {
    final List<FlutterView> flutterViews = await getFlutterViews();
    if (flutterViews.isEmpty) {
      throw VmServiceDisappearedException();
    }

    final List<vm_service.IsolateRef> refs = <vm_service.IsolateRef>[];
    for (final FlutterView flutterView in flutterViews) {
      final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate;
      if (uiIsolate != null) {
        refs.add(uiIsolate);
      }
    }
    return refs;
  }

  /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has
  /// been collected.
  Future<vm_service.Isolate?> getIsolateOrNull(String isolateId) async {
    return service.getIsolate(isolateId)
      // The .then() call is required to cast from Future<Isolate> to Future<Isolate?>
      .then<vm_service.Isolate?>((vm_service.Isolate isolate) => isolate)
      .catchError((Object? error, StackTrace stackTrace) {
        return null;
      }, test: (Object? error) {
        return (error is vm_service.SentinelException) ||
          (error is vm_service.RPCError && error.code == RPCErrorCodes.kServiceDisappeared);
      });
  }

  /// Create a new development file system on the device.
  Future<vm_service.Response> createDevFS(String fsName) {
    // Call the unchecked version of `callServiceExtension` because the caller
    // has custom handling of certain RPCErrors.
    return service.callServiceExtension(
      '_createDevFS',
      args: <String, Object?>{'fsName': fsName},
    );
  }

  /// Delete an existing file system.
  Future<void> deleteDevFS(String fsName) async {
    await _checkedCallServiceExtension(
      '_deleteDevFS',
      args: <String, Object?>{'fsName': fsName},
    );
  }

  Future<vm_service.Response?> screenshot() {
    return _checkedCallServiceExtension(kScreenshotMethod);
  }

  Future<vm_service.Response?> screenshotSkp() {
    return _checkedCallServiceExtension(kScreenshotSkpMethod);
  }

  /// Set the VM timeline flags.
  Future<void> setTimelineFlags(List<String> recordedStreams) async {
    await _checkedCallServiceExtension(
      'setVMTimelineFlags',
      args: <String, Object?>{
        'recordedStreams': recordedStreams,
      },
    );
  }

  Future<vm_service.Response?> getTimeline() {
    return _checkedCallServiceExtension('getVMTimeline');
  }

  Future<void> dispose() async {
     await service.dispose();
  }
}

/// Thrown when the VM Service disappears while calls are being made to it.
class VmServiceDisappearedException implements Exception { }

/// Whether the event attached to an [Isolate.pauseEvent] should be considered
/// a "pause" event.
bool isPauseEvent(String kind) {
  return kind == vm_service.EventKind.kPauseStart ||
         kind == vm_service.EventKind.kPauseExit ||
         kind == vm_service.EventKind.kPauseBreakpoint ||
         kind == vm_service.EventKind.kPauseInterrupted ||
         kind == vm_service.EventKind.kPauseException ||
         kind == vm_service.EventKind.kPausePostRequest ||
         kind == vm_service.EventKind.kNone;
}

/// A brightness enum that matches the values https://github.com/flutter/engine/blob/3a96741247528133c0201ab88500c0c3c036e64e/lib/ui/window.dart#L1328
/// Describes the contrast of a theme or color palette.
enum Brightness {
  /// The color is dark and will require a light text color to achieve readable
  /// contrast.
  ///
  /// For example, the color might be dark grey, requiring white text.
  dark,

  /// The color is light and will require a dark text color to achieve readable
  /// contrast.
  ///
  /// For example, the color might be bright white, requiring black text.
  light,
}

/// Process a VM service log event into a string message.
String processVmServiceMessage(vm_service.Event event) {
  final String message = utf8.decode(base64.decode(event.bytes!));
  // Remove extra trailing newlines appended by the vm service.
  if (message.endsWith('\n')) {
    return message.substring(0, message.length - 1);
  }
  return message;
}
