///|
/// Handle to a spawned PTY plus its event-loop registration.
///
/// On Unix the PTY master fd is used for both reading and writing, so
/// `read_io` and `write_io` are the same `RawFd` wrapper around the same
/// `IoHandle`. The two fields exist so that reader and writer code paths
/// can use intent-revealing names, and so that a future Windows build
/// can point them at the separate input/output pipe HANDLEs without
/// disturbing callers.
struct Pty {
  handle : PtyHandle
  read_io : @raw_fd.RawFd
  write_io : @raw_fd.RawFd
}

///|
/// POSIX `EINVAL`. Used as the error code when `argv` is empty —
/// `@os_error.errno_to_string` renders it as "Invalid argument".
const EINVAL : Int = 22

///|
/// Spawn a new PTY running the given program.
///
/// `argv[0]` is the program to execute (resolved via PATH by `execvp`),
/// and `argv[1..]` are its arguments.
///
/// `cols` and `rows` default to 80x24 if not specified.
///
/// Must be called from inside an async event-loop context — the PTY's
/// master fd is registered with the event loop on construction so that
/// reads and writes can suspend instead of blocking the thread.
///
/// Raises `@os_error.OSError` wrapping `errno` (Unix) or `GetLastError()`
/// (Windows) if the underlying syscall fails.
pub async fn Pty::spawn(
  argv : Array[String],
  cols? : Int = 80,
  rows? : Int = 24,
) -> Pty {
  if argv.is_empty() {
    raise @os_error.OSError(EINVAL, context="Pty::spawn: empty argv")
  }
  // Flatten argv into a single null-separated byte buffer:
  //   "arg0\0arg1\0...argN\0"
  // The C side walks the buffer by null byte, using the buffer's own
  // length to know when to stop.
  let buf = @buffer.new()
  for arg in argv {
    buf.write_bytes(@utf8.encode(arg[:]))
    buf.write_byte(b'\x00')
  }
  let argv_flat = buf.contents()
  let handle = pty_spawn(argv_flat, cols, rows)
  let spawn_err = pty_check_spawn(handle)
  if spawn_err != 0 {
    pty_close(handle)
    raise @os_error.OSError(spawn_err, context="Pty::spawn")
  }
  // Transfer fd ownership from the C PTY handle to `RawFd`.
  let read_io = @raw_fd.RawFd::new(pty_take_read_fd(handle)) catch {
    err => {
      pty_close(handle)
      raise err
    }
  }
  let write_io = register_write_io(handle, read_io)
  { handle, read_io, write_io }
}

///|
/// On Unix read and write share the same PTY master fd.
#cfg(not(platform="windows"))
fn register_write_io(
  _handle : PtyHandle,
  read_io : @raw_fd.RawFd,
) -> @raw_fd.RawFd {
  read_io
}

///|
/// On Windows ConPTY has a separate input pipe HANDLE.
#cfg(platform="windows")
fn register_write_io(
  handle : PtyHandle,
  read_io : @raw_fd.RawFd,
) -> @raw_fd.RawFd {
  @raw_fd.RawFd::new(pty_take_write_fd(handle)) catch {
    err => {
      read_io.close()
      pty_close(handle)
      raise err
    }
  }
}

///|
/// Get the async reader for this PTY.
///
/// The returned `RawFd` is owned by the `Pty`; do not call `close()`
/// or `detach()` on it. Use `Pty::close` to release the registration.
pub fn Pty::reader(self : Pty) -> @raw_fd.RawFd {
  self.read_io
}

///|
/// Write data to the PTY stdin.
///
/// Suspends the current coroutine via the async event loop until all
/// bytes are written, so a stuck child that stops draining its input
/// buffer won't block other concurrent tasks in the runtime.
///
/// Raises `@os_error.OSError` if the underlying write fails.
pub async fn Pty::write(self : Pty, data : Bytes) -> Unit {
  let total = data.length()
  let mut offset = 0
  while offset < total {
    let n = self.write_io.write(data, offset~, len=total - offset)
    if n <= 0 {
      raise @os_error.OSError(EINVAL, context="Pty::write: short write")
    }
    offset += n
  }
}

///|
/// Resize the PTY window.
///
/// Raises `@os_error.OSError` if the underlying syscall fails.
pub fn Pty::resize(
  self : Pty,
  cols : Int,
  rows : Int,
) -> Unit raise @os_error.OSError {
  // Explicitly narrowed: pty_resize is the only source of error here.
  let err = pty_resize(self.handle, cols, rows)
  if err != 0 {
    raise @os_error.OSError(err, context="Pty::resize")
  }
}

///|
/// Get the spawned child PID when available.
pub fn Pty::pid(self : Pty) -> Int {
  pty_child_pid(self.handle)
}

///|
/// Close the PTY, releasing all resources.
///
/// Closes the owned `RawFd` wrapper(s) and then tears down the underlying
/// OS PTY handle.
///
/// Safe to call multiple times because the underlying close paths are
/// idempotent.
#cfg(not(platform="windows"))
pub fn Pty::close(self : Pty) -> Unit {
  self.read_io.close()
  pty_close(self.handle)
}

///|
/// Close the PTY, releasing all resources.
///
/// Closes the owned `RawFd` wrapper(s) and then tears down the underlying
/// OS PTY handle.
///
/// Safe to call multiple times because the underlying close paths are
/// idempotent.
#cfg(platform="windows")
pub fn Pty::close(self : Pty) -> Unit {
  self.read_io.close()
  self.write_io.close()
  pty_close(self.handle)
}