// 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 'dart:io';

import 'package:path/path.dart' as path;

import 'application_package.dart';
import 'asset.dart';
import 'base/logger.dart';
import 'base/utils.dart';
import 'cache.dart';
import 'commands/build_apk.dart';
import 'commands/install.dart';
import 'device.dart';
import 'globals.dart';
import 'devfs.dart';
import 'observatory.dart';
import 'resident_runner.dart';

String getDevFSLoaderScript() {
  return path.absolute(path.join(Cache.flutterRoot,
                                 'packages',
                                 'flutter',
                                 'bin',
                                 'loader',
                                 'loader_app.dart'));
}

class HotRunner extends ResidentRunner {
  HotRunner(
    Device device, {
    String target,
    DebuggingOptions debuggingOptions,
    bool usesTerminalUI: true
  }) : super(device,
             target: target,
             debuggingOptions: debuggingOptions,
             usesTerminalUI: usesTerminalUI) {
    _projectRootPath = Directory.current.path;
  }

  ApplicationPackage _package;
  String _mainPath;
  String _projectRootPath;
  final AssetBundle bundle = new AssetBundle();

  @override
  Future<int> run({ Completer<int> observatoryPortCompleter, String route }) {
    // Don't let uncaught errors kill the process.
    return runZoned(() {
      return _run(
        observatoryPortCompleter: observatoryPortCompleter,
        route: route
      );
    }, onError: (dynamic error, StackTrace stackTrace) {
      printError('Exception from flutter run: $error', stackTrace);
    });
  }

  Future<int> _run({
    Completer<int> observatoryPortCompleter,
    String route
  }) async {
    _mainPath = findMainDartFile(target);
    if (!FileSystemEntity.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;
    }

    _package = getApplicationPackageForPlatform(device.platform);

    if (_package == null) {
      String message = 'No application found for ${device.platform}.';
      String hint = getMissingPackageHintForPlatform(device.platform);
      if (hint != null)
        message += '\n$hint';
      printError(message);
      return 1;
    }

    // TODO(devoncarew): We shouldn't have to do type checks here.
    if (device is AndroidDevice) {
      printTrace('Running build command.');

      int result = await buildApk(
        device.platform,
        target: target,
        buildMode: debuggingOptions.buildMode
      );

      if (result != 0)
        return result;
    }

    // TODO(devoncarew): Move this into the device.startApp() impls.
    if (_package != null) {
      printTrace("Stopping app '${_package.name}' on ${device.name}.");
      // We don't wait for the stop command to complete.
      device.stopApp(_package);
    }

    // Allow any stop commands from above to start work.
    await new Future<Duration>.delayed(Duration.ZERO);

    // TODO(devoncarew): This fails for ios devices - we haven't built yet.
    if (device is AndroidDevice) {
      printTrace('Running install command.');
      if (!(installApp(device, _package, uninstall: false)))
        return 1;
    }

    Map<String, dynamic> platformArgs = new Map<String, dynamic>();

    await startEchoingDeviceLog();

    if (device.needsDevFS) {
      printStatus('Launching loader on ${device.name}...');
    } else {
      printStatus('Launching ${getDisplayPath(_mainPath)} on ${device.name}...');
    }

    LaunchResult result = await device.startApp(
      _package,
      debuggingOptions.buildMode,
      mainPath: device.needsDevFS ? getDevFSLoaderScript() : _mainPath,
      debuggingOptions: debuggingOptions,
      platformArgs: platformArgs,
      route: route
    );

    if (!result.started) {
      if (device.needsDevFS) {
        printError('Error launching DevFS loader on ${device.name}.');
      } else {
        printError('Error launching ${getDisplayPath(_mainPath)} on ${device.name}.');
      }
      await stopEchoingDeviceLog();
      return 2;
    }

    if (observatoryPortCompleter != null && result.hasObservatory)
      observatoryPortCompleter.complete(result.observatoryPort);

    await connectToServiceProtocol(result.observatoryPort);

    if (device.needsDevFS) {
      _loaderShowMessage('Connecting...', progress: 0);
      bool result = await _updateDevFS(
        progressReporter: (int progress, int max) {
          _loaderShowMessage('Syncing files to device...', progress: progress, max: max);
        }
      );
      if (!result) {
        _loaderShowMessage('Failed.');
        printError('Could not perform initial file synchronization.');
        return 3;
      }
      printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
      _loaderShowMessage('Launching...');
      await _launchFromDevFS(_package, _mainPath);
    }

    printStatus('Application running.');

    setupTerminal();

    registerSignalHandlers();

    return await waitForAppToFinish();
  }

  @override
  void handleTerminalCommand(String code) {
    final String lower = code.toLowerCase();
    if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
      // F5, restart
      if (code == 'r') {
        // lower-case 'r'
        _reloadSources();
      } else {
        // upper-case 'R'.
        _restartFromSources();
      }
    }
  }

  void _loaderShowMessage(String message, { int progress, int max }) {
    serviceProtocol.flutterLoaderShowMessage(serviceProtocol.firstIsolateId, message);
    if (progress != null) {
      serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, progress.toDouble());
      serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, max?.toDouble() ?? 0.0);
    } else {
      serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, 0.0);
      serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, -1.0);
    }
  }

  DevFS _devFS;
  Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async {
    if (_devFS == null) {
      String fsName = path.basename(_projectRootPath);
      _devFS = new DevFS(serviceProtocol,
                         fsName,
                         new Directory(_projectRootPath));

      try {
        await _devFS.create();
      } catch (error) {
        _devFS = null;
        printError('Error initializing DevFS: $error');
        return false;
      }
    }

    final bool rebuildBundle = bundle.needsBuild();
    if (rebuildBundle) {
      Status bundleStatus = logger.startProgress('Updating assets...');
      await bundle.build();
      bundleStatus.stop(showElapsedTime: true);
    }
    Status devFSStatus = logger.startProgress('Syncing files on device...');
    await _devFS.update(progressReporter: progressReporter,
                        bundle: bundle,
                        bundleDirty: rebuildBundle);
    devFSStatus.stop(showElapsedTime: true);
    printStatus('Synced ${getSizeAsMB(_devFS.bytes)} MB');
    return true;
  }

  Future<Null> _cleanupDevFS() async {
    if (_devFS != null) {
      // Cleanup the devFS.
      await _devFS.destroy();
    }
    _devFS = null;
  }

  Future<Null> _launchInView(String entryPath,
                             String packagesPath,
                             String assetsDirectoryPath) async {
    String viewId = await serviceProtocol.getFirstViewId();
    // When this completer completes the isolate is running.
    // TODO(johnmccutchan): Have the framework send an event after the first
    // frame is rendered and use that instead of 'runnable'.
    Completer<Null> completer = new Completer<Null>();
    StreamSubscription<Event> subscription =
       serviceProtocol.onIsolateEvent.listen((Event event) {
     if (event.kind == 'IsolateStart') {
       printTrace('Isolate is spawned.');
     } else if (event.kind == 'IsolateRunnable') {
       printTrace('Isolate is runnable.');
       completer.complete(null);
     }
    });
    await serviceProtocol.runInView(viewId,
                                   entryPath,
                                   packagesPath,
                                   assetsDirectoryPath);
    await completer.future;
    await subscription.cancel();
  }

  Future<Null> _launchFromDevFS(ApplicationPackage package,
                                String mainScript) async {
    String entryPath = path.relative(mainScript, from: _projectRootPath);
    String deviceEntryPath =
        _devFS.baseUri.resolve(entryPath).toFilePath();
    String devicePackagesPath =
        _devFS.baseUri.resolve('.packages').toFilePath();
    String deviceAssetsDirectoryPath =
        _devFS.baseUri.resolve('build/flx').toFilePath();
    await _launchInView(deviceEntryPath,
                        devicePackagesPath,
                        deviceAssetsDirectoryPath);
  }

  Future<Null> _launchFromDisk(ApplicationPackage package,
                               String mainScript) async {
    Uri baseUri = new Uri.directory(_projectRootPath);
    String entryPath = path.relative(mainScript, from: _projectRootPath);
    String diskEntryPath = baseUri.resolve(entryPath).toFilePath();
    String diskPackagesPath = baseUri.resolve('.packages').toFilePath();
    String diskAssetsDirectoryPath = baseUri.resolve('build/flx').toFilePath();
    await _launchInView(diskEntryPath,
                        diskPackagesPath,
                        diskAssetsDirectoryPath);
  }

  Future<Null> _restartFromSources() async {
    if (_devFS == null) {
      Status restartStatus = logger.startProgress('Restarting application...');
      await _launchFromDisk(_package, _mainPath);
      restartStatus.stop(showElapsedTime: true);
    } else {
      await _updateDevFS();
      Status restartStatus = logger.startProgress('Restarting application...');
      await _launchFromDevFS(_package, _mainPath);
      restartStatus.stop(showElapsedTime: true);
    }
  }

  /// Returns [true] if the reload was successful.
  bool _printReloadReport(Map<String, dynamic> reloadReport) {
    if (!reloadReport['success']) {
      printError('Hot reload was rejected:');
      for (Map<String, dynamic> notice in reloadReport['details']['notices']) {
        printError('${notice['message']}');
      }
      return false;
    }
    int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
    int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
    printStatus('Reloaded $loadedLibraryCount out of $finalLibraryCount libraries.');
    return true;
  }

  @override
  Future<bool> restart() => _reloadSources();

  Future<bool> _reloadSources() async {
    if (serviceProtocol.firstIsolateId == null)
      throw 'Application isolate not found';
    if (_devFS != null)
      await _updateDevFS();
    Status reloadStatus = logger.startProgress('Performing hot reload');
    try {
      Map<String, dynamic> reloadReport =
          await serviceProtocol.reloadSources(serviceProtocol.firstIsolateId);
      reloadStatus.stop(showElapsedTime: true);
      if (!_printReloadReport(reloadReport)) {
        // Reload failed.
        return false;
      }
    } catch (errorMessage) {
      reloadStatus.stop(showElapsedTime: true);
      printError('Hot reload failed:\n$errorMessage');
      return false;
    }
    Status reassembleStatus =
        logger.startProgress('Reassembling application');
    try {
      await serviceProtocol.flutterReassemble(serviceProtocol.firstIsolateId);
    } catch (_) {
      reassembleStatus.stop(showElapsedTime: true);
      printError('Reassembling application failed.');
      return false;
    }
    reassembleStatus.stop(showElapsedTime: true);
    return true;
  }

  @override
  void printHelp() {
    printStatus('Type "h" or F1 for this help message. Type "q", F10, or ctrl-c to quit.', emphasis: true);
    printStatus('Type "r" or F5 to perform a hot reload of the app.', emphasis: true);
    printStatus('Type "R" to restart the app', emphasis: true);
    printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.', emphasis: true);
  }

  @override
  Future<Null> cleanupAfterSignal() async {
    await stopEchoingDeviceLog();
    await stopApp();
  }

  @override
  Future<Null> cleanupAtFinish() async {
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}
