// 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 'package:meta/meta.dart';

import '../application_package.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../convert.dart';
import '../dart/package_map.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../protocol_discovery.dart';
import '../version.dart';

class FlutterTesterApp extends ApplicationPackage {
  factory FlutterTesterApp.fromCurrentDirectory() {
    return FlutterTesterApp._(globals.fs.currentDirectory);
  }

  FlutterTesterApp._(Directory directory)
    : _directory = directory,
      super(id: directory.path);

  final Directory _directory;

  @override
  String get name => _directory.basename;

  @override
  File get packagesFile => _directory.childFile('.packages');
}

// TODO(scheglov): This device does not currently work with full restarts.
class FlutterTesterDevice extends Device {
  FlutterTesterDevice(String deviceId) : super(
      deviceId,
      platformType: null,
      category: null,
      ephemeral: false,
  );

  Process _process;
  final DevicePortForwarder _portForwarder = _NoopPortForwarder();

  @override
  Future<bool> get isLocalEmulator async => false;

  @override
  Future<String> get emulatorId async => null;

  @override
  String get name => 'Flutter test device';

  @override
  DevicePortForwarder get portForwarder => _portForwarder;

  @override
  Future<String> get sdkNameAndVersion async {
    final FlutterVersion flutterVersion = globals.flutterVersion;
    return 'Flutter ${flutterVersion.frameworkRevisionShort}';
  }

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester;

  @override
  void clearLogs() { }

  final _FlutterTesterDeviceLogReader _logReader =
      _FlutterTesterDeviceLogReader();

  @override
  DeviceLogReader getLogReader({ ApplicationPackage app }) => _logReader;

  @override
  Future<bool> installApp(ApplicationPackage app) async => true;

  @override
  Future<bool> isAppInstalled(ApplicationPackage app) async => false;

  @override
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;

  @override
  bool isSupported() => true;

  bool _isRunning = false;
  bool get isRunning => _isRunning;

  @override
  Future<LaunchResult> startApp(
    ApplicationPackage package, {
    @required String mainPath,
    String route,
    @required DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
    bool prebuiltApplication = false,
    bool ipv6 = false,
  }) async {
    final BuildInfo buildInfo = debuggingOptions.buildInfo;

    if (!buildInfo.isDebug) {
      globals.printError('This device only supports debug mode.');
      return LaunchResult.failed();
    }

    final String shellPath = globals.artifacts.getArtifactPath(Artifact.flutterTester);
    if (!globals.fs.isFileSync(shellPath)) {
      throwToolExit('Cannot find Flutter shell at $shellPath');
    }

    final List<String> command = <String>[
      shellPath,
      '--run-forever',
      '--non-interactive',
      '--enable-dart-profiling',
      '--packages=${PackageMap.globalPackagesPath}',
    ];
    if (debuggingOptions.debuggingEnabled) {
      if (debuggingOptions.startPaused) {
        command.add('--start-paused');
      }
      if (debuggingOptions.disableServiceAuthCodes) {
        command.add('--disable-service-auth-codes');
      }
      if (debuggingOptions.hasObservatoryPort) {
        command.add('--observatory-port=${debuggingOptions.hostVmServicePort}');
      }
    }

    // Build assets and perform initial compilation.
    final String assetDirPath = getAssetBuildDirectory();
    final String applicationKernelFilePath = getKernelPathForTransformerOptions(
      globals.fs.path.join(getBuildDirectory(), 'flutter-tester-app.dill'),
      trackWidgetCreation: buildInfo.trackWidgetCreation,
    );
    await BundleBuilder().build(
      buildMode: buildInfo.mode,
      mainPath: mainPath,
      assetDirPath: assetDirPath,
      applicationKernelFilePath: applicationKernelFilePath,
      precompiledSnapshot: false,
      trackWidgetCreation: buildInfo.trackWidgetCreation,
      platform: getTargetPlatformForName(getNameForHostPlatform(getCurrentHostPlatform())),
    );
    command.add('--flutter-assets-dir=$assetDirPath');

    command.add(applicationKernelFilePath);

    try {
      globals.printTrace(command.join(' '));

      _isRunning = true;
      _process = await globals.processManager.start(command,
        environment: <String, String>{
          'FLUTTER_TEST': 'true',
        },
      );
      // Setting a bool can't fail in the callback.
      unawaited(_process.exitCode.then<void>((_) => _isRunning = false));
      _process.stdout
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .listen((String line) {
          _logReader.addLine(line);
        });
      _process.stderr
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .listen((String line) {
          _logReader.addLine(line);
        });

      if (!debuggingOptions.debuggingEnabled) {
        return LaunchResult.succeeded();
      }

      final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(
        getLogReader(),
        hostPort: debuggingOptions.hostVmServicePort,
        devicePort: debuggingOptions.deviceVmServicePort,
        ipv6: ipv6,
      );

      final Uri observatoryUri = await observatoryDiscovery.uri;
      return LaunchResult.succeeded(observatoryUri: observatoryUri);
    } catch (error) {
      globals.printError('Failed to launch $package: $error');
      return LaunchResult.failed();
    }
  }

  @override
  Future<bool> stopApp(ApplicationPackage app) async {
    _process?.kill();
    _process = null;
    return true;
  }

  @override
  Future<bool> uninstallApp(ApplicationPackage app) async => true;

  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;

  @override
  Future<void> dispose() async {
    _logReader?.dispose();
    await _portForwarder?.dispose();
  }
}

class FlutterTesterDevices extends PollingDeviceDiscovery {
  FlutterTesterDevices() : super('Flutter tester');

  static const String kTesterDeviceId = 'flutter-tester';

  static bool showFlutterTesterDevice = false;

  final FlutterTesterDevice _testerDevice =
      FlutterTesterDevice(kTesterDeviceId);

  @override
  bool get canListAnything => true;

  @override
  bool get supportsPlatform => true;

  @override
  Future<List<Device>> pollingGetDevices() async {
    return showFlutterTesterDevice ? <Device>[_testerDevice] : <Device>[];
  }
}

class _FlutterTesterDeviceLogReader extends DeviceLogReader {
  final StreamController<String> _logLinesController =
      StreamController<String>.broadcast();

  @override
  int get appPid => 0;

  @override
  Stream<String> get logLines => _logLinesController.stream;

  @override
  String get name => 'flutter tester log reader';

  void addLine(String line) => _logLinesController.add(line);

  @override
  void dispose() {}
}

/// A fake port forwarder that doesn't do anything. Used by flutter tester
/// where the VM is running on the same machine and does not need ports forwarding.
class _NoopPortForwarder extends DevicePortForwarder {
  @override
  Future<int> forward(int devicePort, { int hostPort }) {
    if (hostPort != null && hostPort != devicePort) {
      throw 'Forwarding to a different port is not supported by flutter tester';
    }
    return Future<int>.value(devicePort);
  }

  @override
  List<ForwardedPort> get forwardedPorts => <ForwardedPort>[];

  @override
  Future<void> unforward(ForwardedPort forwardedPort) async { }

  @override
  Future<void> dispose() async { }
}
