// 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 'base/file_system.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'globals.dart' as globals;
import 'resident_runner.dart';
import 'tracing.dart';
import 'vmservice.dart';

const String kFlutterTestOutputsDirEnvName = 'FLUTTER_TEST_OUTPUTS_DIR';
class ColdRunner extends ResidentRunner {
  ColdRunner(
    super.devices, {
    required super.target,
    required super.debuggingOptions,
    this.traceStartup = false,
    this.awaitFirstFrameWhenTracing = true,
    this.applicationBinary,
    this.multidexEnabled = false,
    bool super.ipv6 = false,
    super.stayResident,
    super.machine,
    super.devtoolsHandler,
  }) : super(
          hotMode: false,
        );

  final bool traceStartup;
  final bool awaitFirstFrameWhenTracing;
  final File? applicationBinary;
  final bool multidexEnabled;
  bool _didAttach = false;

  @override
  bool get canHotReload => false;

  @override
  Logger get logger => globals.logger;

  @override
  FileSystem get fileSystem => globals.fs;

  @override
  Future<int> run({
    Completer<DebugConnectionInfo>? connectionInfoCompleter,
    Completer<void>? appStartedCompleter,
    bool enableDevTools = false,
    String? route,
  }) async {
    try {
      for (final FlutterDevice? device in flutterDevices) {
        final int result = await device!.runCold(
          coldRunner: this,
          route: route,
        );
        if (result != 0) {
          appFailedToStart();
          return result;
        }
      }
    } on Exception catch (err, stack) {
      globals.printError('$err\n$stack');
      appFailedToStart();
      return 1;
    }

    // Connect to observatory.
    if (debuggingEnabled) {
      try {
        await connectToServiceProtocol(allowExistingDdsInstance: false);
      } on Exception catch (exception) {
        globals.printError(exception.toString());
        appFailedToStart();
        return 2;
      }
    }

    if (enableDevTools && debuggingEnabled) {
      // The method below is guaranteed never to return a failing future.
      unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
        devToolsServerAddress: debuggingOptions.devToolsServerAddress,
        flutterDevices: flutterDevices,
      ));
    }

    if (flutterDevices.first.observatoryUris != null) {
      // For now, only support one debugger connection.
      connectionInfoCompleter?.complete(DebugConnectionInfo(
        httpUri: flutterDevices.first.vmService!.httpAddress,
        wsUri: flutterDevices.first.vmService!.wsAddress,
      ));
    }

    globals.printTrace('Application running.');

    for (final FlutterDevice? device in flutterDevices) {
      if (device!.vmService == null) {
        continue;
      }
      await device.initLogReader();
      globals.printTrace('Connected to ${device.device!.name}');
    }

    if (traceStartup) {
      // Only trace startup for the first device.
      final FlutterDevice device = flutterDevices.first;
      if (device.vmService != null) {
        globals.printStatus('Tracing startup on ${device.device!.name}.');
        final String outputPath = globals.platform.environment[kFlutterTestOutputsDirEnvName] ?? getBuildDirectory();
        await downloadStartupTrace(
          device.vmService!,
          awaitFirstFrame: awaitFirstFrameWhenTracing,
          logger: globals.logger,
          output: globals.fs.directory(outputPath),
        );
      }
      appFinished();
    }

    appStartedCompleter?.complete();

    writeVmServiceFile();

    if (stayResident && !traceStartup) {
      return waitForAppToFinish();
    }
    await cleanupAtFinish();
    return 0;
  }

  @override
  Future<int> attach({
    Completer<DebugConnectionInfo>? connectionInfoCompleter,
    Completer<void>? appStartedCompleter,
    bool allowExistingDdsInstance = false,
    bool enableDevTools = false,
    bool needsFullRestart = true,
  }) async {
    _didAttach = true;
    try {
      await connectToServiceProtocol(
        getSkSLMethod: writeSkSL,
        allowExistingDdsInstance: allowExistingDdsInstance,
      );
    } on Exception catch (error) {
      globals.printError('Error connecting to the service protocol: $error');
      return 2;
    }

    for (final FlutterDevice? device in flutterDevices) {
      await device!.initLogReader();
    }
    for (final FlutterDevice? device in flutterDevices) {
      final List<FlutterView> views = await device!.vmService!.getFlutterViews();
      for (final FlutterView view in views) {
        globals.printTrace('Connected to $view.');
      }
    }

    if (enableDevTools && debuggingEnabled) {
      // The method below is guaranteed never to return a failing future.
      unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
        devToolsServerAddress: debuggingOptions.devToolsServerAddress,
        flutterDevices: flutterDevices,
      ));
    }

    appStartedCompleter?.complete();
    if (stayResident) {
      return waitForAppToFinish();
    }
    await cleanupAtFinish();
    return 0;
  }

  @override
  Future<void> cleanupAfterSignal() async {
    await stopEchoingDeviceLog();
    if (_didAttach) {
      appFinished();
    }
    await exitApp();
  }

  @override
  Future<void> cleanupAtFinish() async {
    for (final FlutterDevice? flutterDevice in flutterDevices) {
      await flutterDevice!.device!.dispose();
    }

    await residentDevtoolsHandler!.shutdown();
    await stopEchoingDeviceLog();
  }

  @override
  void printHelp({ required bool details }) {
    globals.printStatus('Flutter run key commands.');
    if (details) {
      printHelpDetails();
      commandHelp.hWithDetails.print();
    } else {
      commandHelp.hWithoutDetails.print();
    }
    if (_didAttach) {
      commandHelp.d.print();
    }
    commandHelp.c.print();
    commandHelp.q.print();
    printDebuggerList();
  }

  @override
  Future<void> preExit() async {
    for (final FlutterDevice? device in flutterDevices) {
      // If we're running in release mode, stop the app using the device logic.
      if (device!.vmService == null) {
        await device.device!.stopApp(device.package, userIdentifier: device.userIdentifier);
      }
    }
    await super.preExit();
  }
}