///|
/// How one message's text travels: a line, a WebSocket frame, an HTTP body. Implement this to carry
/// JSON-RPC over anything.
pub(open) trait Frames {
  /// The next message's text, or `None` once the other side has closed.
  async fn read(Self) -> String?
  async fn write(Self, String) -> Unit
}

///|
/// The longest message `lines` and `content_length` take: 64 MiB. vscode-jsonrpc and the MCP SDKs
/// set no bound, gRPC's default is 4 MiB; no common value exists, so this takes the safe side without
/// refusing what a large tool result needs.
pub let limit : Int32 = 67108864

///|
/// Bytes read and not yet handed out. `Reader` buffers too, but gives no way to stop at a bound.
priv struct Held {
  reader : &@io.Reader
  limit : Int32
  mut store : FixedArray[Byte]
  mut start : Int32
  mut end : Int32
  /// Everything before this has been searched, so a long message is not searched again per read.
  mut searched : Int32
}

///|
fn Held::new(reader : &@io.Reader, limit : Int32) -> Held {
  {
    reader,
    limit,
    store: FixedArray::make(4096, b'\x00'),
    start: 0,
    end: 0,
    searched: 0,
  }
}

///|
fn Held::length(self : Held) -> Int32 {
  self.end - self.start
}

///|
/// Read more. False at the end of the input.
async fn Held::fill(self : Held) -> Bool {
  if self.end == self.store.length() {
    let length = self.length()
    let store = if length * 2 > self.store.length() {
      FixedArray::make(self.store.length() * 2, b'\x00')
    } else {
      self.store
    }
    FixedArray::blit_to(self.store, store, len=length, src_offset=self.start)
    self.store = store
    self.searched -= self.start
    self.start = 0
    self.end = length
  }
  let got = self.reader.read(
    self.store,
    offset=self.end,
    max_len=self.store.length() - self.end,
  )
  self.end += got
  got > 0
}

///|
/// How far `mark` is from the next byte to hand out.
fn Held::find(self : Held, mark : Bytes) -> Int32? {
  let last = self.end - mark.length()
  let mut at = @cmp.maximum(self.start, self.searched)
  while at <= last {
    let mut same = true
    for i in 0.. Bytes {
  let out = Bytes::makei(n, i => self.store[self.start + i])
  self.start += n + skip
  self.searched = self.start
  out
}

///|
fn Held::too_long(self : Held, got : Int32) -> Unit raise @moonjrpc.Failed {
  if got > self.limit {
    raise @moonjrpc.TooLong(limit=self.limit, got=got.to_int64())
  }
}

///|
/// Text that is not valid UTF-8 gets the same answer as text that is not JSON, so it comes through as
/// something no JSON parser takes.
fn text_of(bytes : Bytes) -> String {
  @utf8.decode(bytes) catch {
    _ => "\u{FFFD}"
  }
}

///|
priv struct Lines {
  held : Held
  writer : &@io.Writer
}

///|
impl Frames for Lines with fn read(self) {
  let held = self.held
  while true {
    match held.find(b"\n") {
      Some(at) => {
        held.too_long(at)
        let text = text_of(held.take(at, 1)).trim(chars=" \t\r").to_owned()
        if !text.is_empty() {
          return Some(text)
        }
      }
      None => {
        held.too_long(held.length())
        if !held.fill() {
          let text = text_of(held.take(held.length(), 0))
            .trim(chars=" \t\r")
            .to_owned()
          return if text.is_empty() { None } else { Some(text) }
        }
      }
    }
  }
  None
}

///|
impl Frames for Lines with fn write(self, text) {
  self.writer.write("\{text}\n")
}

///|
/// One message to a line: the stdio transport of MCP and ACP. Blank lines are skipped, a last line
/// with no newline still counts, and a line over `limit` bytes raises `TooLong`.
pub fn lines(
  reader : &@io.Reader,
  writer : &@io.Writer,
  limit~ : Int32 = limit,
) -> &Frames {
  ({ held: Held::new(reader, limit), writer, } : Lines)
}

///|
priv struct ContentLength {
  held : Held
  writer : &@io.Writer
}

///|
/// The `Content-Length` of a header block. The name is matched in any case; the other headers, such
/// as `Content-Type`, do not change how the body is read.
fn length_of(headers : Bytes) -> Int64 raise @moonjrpc.Failed {
  for line in text_of(headers).split("\r\n") {
    guard line.find(":") is Some(at) else {
      raise @moonjrpc.Malformed("header without a colon: \{line}")
    }
    if line[:at].to_lower() == "content-length" {
      let digits = line[at + 1:].trim(chars=" \t")
      guard !digits.is_empty() && digits.length() <= 10 else {
        raise @moonjrpc.Malformed("bad Content-Length: \{line}")
      }
      let mut length = 0L
      for digit in digits {
        guard digit is ('0'..<='9') else {
          raise @moonjrpc.Malformed("bad Content-Length: \{line}")
        }
        length = length * 10 + (digit.to_int() - '0'.to_int()).to_int64()
      }
      return length
    }
  }
  raise @moonjrpc.Malformed("no Content-Length")
}

///|
impl Frames for ContentLength with fn read(self) {
  let held = self.held
  let mark = b"\r\n\r\n"
  let end = while true {
    match held.find(mark) {
      Some(at) => break at
      None => {
        held.too_long(held.length())
        if !held.fill() {
          if held.length() == 0 {
            return None
          }
          raise @moonjrpc.Malformed("the input ended inside the headers")
        }
      }
    }
  } nobreak {
    return None
  }
  held.too_long(end)
  let length = length_of(held.take(end, mark.length()))
  if length > held.limit.to_int64() {
    raise @moonjrpc.TooLong(limit=held.limit, got=length)
  }
  let length = length.to_int()
  while held.length() < length {
    if !held.fill() {
      raise @moonjrpc.Malformed("the input ended inside a message")
    }
  }
  Some(text_of(held.take(length, 0)))
}

///|
impl Frames for ContentLength with fn write(self, text) {
  let body = @utf8.encode(text)
  self.writer.write("Content-Length: \{body.length()}\r\n\r\n")
  self.writer.write(body)
}

///|
/// A `Content-Length` header, a blank line, the body: the base protocol of LSP. A message over `limit`
/// bytes raises `TooLong`.
pub fn content_length(
  reader : &@io.Reader,
  writer : &@io.Writer,
  limit~ : Int32 = limit,
) -> &Frames {
  ({ held: Held::new(reader, limit), writer, } : ContentLength)
}