///|
/// A fully-framed Server-Sent Event.
///
/// Per the SSE spec, an event is terminated by a blank line and may carry
/// multiple `data:` lines (concatenated with newlines), an `event:` type,
/// an `id:`, and a `retry:` value.
pub(all) struct SSEEvent {
  event : String?
  data : String
  id : String?
  retry : Int?
} derive(Eq, Debug)

///|
/// A stateful Server-Sent Events framer.
///
/// Feed it lines (without worrying about trailing CR/LF) via `push_line`;
/// it returns a complete `SSEEvent` whenever a blank line terminates the
/// current event. This correctly handles multi-line `data:` payloads, which
/// the naive one-line-per-event approach cannot.
pub struct SSEParser {
  mut event : String?
  data : StringBuilder
  mut id : String?
  mut retry : Int?
  mut has_data : Bool
}

///|
/// Create a fresh SSE parser.
pub fn SSEParser::new() -> SSEParser {
  {
    event: None,
    data: StringBuilder::new(),
    id: None,
    retry: None,
    has_data: false,
  }
}

///|
/// Reset internal state after emitting an event.
fn SSEParser::reset(self : SSEParser) -> Unit {
  self.event = None
  self.data.reset()
  self.id = None
  self.retry = None
  self.has_data = false
}

///|
/// Feed one line into the parser (the trailing newline may be included or not).
///
/// Returns `Some(event)` when this line (a blank line) terminates an event,
/// otherwise `None`.
pub fn SSEParser::push_line(self : SSEParser, raw : String) -> SSEEvent? {
  // Strip a single trailing "\r" and/or "\n".
  let mut end = raw.length()
  if end > 0 && raw[end - 1] == '\n' {
    end = end - 1
  }
  if end > 0 && raw[end - 1] == '\r' {
    end = end - 1
  }
  let line = raw[:end].to_owned()

  // A blank line dispatches the accumulated event.
  if line.length() == 0 {
    if !self.has_data &&
      self.event is None &&
      self.id is None &&
      self.retry is None {
      // Nothing accumulated; ignore stray blank lines.
      return None
    }
    let ev = {
      event: self.event,
      data: self.data.to_string(),
      id: self.id,
      retry: self.retry,
    }
    self.reset()
    return Some(ev)
  }

  // Comment line.
  if line.has_prefix(":") {
    return None
  }

  // Split into field / value at the first colon.
  let (field, value) = match line.find(":") {
    Some(idx) => {
      let f = line[:idx].to_owned()
      let mut v = line[idx + 1:].to_owned()
      if v.has_prefix(" ") {
        v = v[1:].to_owned()
      }
      (f, v)
    }
    None => (line, "")
  }
  match field {
    "event" => self.event = Some(value)
    "data" => {
      if self.has_data {
        self.data.write_string("\n")
      }
      self.data.write_string(value)
      self.has_data = true
    }
    "id" => self.id = Some(value)
    "retry" => self.retry = Some(@string.parse_int(value)) catch { _ => None }
    _ => () // unknown field: ignore per spec
  }
  None
}