// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Execute a system process with command `cmd`,
/// and provide `args` as extra arguments to `cmd.
/// Both `cmd` and elements of `args` are encoded using UTF8.
/// The process ID of the spawned process will be returned.
///
/// The spawned process would be *orphan*, meaning that it will
/// keep running until completion or explicitly terminated,
/// even if the task that calls `spawn_orphan` is cancelled.
/// Users are recommended to use `@process.run` whenever possible,
/// because `@process.run` has better structured concurrency integration.
/// `spawn_orphan` should only be used when:
///
/// - the process is intended to be orphan,
///   i.e. keep running after parent process terminates
/// - operations on the process ID is needed, such as sending signals to the child process
///
/// The meaning of the arguments is the same as `@process.run`,
/// see `@process.run` for more details.
pub async fn spawn_orphan(
  cmd : StringView,
  args : ArrayView[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  stdout? : &ProcessOutput,
  stderr? : &ProcessOutput,
  cwd? : StringView,
  no_console_window? : Bool = false,
) -> Int {
  let proc = raw_spawn(
    cmd,
    args,
    extra_env~,
    inherit_env~,
    stdin~,
    stdout~,
    stderr~,
    cwd~,
    no_console_window~,
    is_orphan=true,
    context="@process.spawn_orphan()",
  )
  defer proc.close()
  proc.pid
}

///|
/// Wait for the process with specfic ID to terminate,
/// and return the exit code of the process.
/// If the process is killed by a signal, the result would be `-signal_number`.
pub async fn wait_pid(pid : Int) -> Int {
  let context = "@process.wait_pid()"
  let process = @event_loop.Process::from_pid(pid, context~)
  defer process.close()
  process.wait_pid(context~)
}

///|
/// Execute a system process with command `cmd`,
/// and provide `args` as extra arguments to `cmd.
/// Both `cmd` and elements of `args` are encoded using UTF8.
///
/// `run` will block until the new process terminates,
/// and returns the exit status of the process.
/// To run the process in background, use `@async.spawn` or `@async.spawn_bg`.
/// If the process is killed by a signal, the result would be `-signal_number`.
///
/// If `inherit_env` is `true` (`true` by default),
/// the new process will inherit environment variables of current process.
///
/// `extra_env`, if present, will set extra environment variables for the new process
/// (in addition to those inherited ones, if `inherit_env` is `true`).
/// Keys and values of `extra_env` are encoded using UTF8.
///
/// The standard IO of the new process will be redirected to
/// `stdin`, stdout` and  stderr`, if set.
/// Standard IO channel can be redirected to one of the following:
///
/// - a temporary pipe created via `read_from_process` or `write_to_process`,
///   which can be used to read from/write to the process directly
/// - a file on the filesystem, via `redirect_to_file` or `redirect_from_file`
/// - an existing `@pipe.PipeRead` or `@pipe.PipeWrite`,
///   for example redirecting standard error to standard out.
///
/// Note than when passing an existing pipe to the process,
/// the ownership of the pipe is *NOT* transferred.
/// So the caller should still close the channel manually when apporiate.
///
/// If `cwd` is present, the spawned command will be executed
/// in the directory specified by `cwd`.
///
/// When `no_console_window` is `true` (`false` by default),
/// creation of a new console window will be disabled on Windows.
/// By default a new console window may be created
/// when the calling process is a GUI application
/// and the child process is a conlose application.
/// `no_console_window` has no effect on non-Windows platforms.
///
/// If current task is cancelled while blocking,
/// `cancel_handler` will be used to automatically stop the process.
/// `@process.run` will not return until the process terminates, even if cancelled.
/// The default value of `cancel_handler` is `graceful_cancel(timeout=5000)`
/// (First try to gracefully terminate the process, and if the process is still running
/// after five seconds, terminate it forcefully).
///
/// If `cancel_handler` is still running after the process terminates,
/// it will be cancelled.
pub async fn run(
  cmd : StringView,
  args : ArrayView[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  stdout? : &ProcessOutput,
  stderr? : &ProcessOutput,
  cwd? : StringView,
  no_console_window? : Bool = false,
  cancel_handler? : CancellationHandler = graceful_cancel(timeout=5000),
) -> Int {
  let context = "@process.run()"
  let process = raw_spawn(
    cmd,
    args,
    extra_env~,
    inherit_env~,
    stdin~,
    stdout~,
    stderr~,
    cwd~,
    no_console_window~,
    is_orphan=false,
    context~,
  )
  defer process.close()
  let pid = process.pid
  process.wait_pid(context~) catch {
    _ if @coroutine.is_being_cancelled() =>
      @async.protect_from_cancel() <| () => {
        @async.with_task_group() <| group => {
          group.spawn_bg(no_wait=true, () => cancel_handler(pid))
          process.wait_pid(context~)
        }
      }
    err => raise err
  }
}

///|
/// A handle to a spawned process
pub struct Process {
  pid : Int
  priv result : @async.Task[Int]
}

///|
/// Wait for a process to terminate, return the exit code of the process
/// If the process is killed by a signal, the result would be `-signal_number`.
pub async fn Process::wait(self : Process) -> Int {
  self.result.wait()
}

///|
/// If the process already terminated, return its exit code.
/// If the process is killed by a signal, the result would be `-signal_number`.
/// If the process is still running, return `None`.
pub fn Process::try_wait(self : Process) -> Int? raise {
  self.result.try_wait()
}

///|
/// Cancel a child process.
/// The method of cancellation is determined by the `cancel_handler` parameter of `@process.spawn`.
/// Notice that after `.cancel()` is called,
/// if may take a while before the child process actually terminates.
/// Use `.wait()` to wait for actual termination of the child process.
pub fn Process::cancel(self : Process) -> Unit {
  self.result.cancel()
}

///|
/// Spawn a child process inside a task group.
/// If `no_wait` is `false` (`fales` by default),
/// the task group will only exit when the child process terminates.
/// If `no_wait` is `true`, the child process will be terminated automatically
/// when the task group terminates.
///
/// A `Process` object will be returned,
/// users can retrieve the PID of the spawned process via `.pid`,
/// or wait for the process to terminate via `.wait()`/`.try_wait()`.
///
/// All arguments except `group` and `no_wait` have the same meaning as `@process.run`,
/// see `@process.run` for more details.
pub async fn[X] spawn(
  group : @async.TaskGroup[X],
  cmd : StringView,
  args : ArrayView[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  stdout? : &ProcessOutput,
  stderr? : &ProcessOutput,
  cwd? : StringView,
  no_console_window? : Bool = false,
  cancel_handler? : CancellationHandler = graceful_cancel(timeout=5000),
  no_wait? : Bool,
) -> Process {
  let context = "@process.spawn()"
  let process = raw_spawn(
    cmd,
    args,
    extra_env~,
    inherit_env~,
    stdin~,
    stdout~,
    stderr~,
    cwd~,
    no_console_window~,
    is_orphan=false,
    context~,
  )
  let pid = process.pid
  let result = group.spawn(no_wait?, () => {
    defer process.close()
    process.wait_pid(context~) catch {
      _ if @coroutine.is_being_cancelled() =>
        @async.protect_from_cancel() <| () => {
          @async.with_task_group() <| group => {
            group.spawn_bg(no_wait=true, () => cancel_handler(pid))
            process.wait_pid(context~)
          }
        }
      err => raise err
    }
  })
  { pid, result }
}

///|
/// Run a process and collect its standard output.
/// Return the exit code of the process and the content of its standard output.
///
/// The meaning of parameters is the same as `@process.run`
pub async fn collect_stdout(
  cmd : StringView,
  args : ArrayView[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  stderr? : &ProcessOutput,
  cwd? : StringView,
  no_console_window? : Bool,
) -> (Int, &@io.Data) {
  let (r, w) = read_from_process()
  @async.with_task_group() <| group => {
    defer r.close()
    let exit_code = group.spawn(() => {
      run(
        cmd,
        args,
        extra_env~,
        inherit_env~,
        stdin?,
        stdout=w,
        stderr?,
        cwd?,
        no_console_window?,
      )
    })
    let output = r.read_all()
    (exit_code.wait(), output)
  }
}

///|
/// Run a process and collect its standard error.
/// Return the exit code of the process and the content of its standard error.
///
/// The meaning of parameters is the same as `@process.run`
pub async fn collect_stderr(
  cmd : StringView,
  args : ArrayView[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  stdout? : &ProcessOutput,
  cwd? : StringView,
  no_console_window? : Bool,
) -> (Int, &@io.Data) {
  let (r, w) = read_from_process()
  @async.with_task_group() <| group => {
    defer r.close()
    let exit_code = group.spawn(() => {
      run(
        cmd,
        args,
        extra_env~,
        inherit_env~,
        stdin?,
        stdout?,
        stderr=w,
        cwd?,
        no_console_window?,
      )
    })
    let output = r.read_all()
    (exit_code.wait(), output)
  }
}

///|
/// Run a process and collect its standard output & standard error.
/// Return the exit code of the process, the content of its standard output,
/// and the content of its standard error.
///
/// The meaning of parameters is the same as `@process.run`
pub async fn collect_output(
  cmd : StringView,
  args : ArrayView[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  cwd? : StringView,
  no_console_window? : Bool,
) -> (Int, &@io.Data, &@io.Data) {
  let (r_out, w_out) = read_from_process()
  let (r_err, w_err) = read_from_process()
  @async.with_task_group() <| group => {
    let exit_code = group.spawn(() => {
      run(
        cmd,
        args,
        extra_env~,
        inherit_env~,
        stdin?,
        stdout=w_out,
        stderr=w_err,
        cwd?,
        no_console_window?,
      )
    })
    let stdout = group.spawn(() => {
      defer r_out.close()
      r_out.read_all()
    })
    let stderr = group.spawn(() => {
      defer r_err.close()
      r_err.read_all()
    })
    (exit_code.wait(), stdout.wait(), stderr.wait())
  }
}

///|
/// Run a process, merge and collect its standard output & standard error.
/// Return the exit code of the process,
/// and the content of its standard output and standard error.
///
/// The meaning of parameters is the same as `@process.run`
pub async fn collect_output_merged(
  cmd : StringView,
  args : Array[String],
  extra_env? : Map[String, String] = Map([]),
  inherit_env? : Bool = true,
  stdin? : &ProcessInput,
  cwd? : StringView,
  no_console_window? : Bool,
) -> (Int, &@io.Data) {
  let (r, w) = read_from_process()
  @async.with_task_group() <| group => {
    defer r.close()
    let exit_code = group.spawn(() => {
      run(
        cmd,
        args,
        extra_env~,
        inherit_env~,
        stdin?,
        stdout=w,
        stderr=w,
        cwd?,
        no_console_window?,
      )
    })
    let output = r.read_all()
    (exit_code.wait(), output)
  }
}