// Native backend: thin wrappers over the C stubs in `sys_native.c`.

///|
extern "C" fn c_last_err() -> Int32 = "moonbit_rgsys_last_err"

///|
extern "C" fn c_cli_args() -> FixedArray[Bytes] = "moonbit_rt_get_cli_args"

///|
extern "C" fn c_exit(code : Int32) = "moonbit_rgsys_exit"

///|
#borrow(name)
extern "C" fn c_getenv(name : Bytes) -> Bytes = "moonbit_rgsys_getenv"

///|
extern "C" fn c_environ() -> Bytes = "moonbit_rgsys_environ"

///|
extern "C" fn c_getcwd() -> Bytes = "moonbit_rgsys_getcwd"

///|
extern "C" fn c_gethostname() -> Bytes = "moonbit_rgsys_gethostname"

///|
extern "C" fn c_now_nanos() -> Int64 = "moonbit_rgsys_now_nanos"

///|
extern "C" fn c_is_macos() -> Int32 = "moonbit_rgsys_is_macos"

///|
extern "C" fn c_ignore_sigpipe() = "moonbit_rgsys_ignore_sigpipe"

///|
extern "C" fn c_host_errno(linux : Int32) -> Int32 = "moonbit_rgsys_host_errno"

///|
extern "C" fn c_strerror(e : Int32) -> Bytes = "moonbit_rgsys_strerror"

///|
/// The host errno number and description for a Linux-style errno, as
/// displayed by Rust's std on this platform.
fn host_error(linux : Int32) -> (Int32, String?) {
  let e = c_host_errno(linux)
  let msg = decode(c_strerror(e))
  (e, if msg.is_empty() { None } else { Some(msg) })
}

///|
/// Makes writes to a closed pipe fail with a broken pipe error instead of
/// terminating the process (like Rust programs, which ignore SIGPIPE).
/// Spawned processes still get the default disposition.
pub fn ignore_sigpipe() -> Unit {
  c_ignore_sigpipe()
}

///|
#borrow(buf)
extern "C" fn c_write_all(
  fd : Int32,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Int32 = "moonbit_rgsys_write_all"

///|
#borrow(buf)
extern "C" fn c_read(
  fd : Int32,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Int32 = "moonbit_rgsys_read"

///|
extern "C" fn c_isatty(fd : Int32) -> Int32 = "moonbit_rgsys_isatty"

///|
extern "C" fn c_stdin_readable() -> Int32 = "moonbit_rgsys_stdin_readable"

///|
#borrow(path)
extern "C" fn c_open_read(path : Bytes) -> Int32 = "moonbit_rgsys_open_read"

///|
extern "C" fn c_close(fd : Int32) = "moonbit_rgsys_close"

///|
#borrow(path, data)
extern "C" fn c_write_file(path : Bytes, data : Bytes, len : Int32) -> Int32 = "moonbit_rgsys_write_file"

///|
#borrow(path)
extern "C" fn c_mkdir(path : Bytes) -> Int32 = "moonbit_rgsys_mkdir"

///|
#borrow(path)
extern "C" fn c_rmdir(path : Bytes) -> Int32 = "moonbit_rgsys_rmdir"

///|
#borrow(path)
extern "C" fn c_unlink(path : Bytes) -> Int32 = "moonbit_rgsys_unlink"

///|
#borrow(target, link)
extern "C" fn c_symlink(target : Bytes, link : Bytes) -> Int32 = "moonbit_rgsys_symlink"

///|
#borrow(path, out)
extern "C" fn c_stat(
  path : Bytes,
  follow : Int32,
  out : FixedArray[Int64],
) -> Int32 = "moonbit_rgsys_stat"

///|
#borrow(path)
extern "C" fn c_readdir(path : Bytes) -> Bytes = "moonbit_rgsys_readdir"

///|
#borrow(path)
extern "C" fn c_readlink(path : Bytes) -> Bytes = "moonbit_rgsys_readlink"

///|
#borrow(path)
extern "C" fn c_realpath(path : Bytes) -> Bytes = "moonbit_rgsys_realpath"

///|
fn last_error() -> IOError {
  IOError::from_errno(c_last_err())
}

///|
/// NUL-terminated UTF-8 encoding of `s` for passing to C.
fn cstr(s : String) -> Bytes raise IOError {
  let b = path_bytes(s)
  let out = FixedArray::make(b.length() + 1, b'\x00')
  out.blit_from_bytes(0, b, 0, b.length())
  out.unsafe_reinterpret_as_bytes()
}

///|
fn decode(b : BytesView) -> String {
  // Lossless: invalid UTF-8 (e.g. in file names) is escaped (see `osstr`).
  @osstr.decode(b)
}

///|
/// Splits a blob of NUL-terminated strings.
fn split_nul(b : Bytes) -> Array[BytesView] {
  let out = []
  let mut start = 0
  for i in 0.. Array[String] {
  [
    for a in c_cli_args() => decode(a)
  ]
}

///|
/// The value of the environment variable `name`, if set.
pub fn env_var(name : String) -> String? {
  let n = cstr(name) catch { _ => return None }
  let b = c_getenv(n)
  if b.length() == 0 {
    None
  } else {
    Some(decode(b[1:]))
  }
}

///|
/// All environment variables.
pub fn env_vars() -> Array[(String, String)] {
  let out = []
  for entry in split_nul(c_environ()) {
    let s = decode(entry)
    match s.find("=") {
      Some(i) =>
        out.push(
          (
            s.unsafe_substring(start=0, end=i),
            s.unsafe_substring(start=i + 1, end=s.length()),
          ),
        )
      None => out.push((s, ""))
    }
  }
  out
}

///|
/// Terminates the process with the given exit code.
pub fn exit(code : Int32) -> Unit {
  c_exit(code)
}

///|
/// The current working directory.
pub fn current_dir() -> String? {
  let b = c_getcwd()
  if c_last_err() != 0 {
    None
  } else {
    Some(decode(b))
  }
}

///|
/// The host name of the machine.
pub fn hostname() -> String? {
  let b = c_gethostname()
  if c_last_err() != 0 {
    None
  } else {
    Some(decode(b))
  }
}

///|
pub fn is_windows() -> Bool {
  false
}

///|
/// Whether ripgrep would search files through memory maps on this platform
/// when asked to (it never does on macOS, where they are slow).
pub fn mmap_supported() -> Bool {
  c_is_macos() == 0
}

///|
/// A monotonic clock reading in nanoseconds.
pub fn now_nanos() -> Int64 {
  c_now_nanos()
}

///|
/// Writes all of `buf[start:start + len]` to `fd`.
pub fn write_fd(
  fd : Int32,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Unit raise IOError {
  check_range(buf.length(), start, len)
  if c_write_all(fd, buf, start, len) < 0 {
    raise last_error()
  }
}

///|
/// Reads up to `len` bytes from `fd` into `buf[start:start + len]`. Returns
/// `0` at end of file.
pub fn read_fd(
  fd : Int32,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Int32 raise IOError {
  check_range(buf.length(), start, len)
  let n = c_read(fd, buf, start, len)
  if n < 0 {
    raise last_error()
  }
  n
}

///|
/// Whether `fd` refers to a terminal.
pub fn is_tty(fd : Int32) -> Bool {
  c_isatty(fd) != 0
}

///|
/// Whether stdin is a file, a pipe/FIFO or a socket (and not a terminal).
pub fn stdin_is_readable() -> Bool {
  c_stdin_readable() != 0
}

///|
fn sys_open_read(path : String) -> Int32 raise IOError {
  let fd = c_open_read(cstr(path))
  if fd < 0 {
    raise last_error()
  }
  fd
}

///|
fn sys_close(fd : Int32) -> Unit {
  c_close(fd)
}

///|
fn sys_mkdir(path : String) -> Unit raise IOError {
  if c_mkdir(cstr(path)) < 0 {
    raise last_error()
  }
}

///|
fn sys_rmdir(path : String) -> Unit raise IOError {
  if c_rmdir(cstr(path)) < 0 {
    raise last_error()
  }
}

///|
/// Writes `data` to the file at `path`, creating or truncating it.
pub fn write_file(path : String, data : Bytes) -> Unit raise IOError {
  if c_write_file(cstr(path), data, data.length()) < 0 {
    raise last_error()
  }
}

///|
/// Removes a file (or symlink).
pub fn remove_file(path : String) -> Unit raise IOError {
  if c_unlink(cstr(path)) < 0 {
    raise last_error()
  }
}

///|
/// Creates a symlink at `link` pointing to `target`.
pub fn symlink(target : String, link : String) -> Unit raise IOError {
  if c_symlink(cstr(target), cstr(link)) < 0 {
    raise last_error()
  }
}

///|
fn kind_to_file_type(kind : Int32) -> FileType? {
  match kind {
    0 => None
    1 => Some(File)
    2 => Some(Dir)
    3 => Some(Symlink)
    _ => Some(Other)
  }
}

///|
fn stat_impl(path : String, follow : Bool) -> Metadata raise IOError {
  let out : FixedArray[Int64] = FixedArray::make(4, 0)
  if c_stat(cstr(path), if follow { 1 } else { 0 }, out) < 0 {
    raise last_error()
  }
  {
    file_type: kind_to_file_type(out[0].to_int()).unwrap_or(Other),
    len: out[1],
    dev: out[2].reinterpret_as_uint64(),
    ino: out[3].reinterpret_as_uint64(),
  }
}

///|
/// The length of the file at `path` (following symlinks).
pub fn file_len(path : String) -> Int64 raise IOError {
  metadata(path).len
}

///|
/// Metadata for `path`, following symlinks.
pub fn metadata(path : String) -> Metadata raise IOError {
  stat_impl(path, true)
}

///|
/// Metadata for `path`, without following a final symlink.
pub fn symlink_metadata(path : String) -> Metadata raise IOError {
  stat_impl(path, false)
}

///|
/// Lists the entries of the directory at `path` (excluding `.` and `..`).
pub fn read_dir(path : String) -> Array[DirEntry] raise IOError {
  let blob = c_readdir(cstr(path))
  if c_last_err() != 0 {
    raise last_error()
  }
  [
    for e in split_nul(blob) => {
      { name: decode(e[1:]), file_type: kind_to_file_type(e[0].to_int()), }
    }
  ]
}

///|
/// The canonical, absolute form of `path` (resolving symlinks).
pub fn canonicalize(path : String) -> String raise IOError {
  let b = c_realpath(cstr(path))
  if c_last_err() != 0 {
    raise last_error()
  }
  decode(b)
}

///|
/// The target of the symlink at `path`.
pub fn read_link(path : String) -> String raise IOError {
  let b = c_readlink(cstr(path))
  if c_last_err() != 0 {
    raise last_error()
  }
  decode(b)
}

///|
#borrow(prog, argblob, stdin_path)
extern "C" fn c_spawn(
  prog : Bytes,
  argblob : Bytes,
  nargs : Int32,
  stdin_path : Bytes,
) -> Int32 = "moonbit_rgsys_spawn"

///|
#borrow(buf)
extern "C" fn c_proc_read(
  h : Int32,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Int32 = "moonbit_rgsys_proc_read"

///|
extern "C" fn c_proc_close_stdout(h : Int32) = "moonbit_rgsys_proc_close_stdout"

///|
extern "C" fn c_proc_wait(h : Int32) -> Int32 = "moonbit_rgsys_proc_wait"

///|
extern "C" fn c_proc_finish(h : Int32) -> Bytes = "moonbit_rgsys_proc_finish"

///|
/// NUL-terminated arguments, concatenated, for passing to C.
fn arg_blob(args : Array[String]) -> FixedArray[Byte] raise IOError {
  let parts = []
  for a in args {
    parts.push(cstr(a))
  }
  let total = parts.fold(init=0, (acc, p) => acc + p.length())
  let argblob = FixedArray::make(total + 1, b'\x00')
  let mut off = 0
  for p in parts {
    argblob.blit_from_bytes(off, p, 0, p.length())
    off += p.length()
  }
  argblob
}

///|
/// Spawns `program` (looked up in `PATH`) with `args`, with its stdout and
/// stderr piped. The child's stdin is the file at `stdin_path`, or
/// `/dev/null`.
pub fn Process::spawn(
  program : String,
  args : Array[String],
  stdin_path? : String,
) -> Process raise IOError {
  let argblob = arg_blob(args)
  let h = c_spawn(
    cstr(program),
    argblob.unsafe_reinterpret_as_bytes(),
    args.length(),
    cstr(stdin_path.unwrap_or("/dev/null")),
  )
  if h < 0 {
    raise last_error()
  }
  { handle: h, finished: false, }
}

///|
/// Reads the child's stdout into `buf[start:start+len]` (collecting its
/// stderr meanwhile). Returns 0 at the end of the output.
pub fn Process::read(
  self : Process,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Int32 raise IOError {
  if self.finished {
    return 0
  }
  check_range(buf.length(), start, len)
  let n = c_proc_read(self.handle, buf, start, len)
  if n < 0 {
    raise last_error()
  }
  n
}

///|
/// Closes the child's stdout, waits for it to exit and returns its exit
/// status (128 + the signal number if it was killed) and its stderr.
pub fn Process::finish(self : Process) -> (Int32, Bytes) raise IOError {
  if self.finished {
    return (0, b"")
  }
  self.finished = true
  c_proc_close_stdout(self.handle)
  let status = c_proc_wait(self.handle)
  let err = if status < 0 { Some(last_error()) } else { None }
  let stderr = c_proc_finish(self.handle)
  if err is Some(e) {
    raise e
  }
  (status, stderr)
}

///|
#borrow(path, out)
extern "C" fn c_file_times(path : Bytes, out : FixedArray[Int64]) -> Int32 = "moonbit_rgsys_file_times"

///|
/// Returns the modification, access and creation times of `path`
/// (following symlinks).
pub fn file_times(path : String) -> FileTimes raise IOError {
  let out : FixedArray[Int64] = FixedArray::make(9, 0)
  if c_file_times(cstr(path), out) < 0 {
    raise last_error()
  }
  let get = (i : Int32) => {
    if out[i + 2] != 0L {
      Some(out[i] * 1000000000L + out[i + 1])
    } else {
      None
    }
  }
  { modified: get(0), accessed: get(3), created: get(6), }
}