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

///|
/// A temporary pipe used to read output from a spawned process
struct ReadFromProcess {
  io : @event_loop.IoHandle
  read_buf : @io.ReaderBuffer
}

///|
/// A temporary pipe used to write data to a spawned process
struct WriteToProcess(@event_loop.IoHandle)

///|
/// Create a temporary pipe for reading from stdout/stderr of a process.
/// The return value is a pair `(r, w)`,
/// where `r` is a temporary pipe that can be used to read process output,
/// and `w` should be passed to `@process.run`.
///
/// `w` is temporary: it can only be passed to one `@process.run` call.
/// However, it is safe to pass `w` to both `stdout` and `stderr` of the same process.
pub fn read_from_process() -> (ReadFromProcess, &ProcessOutput) raise {
  let context = "@process.read_from_process()"
  let (r, w) = @fd_util.pipe(
    read_end_is_async=true,
    write_end_is_async=false,
    context~,
  )
  let r = @event_loop.IoHandle::from_fd(r, kind=Pipe)
  let w = @event_loop.IoHandle::from_fd(w, kind=Pipe, is_async=false)
  (
    { io: r, read_buf: @io.ReaderBuffer::new() },
    TempPipeWrite::{ pipe: w, closed: false },
  )
}

///|
/// Create a temporary pipe for writing to stdin of a process.
/// The return value is a pair `(r, w)`,
/// where `w` is a temporary pipe that can be used to write to process output,
/// and `r` should be passed to `@process.run`.
pub fn write_to_process() -> (&ProcessInput, WriteToProcess) raise {
  let context = "@process.write_to_process()"
  let (r, w) = @fd_util.pipe(
    read_end_is_async=false,
    write_end_is_async=true,
    context~,
  )
  let r = @event_loop.IoHandle::from_fd(r, kind=Pipe, is_async=false)
  let w = @event_loop.IoHandle::from_fd(w, kind=Pipe)
  (TempPipeRead::{ pipe: r, closed: false }, w)
}

///|
pub fn ReadFromProcess::close(self : ReadFromProcess) -> Unit {
  self.io.close()
}

///|
pub impl @io.Reader for ReadFromProcess with fn _get_internal_buffer(self) {
  self.read_buf
}

///|
pub impl @io.Reader for ReadFromProcess with fn _direct_read(
  self,
  buf,
  offset~,
  max_len~,
) {
  self.io.read(
    buf,
    offset~,
    len=max_len,
    context="@process.ReadFromProcess::read()",
  )
}

///|
pub fn WriteToProcess::close(self : WriteToProcess) -> Unit {
  let WriteToProcess(io) = self
  io.close()
}

///|
pub impl @io.Writer for WriteToProcess with fn write_once(
  self,
  buf,
  offset~,
  len~,
) {
  let WriteToProcess(io) = self
  io.write(buf, offset~, len~, context="@process.WriteToProcess::write()")
}

///|
fn @fs.CreateMode::to_int(self : @fs.CreateMode) -> Int = "%identity"

///|
/// Redirect the output of a process to the file at `path`.
/// The meaning of `append`, `create_mode` and `permission` is the same as `@fs.open`,
/// see the document of `@fs.open` for more details.
#label_migration(create, fill=false, msg="the option `create` is deprecated, use `create_mode` and `permission` instead")
#label_migration(truncate, fill=false, msg="the option `truncate` is deprecated, use `create_mode` instead")
pub async fn redirect_to_file(
  path : String,
  append? : Bool = false,
  create_mode? : @fs.CreateMode,
  permission? : Int,
  create? : Int,
  truncate? : Bool = false,
) -> &ProcessOutput {
  let create_mode = match create_mode {
    Some(mode) => mode
    None =>
      match (create is Some(_), truncate) {
        (true, true) => CreateOrTruncate
        (true, false) => OpenOrCreate
        (false, true) => TruncateExisting
        (false, false) => OpenExisting
      }
  }
  let permission = match permission {
    Some(perm) => perm
    None if create is Some(perm) => perm
    None => 0o644
  }
  let (file, _) = @event_loop.open(
    path,
    1, // write only
    create=create_mode.to_int(),
    append~,
    sync=0,
    mode=permission,
    context="@process.redirect_to_file()",
  )
  RedirectToFile(file)
}

///|
/// Redirect the content of a file at `path` to the stdin of a process.
pub async fn redirect_from_file(path : String) -> &ProcessInput {
  let (file, _) = @event_loop.open(
    path,
    0, // read only
    create=0, // `OpenExisting`
    append=false,
    sync=0,
    mode=0,
    context="@process.redirect_from_file()",
  )
  RedirectToFile(file)
}

///|
/// An entity that can be used to redirect stdin of a process
trait ProcessInput {
  fn fd(Self) -> @fd_util.Fd raise
  fn after_spawn(Self) -> Unit = _
}

///|
impl ProcessInput with fn after_spawn(_) {
  ()
}

///|
pub impl ProcessInput for @pipe.PipeRead with fn fd(self) {
  self.fd()
}

///|
pub impl ProcessInput for @stdio.Input with fn fd(self) {
  self.fd()
}

///|
/// An entity that can be used to redirect stdout/stderr of a process
trait ProcessOutput {
  fn fd(Self) -> @fd_util.Fd raise
  fn after_spawn(Self) -> Unit = _
}

///|
impl ProcessOutput with fn after_spawn(_) {
  ()
}

///|
pub impl ProcessOutput for @pipe.PipeWrite with fn fd(self) {
  self.fd()
}

///|
pub impl ProcessOutput for @stdio.Output with fn fd(self) {
  self.fd()
}

///|
priv struct TempPipeRead {
  pipe : @event_loop.IoHandle
  mut closed : Bool
}

///|
priv struct TempPipeWrite {
  pipe : @event_loop.IoHandle
  mut closed : Bool
}

///|
impl ProcessOutput for TempPipeWrite with fn fd(self) {
  self.pipe.fd()
}

///|
impl ProcessOutput for TempPipeWrite with fn after_spawn(self) {
  if !self.closed {
    self.closed = true
    self.pipe.close()
  }
}

///|
impl ProcessInput for TempPipeRead with fn fd(self) {
  self.pipe.fd()
}

///|
impl ProcessInput for TempPipeRead with fn after_spawn(self) {
  if !self.closed {
    self.closed = true
    self.pipe.close()
  }
}

///|
priv struct RedirectToFile(@event_loop.IoHandle)

///|
impl ProcessOutput for RedirectToFile with fn fd(self) {
  self.0.fd()
}

///|
impl ProcessOutput for RedirectToFile with fn after_spawn(self) {
  self.0.close()
}

///|
impl ProcessInput for RedirectToFile with fn fd(self) {
  self.0.fd()
}

///|
impl ProcessInput for RedirectToFile with fn after_spawn(self) {
  self.0.close()
}