// Copyright (c) 2024, 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:isolate';

import 'package:devtools_shared/devtools_shared.dart' show DtdInfo;
import 'package:path/path.dart' as path;

import 'utils.dart';

String getDTDSnapshotDir() {
  // This logic is originally from pkg/dartdev/lib/src/sdk.dart
  //
  // Find SDK path.
  (String, bool)? trySDKPath(String executablePath) {
    // The common case, and how cli_util.dart computes the Dart SDK directory,
    // [path.dirname] called twice on Platform.executable. We confirm by
    // asserting that the directory `./bin/snapshots/` exists in this directory:
    var sdkPath = path.absolute(path.dirname(path.dirname(executablePath)));
    var snapshotsDir = path.join(sdkPath, 'bin', 'snapshots');
    var runFromBuildRoot = false;
    final type = FileSystemEntity.typeSync(snapshotsDir);
    if (type != FileSystemEntityType.directory &&
        type != FileSystemEntityType.link) {
      // This is the less common case where the user is in
      // the checked out Dart SDK, and is executing `dart` via:
      // ./out/ReleaseX64/dart ... or in google3.
      sdkPath = path.absolute(path.dirname(executablePath));
      snapshotsDir = sdkPath;
      runFromBuildRoot = true;
    }

    // Try to locate the DDS snapshot to determine if we're able to find
    // the SDK snapshots with this SDK path. This is meant to handle
    // non-standard SDK layouts that can involve symlinks (e.g., Brew
    // installations, google3 tests, etc).
    if (!File(
      path.join(snapshotsDir, 'dds_aot.dart.snapshot'),
    ).existsSync()) {
      // We do not have an AOT snpashot and hence look for the JIT snapshot.
      if (!File(
        path.join(snapshotsDir, 'dds.dart.snapshot'),
      ).existsSync()) {
        return null;
      }
    }
    return (sdkPath, runFromBuildRoot);
  }

  final (sdkPath, runFromBuildRoot) = trySDKPath(Platform.resolvedExecutable) ??
      trySDKPath(Platform.executable)!;

  final String snapshotDir;
  if (runFromBuildRoot) {
    snapshotDir = sdkPath;
  } else {
    snapshotDir = path.absolute(sdkPath, 'bin', 'snapshots');
  }

  return snapshotDir;
}

Future<DtdInfo?> startDtd({
  required bool machineMode,
  required bool printDtdUri,
}) async {
  final snapshotDir = getDTDSnapshotDir();
  final dtdAotSnapshot =  path.absolute(
    snapshotDir,
    'dart_tooling_daemon_aot.dart.snapshot',
  );
  final completer = Completer<DtdInfo?>();
  void completeForError() => completer.complete(null);

  final exitPort = ReceivePort()
    ..listen((_) {
      completeForError();
    });
  final errorPort = ReceivePort()
    ..listen((_) {
      completeForError();
    });
  final receivePort = ReceivePort()
    ..listen((message) {
      try {
        // [message] is a JSON encoded String from package:dtd_impl.
        final json = jsonDecode(message) as Map<String, Object?>;
        if (json
            case {
              'tooling_daemon_details': {
                'uri': String uri,
                'trusted_client_secret': String secret,
              }
            }) {
          if (printDtdUri || machineMode) {
            DevToolsUtils.printOutput(
              'Serving the Dart Tooling Daemon at $uri',
              {
                'event': 'server.dtdStarted',
                'params': {'uri': uri},
              },
              machineMode: machineMode,
            );
          }
          completer.complete(DtdInfo(Uri.parse(uri), secret: secret));
        }
      } catch (_) {
        completeForError();
      }
    });

  try {
    // Try to spawn an isolate using the AOT snapshot of the tooling daemon.
    await Isolate.spawnUri(
      Uri.file(dtdAotSnapshot),
      ['--machine'],
      receivePort.sendPort,
      onExit: exitPort.sendPort,
      onError: errorPort.sendPort,
    );
  } catch (_, __) {
    // Spawning an isolate using the AOT snapshot of the tooling daemon failed,
    // try again using the JIT snapshot of the tooling daemon.
    final dtdSnapshot =  path.absolute(
      snapshotDir,
      'dart_tooling_daemon.dart.snapshot',
    );
    try {
      await Isolate.spawnUri(
        Uri.file(dtdSnapshot),
        ['--machine'],
        receivePort.sendPort,
        onExit: exitPort.sendPort,
        onError: errorPort.sendPort,
      );
    } catch (_, __) {
      completeForError();
    }
  }

  final result = await completer.future.timeout(
    const Duration(seconds: 5),
    onTimeout: () => null,
  );
  receivePort.close();
  errorPort.close();
  exitPort.close();
  return result;
}
