// 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.

// @dart = 2.8

import 'dart:async';

import 'package:meta/meta.dart';
import 'package:process/process.dart';

import '../application_package.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../desktop_device.dart';
import '../device.dart';
import '../device_port_forwarder.dart';
import '../features.dart';
import '../project.dart';
import 'application_package.dart';
import 'build_windows.dart';
import 'uwptool.dart';
import 'windows_workflow.dart';

/// A device that represents a desktop Windows target.
class WindowsDevice extends DesktopDevice {
  WindowsDevice({
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
  }) : super(
      'windows',
      platformType: PlatformType.windows,
      ephemeral: false,
      processManager: processManager,
      logger: logger,
      fileSystem: fileSystem,
      operatingSystemUtils: operatingSystemUtils,
  );

  @override
  bool isSupported() => true;

  @override
  String get name => 'Windows';

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

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.windows.existsSync();
  }

  @override
  Future<void> buildForDevice(
    covariant WindowsApp package, {
    String mainPath,
    BuildInfo buildInfo,
  }) async {
    await buildWindows(
      FlutterProject.current().windows,
      buildInfo,
      target: mainPath,
    );
  }

  @override
  String executablePathForDevice(covariant WindowsApp package, BuildMode buildMode) {
    return package.executable(buildMode);
  }
}

// A device that represents a desktop Windows UWP target.
class WindowsUWPDevice extends Device {
  WindowsUWPDevice({
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
    @required UwpTool uwptool,
  }) : _logger = logger,
       _processManager = processManager,
       _operatingSystemUtils = operatingSystemUtils,
       _fileSystem = fileSystem,
       _uwptool = uwptool,
       super(
         'winuwp',
         platformType: PlatformType.windows,
         ephemeral: false,
         category: Category.desktop,
       );

  final ProcessManager _processManager;
  final Logger _logger;
  final FileSystem _fileSystem;
  final OperatingSystemUtils _operatingSystemUtils;
  final UwpTool _uwptool;
  BuildMode _buildMode;

  int _processId;

  @override
  bool isSupported() => true;

  @override
  String get name => 'Windows (UWP)';

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

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.windowsUwp.existsSync();
  }

  @override
  void clearLogs() { }

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

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

  @override
  FutureOr<DeviceLogReader> getLogReader({covariant BuildableUwpApp app, bool includePastLogs = false}) {
    return NoOpDeviceLogReader('winuwp');
  }

  @override
  Future<bool> installApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
    /// The cmake build generates an install powershell script.
    /// build\winuwp\runner_uwp\AppPackages\<app-name>\<app-name>_<app-version>_<cmake-config>\Add-AppDevPackage.ps1
    final String binaryName = app.name;
    final String packageVersion = app.projectVersion;
    if (packageVersion == null) {
      return false;
    }
    final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug));
    final String generated = '${binaryName}_${packageVersion}_${config}_Test';
    final ProcessResult result = await _processManager.run(<String>[
      'powershell.exe',
      _fileSystem.path.join('build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName, generated, 'install.ps1'),
    ]);
    if (result.exitCode != 0) {
      _logger.printError(result.stdout.toString());
      _logger.printError(result.stderr.toString());
    }
    return result.exitCode == 0;
  }

  @override
  Future<bool> isAppInstalled(covariant ApplicationPackage app, {String userIdentifier}) async => false;

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

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

  @override
  DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();

  @override
  Future<String> get sdkNameAndVersion async => '';

  @override
  Future<LaunchResult> startApp(covariant BuildableUwpApp package, {
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
    bool prebuiltApplication = false,
    bool ipv6 = false,
    String userIdentifier,
  }) async {
    _buildMode = debuggingOptions.buildInfo.mode;
    if (!prebuiltApplication) {
      await buildWindowsUwp(
        package.project,
        debuggingOptions.buildInfo,
        target: mainPath,
      );
    }
    if (!await isAppInstalled(package) && !await installApp(package)) {
      _logger.printError('Failed to install app package');
      return LaunchResult.failed();
    }

    final String guid = package.id;
    if (guid == null) {
      _logger.printError('Could not find PACKAGE_GUID in ${package.project.runnerCmakeFile.path}');
      return LaunchResult.failed();
    }

    final String appId = await _uwptool.getAppIdFromPackageId(guid);

    if (debuggingOptions.buildInfo.mode.isRelease) {
      _processId = await _uwptool.launchApp(appId, <String>[]);
      return _processId != null ? LaunchResult.succeeded() : LaunchResult.failed();
    }

    /// If the terminal is attached, prompt the user to open the firewall port.
    if (_logger.terminal.stdinHasTerminal) {
      await _logger.terminal.promptForCharInput(<String>['Y', 'y'], logger: _logger,
        prompt: 'To continue start an admin cmd prompt and run the following command:\n'
        '   checknetisolation loopbackexempt -is -n=$appId\n'
        'Press "Y/y" once this is complete.'
      );
    }

    /// Currently we do not have a way to discover the VM Service URI.
    final int port = debuggingOptions.deviceVmServicePort ?? await _operatingSystemUtils.findFreePort();
    final List<String> args = <String>[
      '--observatory-port=$port',
      '--disable-service-auth-codes',
      '--enable-dart-profiling',
      if (debuggingOptions.startPaused) '--start-paused',
      if (debuggingOptions.useTestFonts) '--use-test-fonts',
      if (debuggingOptions.debuggingEnabled) ...<String>[
        '--enable-checked-mode',
        '--verify-entry-points',
      ],
      if (debuggingOptions.enableSoftwareRendering) '--enable-software-rendering',
      if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
      if (debuggingOptions.traceSkia) '--trace-skia',
      if (debuggingOptions.traceAllowlist != null) '--trace-allowlist="${debuggingOptions.traceAllowlist}"',
      if (debuggingOptions.endlessTraceBuffer) '--endless-trace-buffer',
      if (debuggingOptions.dumpSkpOnShaderCompilation) '--dump-skp-on-shader-compilation',
      if (debuggingOptions.verboseSystemLogs) '--verbose-logging',
      if (debuggingOptions.cacheSkSL) '--cache-sksl',
      if (debuggingOptions.purgePersistentCache) '--purge-persistent-cache',
      if (platformArgs['trace-startup'] as bool ?? false) '--trace-startup',
    ];
    _processId = await _uwptool.launchApp(appId, args);
    if (_processId == null) {
      return LaunchResult.failed();
    }
    return LaunchResult.succeeded(observatoryUri: Uri.parse('http://localhost:$port'));
  }

  @override
  Future<bool> stopApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
    if (_processId != null) {
      return _processManager.killPid(_processId);
    }
    return false;
  }

  @override
  Future<bool> uninstallApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
    return false;
  }

  @override
  FutureOr<bool> supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;
}

class WindowsDevices extends PollingDeviceDiscovery {
  WindowsDevices({
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
    @required WindowsWorkflow windowsWorkflow,
    @required FeatureFlags featureFlags,
    @required UwpTool uwptool,
  }) : _fileSystem = fileSystem,
      _logger = logger,
      _processManager = processManager,
      _operatingSystemUtils = operatingSystemUtils,
      _windowsWorkflow = windowsWorkflow,
      _featureFlags = featureFlags,
      _uwptool = uwptool,
      super('windows devices');

  final FileSystem _fileSystem;
  final Logger _logger;
  final ProcessManager _processManager;
  final OperatingSystemUtils _operatingSystemUtils;
  final WindowsWorkflow _windowsWorkflow;
  final FeatureFlags _featureFlags;
  final UwpTool _uwptool;

  @override
  bool get supportsPlatform => _windowsWorkflow.appliesToHostPlatform;

  @override
  bool get canListAnything => _windowsWorkflow.canListDevices;

  @override
  Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
    if (!canListAnything) {
      return const <Device>[];
    }
    return <Device>[
      WindowsDevice(
        fileSystem: _fileSystem,
        logger: _logger,
        processManager: _processManager,
        operatingSystemUtils: _operatingSystemUtils,
      ),
      if (_featureFlags.isWindowsUwpEnabled)
        WindowsUWPDevice(
          fileSystem: _fileSystem,
          logger: _logger,
          processManager: _processManager,
          operatingSystemUtils: _operatingSystemUtils,
          uwptool: _uwptool,
        )
    ];
  }

  @override
  Future<List<String>> getDiagnostics() async => const <String>[];
}
