// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: close_sinks,cancel_subscriptions

import 'dart:async';
import 'dart:io' as io;

import 'package:meta/meta.dart';

import 'shared_stdin.dart';

/// Type definition for both [io.Process.start] and [ProcessManager.spawn].
///
/// Useful for taking different implementations of this base functionality.
typedef StartProcess = Future<io.Process> Function(
  String executable,
  List<String> arguments, {
  String workingDirectory,
  Map<String, String> environment,
  bool includeParentEnvironment,
  bool runInShell,
  io.ProcessStartMode mode,
});

/// A high-level abstraction around using and managing processes on the system.
abstract class ProcessManager {
  /// Terminates the global `stdin` listener, making future listens impossible.
  ///
  /// This method should be invoked only at the _end_ of a program's execution.
  static Future<void> terminateStdIn() async {
    await sharedStdIn.terminate();
  }

  /// Create a new instance of [ProcessManager] for the current platform.
  ///
  /// May manually specify whether the current platform [isWindows], otherwise
  /// this is derived from the Dart runtime (i.e. [io.Platform.isWindows]).
  factory ProcessManager({
    Stream<List<int>>? stdin,
    io.IOSink? stdout,
    io.IOSink? stderr,
    bool? isWindows,
  }) {
    stdin ??= sharedStdIn;
    stdout ??= io.stdout;
    stderr ??= io.stderr;
    isWindows ??= io.Platform.isWindows;
    if (isWindows) {
      return _WindowsProcessManager(stdin, stdout, stderr);
    }
    return _UnixProcessManager(stdin, stdout, stderr);
  }

  final Stream<List<int>> _stdin;
  final io.IOSink _stdout;
  final io.IOSink _stderr;

  const ProcessManager._(this._stdin, this._stdout, this._stderr);

  /// Spawns a process by invoking [executable] with [arguments].
  ///
  /// This is _similar_ to [io.Process.start], but all standard input and output
  /// is forwarded/routed between the process and the host, similar to how a
  /// shell script works.
  ///
  /// Returns a future that completes with a handle to the spawned process.
  Future<io.Process> spawn(
    String executable,
    Iterable<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
    bool includeParentEnvironment = true,
    bool runInShell = false,
    io.ProcessStartMode mode = io.ProcessStartMode.normal,
  }) async {
    final process = io.Process.start(
      executable,
      arguments.toList(),
      workingDirectory: workingDirectory,
      environment: environment,
      includeParentEnvironment: includeParentEnvironment,
      runInShell: runInShell,
      mode: mode,
    );
    return _ForwardingSpawn(await process, _stdin, _stdout, _stderr);
  }

  /// Spawns a process by invoking [executable] with [arguments].
  ///
  /// This is _similar_ to [io.Process.start], but `stdout` and `stderr` is
  /// forwarded/routed between the process and host, similar to how a shell
  /// script works.
  ///
  /// Returns a future that completes with a handle to the spawned process.
  Future<io.Process> spawnBackground(
    String executable,
    Iterable<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
    bool includeParentEnvironment = true,
    bool runInShell = false,
    io.ProcessStartMode mode = io.ProcessStartMode.normal,
  }) async {
    final process = io.Process.start(
      executable,
      arguments.toList(),
      workingDirectory: workingDirectory,
      environment: environment,
      includeParentEnvironment: includeParentEnvironment,
      runInShell: runInShell,
      mode: mode,
    );
    return _ForwardingSpawn(
      await process,
      const Stream.empty(),
      _stdout,
      _stderr,
    );
  }

  /// Spawns a process by invoking [executable] with [arguments].
  ///
  /// This is _identical to [io.Process.start] (no forwarding of I/O).
  ///
  /// Returns a future that completes with a handle to the spawned process.
  Future<io.Process> spawnDetached(
    String executable,
    Iterable<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
    bool includeParentEnvironment = true,
    bool runInShell = false,
    io.ProcessStartMode mode = io.ProcessStartMode.normal,
  }) async =>
      io.Process.start(
        executable,
        arguments.toList(),
        workingDirectory: workingDirectory,
        environment: environment,
        includeParentEnvironment: includeParentEnvironment,
        runInShell: runInShell,
        mode: mode,
      );
}

/// A process instance created and managed through [ProcessManager].
///
/// Unlike one created directly by [io.Process.start] or [io.Process.run], a
/// spawned process works more like executing a command in a shell script.
class Spawn implements io.Process {
  final io.Process _delegate;

  Spawn._(this._delegate) {
    _delegate.exitCode.then((_) => _onClosed());
  }

  @mustCallSuper
  void _onClosed() {}

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) =>
      _delegate.kill(signal);

  @override
  Future<int> get exitCode => _delegate.exitCode;

  @override
  int get pid => _delegate.pid;

  @override
  Stream<List<int>> get stderr => _delegate.stderr;

  @override
  io.IOSink get stdin => _delegate.stdin;

  @override
  Stream<List<int>> get stdout => _delegate.stdout;
}

/// Forwards `stdin`/`stdout`/`stderr` to/from the host.
class _ForwardingSpawn extends Spawn {
  final StreamSubscription<List<int>> _stdInSub;
  final StreamSubscription<List<int>> _stdOutSub;
  final StreamSubscription<List<int>> _stdErrSub;
  final StreamController<List<int>> _stdOut;
  final StreamController<List<int>> _stdErr;

  factory _ForwardingSpawn(
    io.Process delegate,
    Stream<List<int>> stdin,
    io.IOSink stdout,
    io.IOSink stderr,
  ) {
    final stdoutSelf = StreamController<List<int>>();
    final stderrSelf = StreamController<List<int>>();
    final stdInSub = stdin.listen(delegate.stdin.add);
    final stdOutSub = delegate.stdout.listen((event) {
      stdout.add(event);
      stdoutSelf.add(event);
    });
    final stdErrSub = delegate.stderr.listen((event) {
      stderr.add(event);
      stderrSelf.add(event);
    });
    return _ForwardingSpawn._delegate(
      delegate,
      stdInSub,
      stdOutSub,
      stdErrSub,
      stdoutSelf,
      stderrSelf,
    );
  }

  _ForwardingSpawn._delegate(
    super.delegate,
    this._stdInSub,
    this._stdOutSub,
    this._stdErrSub,
    this._stdOut,
    this._stdErr,
  ) : super._();

  @override
  void _onClosed() {
    _stdInSub.cancel();
    _stdOutSub.cancel();
    _stdErrSub.cancel();
    super._onClosed();
  }

  @override
  Stream<List<int>> get stdout => _stdOut.stream;

  @override
  Stream<List<int>> get stderr => _stdErr.stream;
}

class _UnixProcessManager extends ProcessManager {
  const _UnixProcessManager(
    super.stdin,
    super.stdout,
    super.stderr,
  ) : super._();
}

class _WindowsProcessManager extends ProcessManager {
  const _WindowsProcessManager(
    super.stdin,
    super.stdout,
    super.stderr,
  ) : super._();
}
