// Code to write HTTP data (port of h11/_writers.py).
//
// Each writer takes an event and appends the bytes to send to `out`.

///|
priv struct ContentLengthWriter {
  mut length : Int64
}

///|
/// Writers for message bodies (WRITERS[SEND_BODY] in Python). Message heads
/// need no writer state: `write_request` and `write_any_response` are
/// chosen directly from the event.
priv enum BodyWriter {
  ContentLength(ContentLengthWriter)
  Chunked
  Http10
}

///|
/// WRITERS[SEND_BODY][framing](*args)
fn writer_for_framing(framing : Framing) -> BodyWriter {
  match framing {
    ContentLength(length, ..) => ContentLength({ length, })
    Chunked => Chunked
    Http10 => Http10
  }
}

///|
fn write_headers(headers : Headers, out : Array[Bytes]) -> Unit {
  // "Since the Host field-value is critical information for handling a
  // request, a user agent SHOULD generate Host as the first header field
  // following the request-line." - RFC 7230
  for item in headers.full_items {
    let (raw_name, name, value) = item
    if name == b"host" {
      out.push(bytes_concat([raw_name, b": ", value, b"\r\n"]))
    }
  }
  for item in headers.full_items {
    let (raw_name, name, value) = item
    if name != b"host" {
      out.push(bytes_concat([raw_name, b": ", value, b"\r\n"]))
    }
  }
  out.push(b"\r\n")
}

///|
fn write_request(
  request : Request,
  out : Array[Bytes],
) -> Unit raise ProtocolError {
  if request.http_version != b"1.1" {
    raise local_error("I only send HTTP/1.1")
  }
  out.push(
    bytes_concat([request.method_, b" ", request.target, b" HTTP/1.1\r\n"]),
  )
  write_headers(request.headers, out)
}

///|
/// Shared between InformationalResponse and Response.
fn write_any_response(
  status_code : Int,
  headers : Headers,
  http_version : Bytes,
  reason : Bytes,
  out : Array[Bytes],
) -> Unit raise ProtocolError {
  if http_version != b"1.1" {
    raise local_error("I only send HTTP/1.1")
  }
  let status_bytes = @utf8.encode(status_code.to_string())
  // We don't bother sending ascii status messages like "OK"; they're
  // optional and ignored by the protocol. (But the space after the numeric
  // status code is mandatory.)
  out.push(bytes_concat([b"HTTP/1.1 ", status_bytes, b" ", reason, b"\r\n"]))
  write_headers(headers, out)
}

///|
// These are careful not to do anything to `data` except take its length and
// push it to `out`, so `send_with_data_passthrough` hands back the very same
// `Bytes` object.
fn BodyWriter::send_data(
  self : BodyWriter,
  data : Bytes,
  out : Array[Bytes],
) -> Unit raise ProtocolError {
  match self {
    ContentLength(w) => {
      w.length -= data.length().to_int64()
      if w.length < 0L {
        raise local_error("Too much data for declared Content-Length")
      }
      out.push(data)
    }
    Chunked =>
      // if we encoded 0-length data in the naive way, it would look like an
      // end-of-message.
      if !data.is_empty() {
        out.push(
          bytes_concat([
            @utf8.encode(data.length().to_string(radix=16)),
            b"\r\n",
          ]),
        )
        out.push(data)
        out.push(b"\r\n")
      }
    Http10 => out.push(data)
  }
}

///|
fn BodyWriter::send_eom(
  self : BodyWriter,
  headers : Headers,
  out : Array[Bytes],
) -> Unit raise ProtocolError {
  match self {
    ContentLength(w) => {
      if w.length != 0L {
        raise local_error("Too little data for declared Content-Length")
      }
      if !headers.is_empty() {
        raise local_error("Content-Length and trailers don't mix")
      }
    }
    Chunked => {
      out.push(b"0\r\n")
      write_headers(headers, out)
    }
    Http10 =>
      if !headers.is_empty() {
        raise local_error("can't send trailers to HTTP/1.0 client")
      }
    // no need to close the socket ourselves, that will be taken care of by
    // Connection: close machinery
  }
}