///|
/// Incremental UTF-16 text framing for newline-delimited stream commands.
/// CRLF and blank lines are accepted. Parsing and reader state commit together.
pub struct CommandReader {
  mut partial : String
  mut line : Int32
  mut closed : Bool
  max_line : Int32
  max_commands : Int32
}

///|
pub fn CommandReader::new(
  max_line~ : Int32 = 1048576,
  max_commands~ : Int32 = 4096,
) -> CommandReader raise CepError {
  if max_line < 1 || max_line > 1048576 {
    raise Invalid(
      path="reader.max_line",
      reason="expected 1..1048576 UTF-16 units",
    )
  }
  if max_commands < 1 || max_commands > 100000 {
    raise Invalid(path="reader.max_commands", reason="expected 1..100000")
  }
  { partial: "", line: 1, closed: false, max_line, max_commands, }
}

///|
fn read_command_line(
  text : String,
  line : Int32,
) -> StreamInput? raise CepError {
  let stripped = text.trim()
  if stripped.length() == 0 {
    return None
  }
  Some(parse_command(stripped.to_owned())) catch {
    Invalid(path~, reason~) =>
      raise Invalid(path="line[\{line}].\{path}", reason~)
    error => raise error
  }
}

///|
pub fn CommandReader::feed(
  self : CommandReader,
  chunk : String,
) -> Array[StreamInput] raise CepError {
  if self.closed {
    raise Invalid(path="reader", reason="reader is already finished")
  }
  let mut builder = StringBuilder()
  builder.write_string(self.partial)
  let mut units = self.partial.length()
  let mut line = self.line
  let commands = []
  let mut segment = 0
  for index in 0..= self.max_commands {
          raise Resource(resource="commands per chunk", limit=self.max_commands)
        }
        commands.push(command)
      }
      builder = StringBuilder()
      units = 0
      if line == 2147483647 {
        raise Invalid(path="reader.line", reason="line counter exhausted")
      }
      line += 1
      segment = index + 1
    } else {
      if units >= self.max_line {
        raise Resource(resource="line UTF-16 units", limit=self.max_line)
      }
      units += 1
    }
  }
  builder.write_view(chunk[segment:])
  self.partial = builder.to_string()
  self.line = line
  commands
}

///|
pub fn CommandReader::finish(
  self : CommandReader,
) -> Array[StreamInput] raise CepError {
  if self.closed {
    return []
  }
  let result = match read_command_line(self.partial, self.line) {
    Some(command) => [command]
    None => []
  }
  self.partial = ""
  self.closed = true
  result
}

///|
pub fn CommandReader::pending_units(self : CommandReader) -> Int32 {
  self.partial.length()
}

///|
pub fn CommandReader::line(self : CommandReader) -> Int32 {
  self.line
}

///|
pub fn CommandReader::is_finished(self : CommandReader) -> Bool {
  self.closed
}

///|
pub fn CommandReader::fork(self : CommandReader) -> CommandReader {
  { ..self, }
}

///|
/// Chunk a complete Unicode string without discarding a scalar at a UTF-16
/// boundary. A two-unit scalar can exceed a requested size of one by one unit.
pub fn command_chunks(
  text : String,
  max_units~ : Int32 = 65536,
) -> Iter[StringView] raise CepError {
  if max_units < 1 || max_units > 1048576 {
    raise Invalid(path="chunks.max_units", reason="expected 1..1048576")
  }
  let mut position = 0
  Iter::new(fn() -> StringView? {
    if position == text.length() {
      return None
    }
    let size = max_units.min(text.length() - position)
    let mut view = text.clamped_view(start=position, end=position + size)
    if view.length() == 0 {
      view = text.exact_view(start=position, end=position + 2)
    }
    position += view.length()
    Some(view)
  })
}