// 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.

/// Note: the VM concatenates all patch files into a single patch file. This
/// file is the first patch in "dart:developer" which contains all the imports
/// used by patches of that library. We plan to change this when we have a
/// shared front end and simply use parts.

import "dart:_internal" show patch;

import "dart:async" show Future, Zone;

import "dart:io" show Platform;

import "dart:isolate" show SendPort;

/// These are the additional parts of this patch library:
part "profiler.dart";
part "timeline.dart";

@patch
@pragma("vm:recognized", "other")
@pragma("vm:external-name", "Developer_debugger")
external bool debugger({bool when = true, String? message});

@patch
@pragma("vm:external-name", "Developer_inspect")
external Object? inspect(Object? object);

@patch
void log(
  String message, {
  DateTime? time,
  int? sequenceNumber,
  int level = 0,
  String name = '',
  Zone? zone,
  Object? error,
  StackTrace? stackTrace,
}) {
  if (message is! String) {
    throw ArgumentError.value(message, "message", "Must be a String");
  }
  time ??= DateTime.now();
  if (time is! DateTime) {
    throw ArgumentError.value(time, "time", "Must be a DateTime");
  }
  if (sequenceNumber == null) {
    sequenceNumber = _nextSequenceNumber++;
  } else {
    _nextSequenceNumber = sequenceNumber + 1;
  }
  _log(
    message,
    time.millisecondsSinceEpoch,
    sequenceNumber,
    level,
    name,
    zone,
    error,
    stackTrace,
  );
}

@patch
@pragma("vm:external-name", "Developer_reachability_barrier")
external int get reachabilityBarrier;

int _nextSequenceNumber = 0;

@pragma("vm:external-name", "Developer_log")
external _log(
  String message,
  int timestamp,
  int sequenceNumber,
  int level,
  String name,
  Zone? zone,
  Object? error,
  StackTrace? stackTrace,
);

@patch
@pragma("vm:external-name", "Developer_postEvent")
external void _postEvent(String eventKind, String eventData);

@patch
@pragma("vm:external-name", "Developer_lookupExtension")
external ServiceExtensionHandler? _lookupExtension(String method);

@patch
@pragma("vm:external-name", "Developer_registerExtension")
external _registerExtension(String method, ServiceExtensionHandler handler);

// This code is only invoked when there is no other Dart code on the stack.
@pragma("vm:entry-point", !bool.fromEnvironment("dart.vm.product"))
_runExtension(
  ServiceExtensionHandler handler,
  String method,
  List<String> parameterKeys,
  List<String> parameterValues,
  SendPort replyPort,
  Object id,
  bool trace_service,
) {
  var parameters = <String, String>{};
  for (var i = 0; i < parameterKeys.length; i++) {
    parameters[parameterKeys[i]] = parameterValues[i];
  }
  var response;
  try {
    response = handler(method, parameters);
  } catch (e, st) {
    var errorDetails = (st == null) ? '$e' : '$e\n$st';
    response = ServiceExtensionResponse.error(
      ServiceExtensionResponse.extensionError,
      errorDetails,
    );
    _postResponse(replyPort, id, response, trace_service);
    return;
  }
  if (response is! Future) {
    response = ServiceExtensionResponse.error(
      ServiceExtensionResponse.extensionError,
      "Extension handler must return a Future",
    );
    _postResponse(replyPort, id, response, trace_service);
    return;
  }
  response
      .catchError((e, st) {
        // Catch any errors eagerly and wrap them in a ServiceExtensionResponse.
        var errorDetails = (st == null) ? '$e' : '$e\n$st';
        return ServiceExtensionResponse.error(
          ServiceExtensionResponse.extensionError,
          errorDetails,
        );
      })
      .then((response) {
        // Post the valid response or the wrapped error after verifying that
        // the response is a ServiceExtensionResponse.
        if (response is! ServiceExtensionResponse) {
          response = ServiceExtensionResponse.error(
            ServiceExtensionResponse.extensionError,
            "Extension handler must complete to a ServiceExtensionResponse",
          );
        }
        _postResponse(replyPort, id, response, trace_service);
      })
      .catchError((e, st) {
        // We do not expect any errors to occur in the .then or .catchError blocks
        // but, suppress them just in case.
      });
}

// This code is only invoked by _runExtension.
_postResponse(
  SendPort replyPort,
  Object id,
  ServiceExtensionResponse response,
  bool trace_service,
) {
  assert(replyPort != null);
  if (id == null) {
    if (trace_service) {
      print("vm-service: posting no response for request");
    }
    // No id -> no response.
    replyPort.send(null);
    return;
  }
  assert(id != null);
  StringBuffer sb = StringBuffer();
  sb.write('{"jsonrpc":"2.0",');
  if (response.isError()) {
    if (trace_service) {
      print("vm-service: posting error response for request $id");
    }
    sb.write('"error":');
  } else {
    if (trace_service) {
      print("vm-service: posting response for request $id");
    }
    sb.write('"result":');
  }
  sb.write('${response._toString()},');
  if (id is String) {
    sb.write('"id":"$id"}');
  } else {
    sb.write('"id":$id}');
  }
  replyPort.send(sb.toString());
}

@patch
@pragma("vm:external-name", "Developer_getServiceMajorVersion")
external int _getServiceMajorVersion();

@patch
@pragma("vm:external-name", "Developer_getServiceMinorVersion")
external int _getServiceMinorVersion();

@patch
@pragma("vm:external-name", "Developer_getServerInfo")
external void _getServerInfo(SendPort sendPort);

@patch
@pragma("vm:external-name", "Developer_webServerControl")
external void _webServerControl(
  SendPort sendPort,
  bool enable,
  bool? silenceOutput,
);

@patch
@pragma("vm:external-name", "Developer_getIsolateIdFromSendPort")
external String? _getIsolateIdFromSendPort(SendPort sendPort);

@patch
@pragma("vm:external-name", "Developer_getObjectId")
external String? _getObjectId(Object object);

@patch
abstract final class NativeRuntime {
  @patch
  @pragma("vm:external-name", "Developer_NativeRuntime_buildId")
  external static String? get buildId;

  @patch
  @pragma("vm:external-name", "Developer_NativeRuntime_writeHeapSnapshotToFile")
  external static void writeHeapSnapshotToFile(String filepath);

  @patch
  static void streamTimelineTo(
    TimelineRecorder recorder, {
    String? path,
    List<TimelineStream> streams = const [
      TimelineStream.dart,
      TimelineStream.gc,
    ],
    bool enableProfiler = false,
    Duration samplingInterval = const Duration(microseconds: 1000),
  }) {
    if (samplingInterval.inMicroseconds < 50) {
      throw ArgumentError.value(
        samplingInterval,
        'samplingInterval',
        'should be at least 50 us',
      );
    }

    if (recorder == TimelineRecorder.systrace) {
      if (path != null) {
        throw ArgumentError.value(
          path,
          'path',
          '$recorder writes output to the global ftrace buffer and '
              'can not redirect output to a specific file',
        );
      }

      if (Platform.isWindows) {
        throw ArgumentError.value(
          recorder,
          'recorder',
          '$recorder not supported on Windows',
        );
      }
    }

    if (recorder != TimelineRecorder.systrace && path == null) {
      throw ArgumentError.value(
        path,
        'path',
        '$recorder needs an output file to write timeline data to',
      );
    }

    if (recorder != TimelineRecorder.perfetto && enableProfiler) {
      throw ArgumentError.value(
        enableProfiler,
        'enableProfiler',
        '$recorder does not support encoding profiling data, '
            'disable profiler or switch to TimelineRecorder.perfetto',
      );
    }

    final recorderName = switch (recorder) {
      TimelineRecorder.perfetto => 'perfettofile',
      TimelineRecorder.chrome => 'file',
      TimelineRecorder.systrace => 'systrace',
    };

    // Convert list of TimelineStream into a comma-separated list of values.
    final streamsString = [
      for (var str in streams)
        switch (str) {
          TimelineStream.api => 'API',
          TimelineStream.compiler => 'Compiler',
          TimelineStream.compilerVerbose => 'CompilerVerbose',
          TimelineStream.dart => 'Dart',
          TimelineStream.debugger => 'Debugger',
          TimelineStream.embedder => 'Embedder',
          TimelineStream.gc => 'GC',
          TimelineStream.isolate => 'Isolate',
          TimelineStream.microtask => 'Microtask',
          TimelineStream.vm => 'vm',
        },
    ].join(',');

    _streamTimelineToImpl(
      recorderName,
      path,
      streamsString,
      enableProfiler,
      samplingInterval.inMicroseconds,
    );
  }

  @pragma("vm:external-name", "Developer_NativeRuntime_streamTimelineTo")
  external static void _streamTimelineToImpl(
    String recorder,
    String? path,
    String streams,
    bool enableProfiler,
    int samplingInterval,
  );

  @patch
  @pragma("vm:external-name", "Developer_NativeRuntime_stopStreamingTimeline")
  external static void stopStreamingTimeline();
}
