// Copyright 2016 The Chromium 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';

import 'base/file_system.dart';
import 'device.dart';
import 'globals.dart';
import 'resident_runner.dart';
import 'tracing.dart';
import 'vmservice.dart';

// TODO(mklim): Test this, flutter/flutter#23031.
class ColdRunner extends ResidentRunner {
  ColdRunner(
    List<FlutterDevice> devices, {
    String target,
    DebuggingOptions debuggingOptions,
    this.traceStartup = false,
    this.awaitFirstFrameWhenTracing = true,
    this.applicationBinary,
    bool ipv6 = false,
    bool stayResident = true,
  }) : super(devices,
             target: target,
             debuggingOptions: debuggingOptions,
             hotMode: false,
             stayResident: stayResident,
             ipv6: ipv6);

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

  @override
  bool get canHotReload => false;

  @override
  bool get canHotRestart => false;

  @override
  Future<int> run({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
    String route,
  }) async {
    final bool prebuiltMode = applicationBinary != null;
    if (!prebuiltMode) {
      if (!fs.isFileSync(mainPath)) {
        String message = 'Tried to run $mainPath, but that file does not exist.';
        if (target == null) {
          message += '\nConsider using the -t option to specify the Dart file to start.';
        }
        printError(message);
        return 1;
      }
    }

    for (FlutterDevice device in flutterDevices) {
      final int result = await device.runCold(
        coldRunner: this,
        route: route,
      );
      if (result != 0) {
        return result;
      }
    }

    // Connect to observatory.
    if (debuggingOptions.debuggingEnabled) {
      try {
        await connectToServiceProtocol();
      } on String catch (message) {
        printError(message);
        return 2;
      }
    }

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

    printTrace('Application running.');

    for (FlutterDevice device in flutterDevices) {
      if (device.vmServices == null) {
        continue;
      }
      device.initLogReader();
      await device.refreshViews();
      printTrace('Connected to ${device.device.name}');
    }

    if (traceStartup) {
      // Only trace startup for the first device.
      final FlutterDevice device = flutterDevices.first;
      if (device.vmServices != null && device.vmServices.isNotEmpty) {
        printStatus('Tracing startup on ${device.device.name}.');
        await downloadStartupTrace(
          device.vmServices.first,
          awaitFirstFrame: awaitFirstFrameWhenTracing,
        );
      }
      appFinished();
    }

    appStartedCompleter?.complete();

    writeVmserviceFile();

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

  @override
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
  }) async {
    _didAttach = true;
    try {
      await connectToServiceProtocol();
    } catch (error) {
      printError('Error connecting to the service protocol: $error');
      // https://github.com/flutter/flutter/issues/33050
      // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
        printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.');
        printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318');
      }
      return 2;
    }
    for (FlutterDevice device in flutterDevices) {
      device.initLogReader();
    }
    await refreshViews();
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        printTrace('Connected to $view.');
      }
    }
    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 (FlutterDevice flutterDevice in flutterDevices) {
      flutterDevice.device.dispose();
    }

    await stopEchoingDeviceLog();
  }

  @override
  void printHelp({ @required bool details }) {
    bool haveDetails = false;
    bool haveAnything = false;
    for (FlutterDevice device in flutterDevices) {
      final String dname = device.device.name;
      if (device.observatoryUris != null) {
        for (Uri uri in device.observatoryUris) {
          printStatus('An Observatory debugger and profiler on $dname is available at $uri');
          haveAnything = true;
        }
      }
    }
    if (supportsServiceProtocol) {
      haveDetails = true;
      if (details) {
        printHelpDetails();
        haveAnything = true;
      }
    }
    final String quitMessage = _didAttach
      ? 'To detach, press "d"; to quit, press "q".'
      : 'To quit, press "q".';
    if (haveDetails && !details) {
      printStatus('For a more detailed help message, press "h". $quitMessage');
    } else if (haveAnything) {
      printStatus('To repeat this help message, press "h". $quitMessage');
    } else {
      printStatus(quitMessage);
    }
  }

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