// Sending a request body. Bytes and streams go out the same way: in writes of
// at most `max_write_bytes`, each its own `Write` phase, so `write_timeout_ms`
// bounds the time one slice takes to leave, as httpx's write timeout does, and
// the exchange deadline bounds the whole upload.

///|
/// The most bytes one write puts on a connection. A chunk from the caller's
/// stream, or a body held in memory, is cut to this size, so one `Write`
/// phase sends at most 64 KiB however large the chunks the stream returns.
let max_write_bytes : Int32 = 64 * 1024

///|
/// The `Content-Length` to declare for `body`, or `None` to leave the framing
/// to the client: chunked once bytes are written, nothing for an empty body.
/// A known length is declared; an empty one only for a method that expects
/// content (RFC 9110 ยง8.6: a user agent should not send `Content-Length` when
/// there is no content and the method does not anticipate any).
fn content_length(http_method : String, body : @http.Body) -> Int64? {
  match body.length() {
    Some(0L) =>
      if http_method.to_upper() is ("POST" | "PUT" | "PATCH") {
        Some(0L)
      } else {
        None
      }
    length => length
  }
}

///|
/// The `Protocol` message for a stream body of `bytes` bytes on a platform
/// that buffers stream bodies up to `limit` bytes, or `None` when it fits.
fn buffer_overflow(limit : Int64?, bytes : Int64) -> String? {
  guard limit is Some(limit) && bytes > limit else { return None }
  Some(
    "a stream request body is buffered in memory on this platform, and this one exceeds \{limit} bytes; send it as bytes instead",
  )
}

///|
/// Raises `Protocol`, before anything is sent, for a stream body declared
/// longer than a buffering platform's `limit`.
fn refuse_unbufferable(
  body : @http.Body,
  limit : Int64?,
) -> Unit raise @http.HttpError {
  guard body is Stream(stream) else { return }
  if buffer_overflow(limit, stream.length().unwrap_or(0L)) is Some(message) {
    raise @http.HttpError::Protocol(message)
  }
}

///|
/// A request body ready to send: bytes, or a stream already taken.
priv enum Outgoing {
  Buffered(Bytes)
  Reader(&@http.BodyStream)
}

///|
/// Takes `body`'s stream, if it has one, so a body that cannot be sent fails
/// before any byte of the request goes out.
fn open_body(body : @http.Body) -> Outgoing raise @http.HttpError {
  match body {
    Bytes(bytes) => Buffered(bytes)
    Stream(stream) => Reader(stream.take())
  }
}

///|
/// Closes a taken stream; nothing to do for bytes.
fn Outgoing::close(self : Outgoing) -> Unit {
  if self is Reader(reader) {
    reader.close()
  }
}

///|
/// Reads a stream declared empty to its end, as `Write` phases of `timer`.
/// The taken stream raises `Protocol` if it produces any byte.
async fn confirm_empty(
  outgoing : Outgoing,
  timer : PhaseTimer,
) -> Unit raise @http.HttpError {
  if outgoing is Reader(reader) {
    while timer.run(Write, () => reader.read_some()) is Some(_) {

    }
  }
}

///|
/// Sends `outgoing` through `write` as `Write` phases of `timer`: each slice
/// of at most `max_write_bytes`, and for a stream each read from it, is one
/// phase. A stream's chunk is written once the next read has returned. A stream that grows past `buffer_limit` raises `Protocol`. The
/// caller closes `outgoing`.
async fn send_outgoing(
  outgoing : Outgoing,
  write : async (BytesView) -> Unit,
  timer : PhaseTimer,
  buffer_limit~ : Int64?,
  first? : Bytes?,
) -> Unit raise @http.HttpError {
  match outgoing {
    Buffered(bytes) => write_slices(bytes, write, timer)
    Reader(reader) => {
      // One chunk is held back until the next read succeeds, so the bytes
      // that complete a body go out only once the stream has ended cleanly:
      // a stream that fails at its end fails before the request is whole.
      // `first` is a first read already made, used before reading on.
      let mut sent = 0L
      let mut held : Bytes? = None
      let mut pending = first
      for ;; {
        let next = match pending {
          Some(read) => {
            pending = None
            read
          }
          None => timer.run(Write, () => reader.read_some())
        }
        guard next is Some(chunk) else { break }
        sent += chunk.length().to_int64()
        if buffer_overflow(buffer_limit, sent) is Some(message) {
          raise @http.HttpError::Protocol(message)
        }
        if chunk.is_empty() {
          continue
        }
        if held is Some(previous) {
          write_slices(previous, write, timer)
        }
        held = Some(chunk)
      }
      if held is Some(last) {
        write_slices(last, write, timer)
      }
    }
  }
}

///|
/// Writes `bytes` in slices of at most `max_write_bytes`, each one `Write`
/// phase of `timer`.
async fn write_slices(
  bytes : Bytes,
  write : async (BytesView) -> Unit,
  timer : PhaseTimer,
) -> Unit raise @http.HttpError {
  for start = 0; start < bytes.length(); start = start + max_write_bytes {
    let slice = bytes[start:(start + max_write_bytes).min(bytes.length())]
    timer.run(Write, () => write(slice))
  }
}