///|
#external
type OutputHandle

///|
#borrow(path)
extern "C" fn output_open(path : Bytes) -> OutputHandle = "smithy_output_open"

///|
extern "C" fn output_stdio(fd : Int32) -> OutputHandle = "smithy_output_stdio"

///|
extern "C" fn output_valid(handle : OutputHandle) -> Bool = "smithy_output_valid"

///|
#borrow(bytes)
extern "C" fn output_write(
  handle : OutputHandle,
  bytes : Bytes,
  size : Int32,
) -> Bool = "smithy_output_write"

///|
extern "C" fn output_flush(handle : OutputHandle) -> Bool = "smithy_output_flush"

///|
extern "C" fn output_close(handle : OutputHandle) -> Bool = "smithy_output_close"

///|
fn native_sink(handle : OutputHandle, owned : Bool) -> OutputSink raise {
  guard output_valid(handle) else { raise CliError("could not open output", 1) }
  {
    write: bytes => {
      guard output_write(handle, bytes, bytes.length()) else {
        raise CliError("could not write output", 1)
      }
    },
    flush: () => {
      guard output_flush(handle) else {
        raise CliError("could not flush output", 1)
      }
    },
    close: () => {
      if owned && !output_close(handle) {
        raise CliError("could not close output", 1)
      }
    },
    flush_records: !owned,
  }
}

///|
fn output_sink(path : String?) -> OutputSink raise {
  match path {
    None => native_sink(output_stdio(1), false)
    Some(path) => {
      guard !path.contains("\u0000") else {
        raise CliError("invalid output path", 1)
      }
      native_sink(output_open(@utf8.encode(path + "\u0000")), true)
    }
  }
}

///|
fn stderr_sink() -> OutputSink raise {
  native_sink(output_stdio(2), false)
}