// Copyright 2019 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 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

import 'application_package.dart';
import 'asset.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/terminal.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'bundle.dart';
import 'dart/package_map.dart';
import 'device.dart';
import 'globals.dart';
import 'project.dart';
import 'resident_runner.dart';
import 'run_hot.dart';
import 'web/asset_server.dart';
import 'web/chrome.dart';
import 'web/compile.dart';

/// A hot-runner which handles browser specific delegation.
class ResidentWebRunner extends ResidentRunner {
  ResidentWebRunner(
    List<FlutterDevice> flutterDevices, {
    String target,
    @required this.flutterProject,
    @required bool ipv6,
    @required DebuggingOptions debuggingOptions,
  }) : super(
          flutterDevices,
          target: target,
          usesTerminalUI: true,
          stayResident: true,
          debuggingOptions: debuggingOptions,
          ipv6: ipv6,
        );

  WebAssetServer _server;
  ProjectFileInvalidator projectFileInvalidator;
  DateTime _lastCompiled;
  WipConnection _connection;
  final FlutterProject flutterProject;

  @override
  Future<int> attach(
      {Completer<DebugConnectionInfo> connectionInfoCompleter,
      Completer<void> appStartedCompleter}) async {
    connectionInfoCompleter?.complete(DebugConnectionInfo());
    setupTerminal();
    final int result = await waitForAppToFinish();
    await cleanupAtFinish();
    return result;
  }

  @override
  Future<void> cleanupAfterSignal() async {
    await _connection.sendCommand('Browser.close');
    _connection = null;
    await _server?.dispose();
  }

  @override
  Future<void> cleanupAtFinish() async {
    await _connection?.sendCommand('Browser.close');
    _connection = null;
    await _server?.dispose();
  }

  @override
  Future<void> handleTerminalCommand(String code) async {
    if (code == 'R') {
      // If hot restart is not supported for all devices, ignore the command.
      if (!canHotRestart) {
        return;
      }
      await restart(fullRestart: true);
    }
  }

  @override
  void printHelp({bool details}) {
    const String fire = '🔥';
    const String rawMessage =
        '  To hot restart (and rebuild state), press "R".';
    final String message = terminal.color(
      fire + terminal.bolden(rawMessage),
      TerminalColor.red,
    );
    const String warning = '👻 ';
    printStatus(warning * 20);
    printStatus('Warning: Flutter\'s support for building web applications is highly experimental.');
    printStatus('For more information see https://github.com/flutter/flutter/issues/34082.');
    printStatus(warning * 20);
    printStatus('');
    printStatus(message);
    const String quitMessage = 'To quit, press "q".';
    printStatus('For a more detailed help message, press "h". $quitMessage');
  }

  @override
  Future<int> run({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
    String route,
    bool shouldBuild = true,
  }) async {
    final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform(
      TargetPlatform.web_javascript,
      applicationBinary: null,
    );
    if (package == null) {
      printError('No application found for TargetPlatform.web_javascript');
      return 1;
    }
    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;
    }
    // Start the web compiler and build the assets.
    await webCompilationProxy.initialize(
      projectDirectory: flutterProject.directory,
    );
    _lastCompiled = DateTime.now();
    final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
    final int build = await assetBundle.build();
    if (build != 0) {
      throwToolExit('Error: Failed to build asset bundle');
    }
    await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);

    // Step 2: Start an HTTP server
    _server = WebAssetServer(flutterProject, target, ipv6);
    await _server.initialize();

    // Step 3: Spawn an instance of Chrome and direct it to the created server.
    final String url = 'http://localhost:${_server.port}';
    final Chrome chrome = await chromeLauncher.launch(url);
    final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
      return chromeTab.url.contains(url); // we don't care about trailing slashes or #
    });
    _connection = await chromeTab.connect();
    _connection.onClose.listen((WipConnection connection) {
      exit();
    });

    // We don't support the debugging proxy yet.
    appStartedCompleter?.complete();
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
      appStartedCompleter: appStartedCompleter,
    );
  }

  @override
  Future<OperationResult> restart({
    bool fullRestart = false,
    bool pauseAfterRestart = false,
    String reason,
    bool benchmarkMode = false,
  }) async {
    final Stopwatch timer = Stopwatch()..start();
    final Status status = logger.startProgress(
      'Performing hot restart...',
      timeout: timeoutConfiguration.fastOperation,
      progressId: 'hot.restart',
    );
    OperationResult result = OperationResult.ok;
    try {
      final List<Uri> invalidatedSources = ProjectFileInvalidator.findInvalidated(
        lastCompiled: _lastCompiled,
        urisToMonitor: <Uri>[
          for (FileSystemEntity entity in flutterProject.directory
              .childDirectory('lib')
              .listSync(recursive: true))
            if (entity is File && entity.path.endsWith('.dart')) entity.uri
        ], // Add new class to track this for web.
        packagesPath: PackageMap.globalPackagesPath,
      );
      await webCompilationProxy.invalidate(inputs: invalidatedSources);
      await _connection.sendCommand('Page.reload');
      await Future<void>.delayed(const Duration(milliseconds: 150));
    } catch (err) {
      result = OperationResult(1, err.toString());
    } finally {
      printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
      status.cancel();
    }
    return result;
  }
}