// Copyright (c) 2025, 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 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'io_utils.dart';
import 'legacy_messages.dart';
import 'lsp_messages.dart';
import 'utils.dart';

abstract class DartLanguageServerBenchmark {
  int verbosity = 0;
  final Uri repoDir = computeRepoDirUri();
  late final Process p;
  late final Timer longRunningRequestsTimer;
  bool _launched = false;
  bool _exited = false;
  Completer<bool> _analyzingCompleter = Completer();
  final _buffer = _Uint8ListHelper();
  int? _headerContentLength;
  bool _printedVmServiceStuff = false;
  final String executableToUse;
  bool latestIsAnalyzing = false;

  /// There's something weird about getting (several) id 3's that wasn't
  /// requested...
  int largestIdSeen = 3;

  final RegExp _newLineRegExp = RegExp('\r?\n');
  final Map<int, OutstandingRequest> _outstandingRequestsWithId = {};

  bool _addedInitialAnalysisDuration = false;
  List<DurationInfo> durationInfo = [];
  List<MemoryInfo> memoryInfo = [];

  final bool _lsp;

  new(List<String> args, {required bool useLspProtocol})
    : _lsp = useLspProtocol,
      executableToUse = extractDartParamOrDefault(args) {
    _checkCorrectDart();
  }

  List<Uri> get additionalWorkspaceUris => const [];
  Uri? get cacheFolder => null;

  LaunchFrom get launchFrom => LaunchFrom.source;

  Uri get rootUri;
  Future<void> afterInitialization();

  void exit() {
    _exited = true;
    longRunningRequestsTimer.cancel();
    if (Platform.isLinux) {
      try {
        File f = File('/proc/${p.pid}/status');
        for (var line in f.readAsLinesSync()) {
          if (line.startsWith('VmPeak:')) {
            memoryInfo.addInfoMaybe(
              'peak virtual memory size',
              _extractSizeFromProcStatusMemoryLine(line),
            );
          } else if (line.startsWith('VmSize:')) {
            memoryInfo.addInfoMaybe(
              'total program size (virtual)',
              _extractSizeFromProcStatusMemoryLine(line),
            );
          } else if (line.startsWith('VmHWM:')) {
            memoryInfo.addInfoMaybe(
              'peak resident set size ("high water mark")',
              _extractSizeFromProcStatusMemoryLine(line),
            );
          } else if (line.startsWith('VmRSS:')) {
            memoryInfo.addInfoMaybe(
              'size of memory portions (rss)',
              _extractSizeFromProcStatusMemoryLine(line),
            );
          }
        }
      } catch (e) {
        // ignore
      }
    }
    p.kill();
  }

  Future<void> run() async {
    if (_lsp) {
      await _runLsp();
    } else {
      await _runLegacy();
    }
  }

  Future<OutstandingRequest?> send(Map<String, dynamic> json) async {
    var result = sendNoFlush(json);
    await p.stdin.flush();
    return result;
  }

  OutstandingRequest? sendNoFlush(Map<String, dynamic> json) {
    // Mostly copied from
    // pkg/analysis_server/lib/src/lsp/channel/lsp_byte_stream_channel.dart
    var jsonEncodedBody = jsonEncode(json);
    var utf8EncodedBody = utf8.encode(jsonEncodedBody);
    var header =
        'Content-Length: ${utf8EncodedBody.length}\r\n'
        'Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n';
    var asciiEncodedHeader = ascii.encode(header);

    OutstandingRequest? result;

    dynamic possibleId = json['id'];
    if (!_lsp && possibleId is String && int.tryParse(possibleId) != null) {
      possibleId = int.tryParse(possibleId);
    }
    if (possibleId is int) {
      if (possibleId > largestIdSeen) {
        largestIdSeen = possibleId;
      }
      result = OutstandingRequest();
      _outstandingRequestsWithId[possibleId] = result;
      if (verbosity > 2) {
        print('Sending message with id $possibleId');
      }
    }
    if (verbosity > 3) {
      print('Sending message: $jsonEncodedBody');
    }

    if (_lsp) {
      // Header is always ascii, body is always utf8!
      p.stdin.add(asciiEncodedHeader);
    }
    p.stdin.add(utf8EncodedBody);
    if (!_lsp) {
      p.stdin.add(const [10]);
    }
    if (verbosity > 2) {
      print('\n\nMessage sent\n\n');
    }
    return result;
  }

  Future<bool> waitUntilIsAnalyzingChanges() async {
    return await _analyzingCompleter.future;
  }

  Future<void> waitWhileAnalyzing() async {
    // Wait until it's done analyzing.
    bool isAnalyzing = await _analyzingCompleter.future;
    Stopwatch stopwatch = Stopwatch()..start();
    while (isAnalyzing) {
      isAnalyzing = await _analyzingCompleter.future;
    }
    if (verbosity >= 0) {
      print('isAnalyzing is now done after ${stopwatch.elapsed}');
    }
    if (!_addedInitialAnalysisDuration) {
      _addedInitialAnalysisDuration = true;
      durationInfo.add(DurationInfo('Initial analysis', stopwatch.elapsed));
    }
  }

  void _checkCorrectDart() {
    Uri exe = Uri.base.resolveUri(Uri.file(executableToUse));
    Uri librariesDart = exe.resolve(
      '../lib/_internal/sdk_library_metadata/lib/libraries.dart',
    );
    if (!File.fromUri(librariesDart).existsSync()) {
      throw 'Execute with a dart that has '
          "'../lib/_internal/sdk_library_metadata/lib/libraries.dart' "
          'available (e.g. out/ReleaseX64/dart-sdk/bin/dart)';
    }
  }

  void _checkLongRunningRequests(Timer timer) {
    if (verbosity < 0) return;
    bool reportedSomething = false;
    for (MapEntry<int, OutstandingRequest> waitingFor
        in _outstandingRequestsWithId.entries) {
      if (waitingFor.value.stopwatch.elapsed > const Duration(seconds: 1)) {
        if (!reportedSomething) {
          print('----');
          reportedSomething = true;
        }
        print(
          '==> Has been waiting for required #${waitingFor.key} for '
          '${waitingFor.value.stopwatch.elapsed}',
        );
      }
    }
    if (reportedSomething) {
      print('----');
    } else {
      // print(" -- not waiting for anything -- ");
    }
  }

  /// Copied from pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart.
  bool _endsWithCrLfCrLf() {
    var l = _buffer.length;
    return l > 4 &&
        _buffer[l - 1] == 10 &&
        _buffer[l - 2] == 13 &&
        _buffer[l - 3] == 10 &&
        _buffer[l - 4] == 13;
  }

  bool _endsWithLf() {
    var l = _buffer.length;
    return l > 1 && _buffer[l - 1] == 10;
  }

  int? _extractSizeFromProcStatusMemoryLine(String line) {
    int colon = line.indexOf(':');
    if (colon < 0) return null;
    int kb = line.indexOf('kB', colon);
    if (kb < 0) return null;
    String substring = line.substring(colon + 1, kb).trim();
    return int.tryParse(substring);
  }

  Future<void> _initializeLegacy() async {
    await send(LegacyMessages.getVersion(largestIdSeen + 1));
    await send(LegacyMessages.setSubscriptionsStatus(largestIdSeen + 1));
    await send(LegacyMessages.updateOptions(largestIdSeen + 1));
    await send(LegacyMessages.setClientCapabilities(largestIdSeen + 1));
    await send(
      LegacyMessages.setAnalysisRoots(largestIdSeen + 1, [
        rootUri,
        ...additionalWorkspaceUris,
      ]),
    );

    // Wait until it's done analyzing.
    await waitWhileAnalyzing();
  }

  Future<void> _initializeLsp() async {
    OutstandingRequest? request = await send(
      LspMessages.initMessage(pid, rootUri, additionalWorkspaceUris),
    );
    await request?.completer.future;
    await send(LspMessages.initNotification);

    // Wait until it's done analyzing.
    await waitWhileAnalyzing();
  }

  Future<void> _launch({required bool lsp}) async {
    if (_launched) throw 'Already launched';
    _launched = true;

    List<String> cacheFolderArgs = [];
    if (cacheFolder != null) {
      cacheFolderArgs.add('--cache');
      cacheFolderArgs.add(cacheFolder!.toFilePath());
    }

    switch (launchFrom) {
      case LaunchFrom.source:
        File serverFile = File.fromUri(
          repoDir.resolve('pkg/analysis_server/bin/server.dart'),
        );
        if (!serverFile.existsSync()) {
          throw "Couldn't find 'analysis_server/bin/server.dart' "
              'expected it at $serverFile';
        }

        // TODO(jensj): Option of passing --profiler
        p = await Process.start(executableToUse, [
          '--enable-vm-service',
          '--profiler',
          serverFile.path,
          lsp ? '--protocol=lsp' : '--protocol=analyzer',
          '--port=9102',
          ...cacheFolderArgs,
        ]);
      case LaunchFrom.dart:
        // TODO(jensj): Option of wrapping in `perf record -g` call.
        p = await Process.start(executableToUse, [
          'language-server',
          lsp ? '--protocol=lsp' : '--protocol=analyzer',
          '--port=9102',
          ...cacheFolderArgs,
        ]);
      case LaunchFrom.aotWithPerf:
      case LaunchFrom.aot:
        File serverFile = File.fromUri(
          repoDir.resolve('pkg/analysis_server/bin/server.aot'),
        );
        if (!serverFile.existsSync()) {
          throw "Couldn't find 'analysis_server/bin/server.aot' "
              'expected it at $serverFile';
        }

        // TODO(jensj): Option of passing --profiler
        Uri dart = Uri.base.resolveUri(Uri.file(executableToUse));
        File aotRuntime = File.fromUri(dart.resolve('dartaotruntime'));
        if (!aotRuntime.existsSync()) {
          throw "Couldn't find 'dartaotruntime' expected it at $aotRuntime";
        }

        p = await Process.start(aotRuntime.path, [
          serverFile.path,
          lsp ? '--protocol=lsp' : '--protocol=analyzer',
          '--port=9102',
          ...cacheFolderArgs,
        ]);

        if (launchFrom == LaunchFrom.aotWithPerf) {
          await Process.start('perf', [
            'record',
            '-p',
            '${p.pid}',
            '-g',
          ], mode: ProcessStartMode.inheritStdio);
        }
    }

    // ignore: unawaited_futures
    p.exitCode.then((exitCode) {
      if (verbosity >= 0) {
        print('Process existed with exitCode $exitCode');
      }
      if (!_exited) {
        _analyzingCompleter.completeError('Process exited 1.');
        while (_outstandingRequestsWithId.isNotEmpty) {
          var entry = _outstandingRequestsWithId.entries.first;
          _outstandingRequestsWithId.remove(entry.key);
          entry.value.completer.completeError('Process exited 2.');
        }
      }
    });

    if (verbosity >= 0) {
      print('Launched with pid ${p.pid}');
    }
  }

  void _listenToStdout(List<int> event) {
    // General idea taken from
    // pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart
    for (int element in event) {
      _buffer.add(element);
      if (verbosity > 3 &&
          _buffer.length >= 1000 &&
          _buffer.length % 1000 == 0) {
        print(
          'DEBUG MESSAGE: Stdout buffer with length ${_buffer.length} so far: '
          '${utf8.decode(_buffer.view())}',
        );
      }
      if (_lsp && _headerContentLength == null && _endsWithCrLfCrLf()) {
        String headerRaw = utf8.decode(_buffer.view());
        _buffer.clear();
        // Use a regex that makes the '\r' optional to handle "The Dart VM service
        // is listening on [..." message - at least on linux - being \n terminated
        // which would otherwise mean that we'd be stuck because no message would
        // start with 'Content-Length:'.
        List<String> headers = headerRaw.split(_newLineRegExp);
        for (String header in headers) {
          if (!_printedVmServiceStuff &&
              header.startsWith('The Dart VM service')) {
            if (verbosity >= 0) {
              print('\n\n$header\n\n');
            }
            _printedVmServiceStuff = true;
          }
          if (header.startsWith('Content-Length:')) {
            String contentLength = header
                .substring('Content-Length:'.length)
                .trim();
            _headerContentLength = int.parse(contentLength);
            break;
          }
        }
      } else if ((_lsp &&
              _headerContentLength != null &&
              _buffer.length == _headerContentLength!) ||
          (!_lsp && _endsWithLf())) {
        String messageString = utf8.decode(_buffer.view());
        _buffer.clear();
        _headerContentLength = null;

        if (messageString.startsWith('The Dart VM service')) {
          if (verbosity >= 0) print('\n\n$messageString\n\n');
          continue;
        }
        if (messageString.startsWith('The Dart DevTools')) {
          if (verbosity >= 0) print('\n\n$messageString\n\n');
          continue;
        }

        Map<String, dynamic> message =
            json.decode(messageString) as Map<String, dynamic>;

        // LSP: {"jsonrpc":"2.0","method":"$/analyzerStatus","params":{"isAnalyzing":false}}
        // Analyzer: {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
        if ((_lsp && message['method'] == r'$/analyzerStatus') ||
            (!_lsp && message['event'] == 'server.status')) {
          dynamic params = message['params'];
          if (params is Map) {
            dynamic isAnalyzing = _lsp
                ? params['isAnalyzing']
                : params['analysis']?['isAnalyzing'];
            if (isAnalyzing is bool) {
              latestIsAnalyzing = isAnalyzing;
              _analyzingCompleter.complete(isAnalyzing);
              _analyzingCompleter = Completer<bool>();
              if (verbosity > 0) {
                print('Got analyzerStatus isAnalyzing = $isAnalyzing');
              }
            }
          }
        }
        dynamic possibleId = message['id'];
        if (!_lsp && possibleId is String && int.tryParse(possibleId) != null) {
          possibleId = int.tryParse(possibleId);
        }
        if (possibleId is int) {
          if (possibleId > largestIdSeen) {
            largestIdSeen = possibleId;
          }

          if (verbosity > 0) {
            if (messageString.length > 100) {
              print('Got message ${messageString.substring(0, 100)}...');
            } else {
              print('Got message $messageString');
            }
          }

          OutstandingRequest? outstandingRequest = _outstandingRequestsWithId
              .remove(possibleId);
          if (outstandingRequest != null) {
            outstandingRequest.stopwatch.stop();
            outstandingRequest.completer.complete(message);
            if (verbosity > 2) {
              print(
                ' => Got response for $possibleId in '
                '${outstandingRequest.stopwatch.elapsed}',
              );
            }
          }
        } else if (verbosity > 1) {
          if (messageString.length > 100) {
            print('Got message ${messageString.substring(0, 100)}...');
          } else {
            print('Got message $messageString');
          }
        }
      }
    }
  }

  Future<void> _runLegacy() async {
    assert(!_lsp);
    await _launch(lsp: false);
    p.stdout.listen(_listenToStdout);
    p.stderr.listen(stderr.add);
    longRunningRequestsTimer = Timer.periodic(
      const Duration(seconds: 1),
      _checkLongRunningRequests,
    );

    await _initializeLegacy();
    if (verbosity >= 0) {
      print('Should now be initialized.');
    }

    await afterInitialization();
  }

  Future<void> _runLsp() async {
    assert(_lsp);
    await _launch(lsp: true);
    p.stdout.listen(_listenToStdout);
    p.stderr.listen(stderr.add);
    longRunningRequestsTimer = Timer.periodic(
      const Duration(seconds: 1),
      _checkLongRunningRequests,
    );

    await _initializeLsp();
    if (verbosity >= 0) {
      print('Should now be initialized.');
    }

    await afterInitialization();
  }
}

class DurationInfo {
  final String name;
  final Duration duration;

  new(this.name, this.duration);
}

enum LaunchFrom { source, dart, aot, aotWithPerf }

class MemoryInfo {
  final String name;
  final int kb;

  new(this.name, this.kb);
}

class OutstandingRequest {
  final Stopwatch stopwatch = Stopwatch();
  final Completer<Map<String, dynamic>> completer = Completer();
  new() {
    stopwatch.start();
  }
}

class _Uint8ListHelper {
  Uint8List data;
  int length = 0;
  new() : data = Uint8List(1024);

  int operator [](int index) {
    if (index < 0 || index >= length) throw 'Out of bounds: $index';
    return data[index];
  }

  void add(int i) {
    if (length == data.length) {
      var newData = Uint8List(length * 2);
      newData.setRange(0, length, data);
      data = newData;
    }
    data[length++] = i;
  }

  void clear() {
    length = 0;
  }

  Uint8List view() {
    return Uint8List.sublistView(data, 0, length);
  }
}

extension on List<MemoryInfo> {
  void addInfoMaybe(String description, int? value) {
    if (value == null) return;
    add(MemoryInfo(description, value));
  }
}
