///|
/// Opens a file for reading.
pub fn File::open(path : String) -> File raise IOError {
  { fd: sys_open_read(path), closed: false, }
}

///|
/// Reads up to `len` bytes into `buf[start:start + len]`. Returns the number
/// of bytes read, which is `0` at end of file.
pub fn File::read(
  self : File,
  buf : FixedArray[Byte],
  start : Int32,
  len : Int32,
) -> Int32 raise IOError {
  if self.closed {
    raise IOError::from_errno(9)
  }
  read_fd(self.fd, buf, start, len)
}

///|
/// Closes the file. Closing an already closed file does nothing.
pub fn File::close(self : File) -> Unit {
  if !self.closed {
    self.closed = true
    sys_close(self.fd)
  }
}

///|
/// Reads the whole file at `path`.
pub fn read_file(path : String) -> Bytes raise IOError {
  let file = File::open(path)
  defer file.close()
  read_to_end(file.fd)
}

///|
/// Reads everything from `fd` until end of file.
fn read_to_end(fd : Int32) -> Bytes raise IOError {
  let chunk : FixedArray[Byte] = FixedArray::make(65536, 0)
  let out = Buffer()
  for ;; {
    let n = read_fd(fd, chunk, 0, chunk.length())
    if n == 0 {
      break
    }
    out.write_bytesview(chunk.unsafe_reinterpret_as_bytes()[0:n])
  }
  out.to_bytes()
}

///|
/// Returns true if `path` exists (following symlinks).
pub fn path_exists(path : String) -> Bool {
  try metadata(path) catch {
    _ => return false
  } noraise {
    _ => true
  }
}

///|
fn is_dir_path(path : String) -> Bool {
  try metadata(path) catch {
    _ => return false
  } noraise {
    md => md.file_type is Dir
  }
}

///|
/// Returns the parent of `path` (lexically), or `None` if `path` has no
/// parent component (e.g. `"foo"` or `"/"`).
fn parent_path(path : String) -> String? {
  let mut end = path.length()
  while end > 1 && path[end - 1] == '/' {
    end -= 1
  }
  for i = end - 1; i >= 0; i = i - 1 {
    if path[i] == '/' {
      let mut j = i
      while j > 0 && path[j - 1] == '/' {
        j -= 1
      }
      return Some(
        if j == 0 {
          "/"
        } else {
          path.unsafe_substring(start=0, end=j)
        },
      )
    }
  }
  None
}

///|
/// Recursively creates a directory and all of its missing parents.
pub fn create_dir_all(path : String) -> Unit raise IOError {
  if path == "" || is_dir_path(path) {
    return
  }
  try sys_mkdir(path) catch {
    e if e.kind() is NotFound => ()
    e => if is_dir_path(path) { return } else { raise e }
  } noraise {
    _ => return
  }
  match parent_path(path) {
    Some(parent) if parent != path => create_dir_all(parent)
    _ => raise IOError::other(Other, "failed to create whole tree")
  }
  sys_mkdir(path) catch {
    e => if !is_dir_path(path) { raise e }
  }
}

///|
/// Removes a directory after removing all of its contents. Symlinks are
/// removed, not followed.
pub fn remove_dir_all(path : String) -> Unit raise IOError {
  let md = symlink_metadata(path)
  if md.file_type is Dir {
    remove_dir_contents(path)
    sys_rmdir(path)
  } else {
    remove_file(path)
  }
}

///|
fn remove_dir_contents(path : String) -> Unit raise IOError {
  for entry in read_dir(path) {
    let child = join_path(path, entry.name)
    let ft = match entry.file_type {
      Some(ft) => ft
      None => symlink_metadata(child).file_type
    }
    if ft is Dir {
      remove_dir_contents(child)
      sys_rmdir(child)
    } else {
      remove_file(child)
    }
  }
}

///|
fn join_path(dir : String, name : String) -> String {
  if dir.has_suffix("/") {
    dir + name
  } else {
    dir + "/" + name
  }
}

///|
fn check_range(
  buf_len : Int32,
  start : Int32,
  len : Int32,
) -> Unit raise IOError {
  if start < 0 || len < 0 || start > buf_len - len {
    raise IOError::other(InvalidInput, "buffer range out of bounds")
  }
}

///|
/// Encodes `s` for the OS (as UTF-8, restoring bytes escaped by `osstr`),
/// rejecting interior NUL characters (like Rust does for paths passed to the
/// OS).
fn path_bytes(s : String) -> Bytes raise IOError {
  for c in s {
    if c == '\u{0}' {
      raise IOError::other(
        InvalidInput,
        "file name contained an unexpected NUL byte",
      )
    }
  }
  @osstr.encode(s)
}

///|
/// Runs `program` (looked up in `PATH`) with `args` to completion,
/// capturing its stdout and stderr. The child's stdin is the file at
/// `stdin_path`, or `/dev/null`.
pub fn run_command(
  program : String,
  args : Array[String],
  stdin_path? : String,
) -> ProcessOutput raise IOError {
  let child = Process::spawn(program, args, stdin_path?)
  let out = Buffer()
  let chunk = FixedArray::make(65536, b'\x00')
  for ;; {
    let n = child.read(chunk, 0, chunk.length())
    if n == 0 {
      break
    }
    out.write_bytes(chunk.unsafe_reinterpret_as_bytes()[0:n])
  }
  let (status, stderr) = child.finish()
  { status, stdout: out.to_bytes(), stderr, }
}