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

import 'application_package.dart';
import 'artifacts.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'build_info.dart';
import 'bundle_builder.dart';
import 'desktop_device.dart';
import 'devfs.dart';
import 'device.dart';
import 'device_port_forwarder.dart';
import 'features.dart';
import 'project.dart';
import 'protocol_discovery.dart';

typedef BundleBuilderFactory = BundleBuilder Function();

BundleBuilder _defaultBundleBuilder() {
  return BundleBuilder();
}

class PreviewDeviceDiscovery extends DeviceDiscovery {
  PreviewDeviceDiscovery({
    required Platform platform,
    required Artifacts artifacts,
    required FileSystem fileSystem,
    required Logger logger,
    required ProcessManager processManager,
    required FeatureFlags featureFlags,
  }) : _artifacts = artifacts,
       _logger = logger,
       _processManager = processManager,
       _fileSystem = fileSystem,
       _platform = platform,
       _features = featureFlags;

  final Platform _platform;
  final Artifacts _artifacts;
  final Logger _logger;
  final ProcessManager _processManager;
  final FileSystem _fileSystem;
  final FeatureFlags _features;

  @override
  bool get canListAnything => _platform.isWindows;

  @override
  bool get supportsPlatform => _platform.isWindows;

  @override
  List<String> get wellKnownIds => <String>['preview'];

  @override
  Future<List<Device>> devices({
    Duration? timeout,
    DeviceDiscoveryFilter? filter,
  }) async {
    final File previewBinary = _fileSystem.file(_artifacts.getArtifactPath(Artifact.flutterPreviewDevice));
    if (!previewBinary.existsSync()) {
      return const <Device>[];
    }
    final PreviewDevice device = PreviewDevice(
      artifacts: _artifacts,
      fileSystem: _fileSystem,
      logger: _logger,
      processManager: _processManager,
      previewBinary: previewBinary,
    );
    final bool matchesRequirements;
    if (!_features.isPreviewDeviceEnabled) {
      matchesRequirements = false;
    } else if (filter == null) {
      matchesRequirements = true;
    } else {
      matchesRequirements = await filter.matchesRequirements(device);
    }
    return <Device>[
      if (matchesRequirements)
        device,
    ];
  }

  @override
  Future<List<Device>> discoverDevices({
    Duration? timeout,
    DeviceDiscoveryFilter? filter,
  }) {
    return devices();
  }
}

/// A device type that runs a prebuilt desktop binary alongside a locally compiled kernel file.
class PreviewDevice extends Device {
  PreviewDevice({
    required ProcessManager processManager,
    required Logger logger,
    required FileSystem fileSystem,
    required Artifacts artifacts,
    required File previewBinary,
    @visibleForTesting BundleBuilderFactory builderFactory = _defaultBundleBuilder,
  }) : _previewBinary = previewBinary,
       _processManager = processManager,
       _logger = logger,
       _fileSystem = fileSystem,
       _bundleBuilderFactory = builderFactory,
       _artifacts = artifacts,
       super('preview', ephemeral: false, category: Category.desktop, platformType: PlatformType.custom);

  final ProcessManager _processManager;
  final Logger _logger;
  final FileSystem _fileSystem;
  final BundleBuilderFactory _bundleBuilderFactory;
  final Artifacts _artifacts;
  final File _previewBinary;

  @override
  void clearLogs() { }

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

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

  final DesktopLogReader _logReader = DesktopLogReader();

  @override
  FutureOr<DeviceLogReader> getLogReader({ApplicationPackage? app, bool includePastLogs = false}) => _logReader;

  @override
  Future<bool> installApp(ApplicationPackage? app, {String? userIdentifier}) async => true;

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

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

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

  @override
  bool isSupported() => true;

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

  @override
  String get name => 'preview';

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

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

  Process? _process;

  @override
  Future<LaunchResult> startApp(ApplicationPackage? package, {
    String? mainPath,
    String? route,
    required DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs = const <String, dynamic>{},
    bool prebuiltApplication = false,
    bool ipv6 = false,
    String? userIdentifier,
  }) async {
    final Directory assetDirectory = _fileSystem.systemTempDirectory
      .createTempSync('flutter_preview.');

    // Build assets and perform initial compilation.
    Status? status;
    try {
      status = _logger.startProgress('Compiling application for preview...');
      await _bundleBuilderFactory().build(
        buildInfo: debuggingOptions.buildInfo,
        mainPath: mainPath,
        platform: TargetPlatform.windows_x64,
        assetDirPath: getAssetBuildDirectory(),
      );
      copyDirectory(_fileSystem.directory(
        getAssetBuildDirectory()),
        assetDirectory.childDirectory('data').childDirectory('flutter_assets'),
      );
    } finally {
      status?.stop();
    }

    // Merge with precompiled executable.
    final String copiedPreviewBinaryPath = assetDirectory.childFile(_previewBinary.basename).path;
    _previewBinary.copySync(copiedPreviewBinaryPath);

    final String windowsPath = _artifacts
      .getArtifactPath(Artifact.windowsDesktopPath, platform: TargetPlatform.windows_x64, mode: BuildMode.debug);
    final File windowsDll = _fileSystem.file(_fileSystem.path.join(windowsPath, 'flutter_windows.dll'));
    final File icu = _fileSystem.file(_fileSystem.path.join(windowsPath, 'icudtl.dat'));
    windowsDll.copySync(assetDirectory.childFile('flutter_windows.dll').path);
    icu.copySync(assetDirectory.childDirectory('data').childFile('icudtl.dat').path);

    final Process process = await _processManager.start(
      <String>[copiedPreviewBinaryPath],
    );
    _process = process;
    _logReader.initializeProcess(process);

    final ProtocolDiscovery vmServiceDiscovery = ProtocolDiscovery.vmService(_logReader,
      devicePort: debuggingOptions.deviceVmServicePort,
      hostPort: debuggingOptions.hostVmServicePort,
      ipv6: ipv6,
      logger: _logger,
    );
    try {
      final Uri? vmServiceUri = await vmServiceDiscovery.uri;
      if (vmServiceUri != null) {
        return LaunchResult.succeeded(vmServiceUri: vmServiceUri);
      }
      _logger.printError(
        'Error waiting for a debug connection: '
        'The log reader stopped unexpectedly.',
      );
    } on Exception catch (error) {
      _logger.printError('Error waiting for a debug connection: $error');
    } finally {
      await vmServiceDiscovery.cancel();
    }
    return LaunchResult.failed();
  }

  @override
  Future<bool> stopApp(ApplicationPackage? app, {String? userIdentifier}) async {
    return _process?.kill() ?? false;
  }

  @override
  Future<TargetPlatform> get targetPlatform async {
    return TargetPlatform.windows_x64;
  }

  @override
  Future<bool> uninstallApp(ApplicationPackage app, {String? userIdentifier}) async {
    return true;
  }

  @override
  DevFSWriter createDevFSWriter(ApplicationPackage? app, String? userIdentifier) {
    return LocalDevFSWriter(fileSystem: _fileSystem);
  }
}
