///|
/// Incremental NDJSON reader. Give it input one line at a time with
/// `read_line`, or in chunks of any size with `feed`; it keeps track of
/// line numbers so errors name the bad line.
pub struct NdjsonReader {
  priv mut line_number : Int
  priv mut pending : String
}

///|
/// Create a reader positioned before line 1.
pub fn NdjsonReader::new() -> NdjsonReader {
  { line_number: 0, pending: "", }
}

///|
/// Number of lines read so far.
pub fn NdjsonReader::line_number(self : NdjsonReader) -> Int {
  self.line_number
}

///|
/// Parse the next line of input (without its `\n`). A blank line gives
/// `None`. A bad line raises `NdjsonParseError` with its line number.
pub fn NdjsonReader::read_line(
  self : NdjsonReader,
  line : StringView,
) -> Envelope? raise NdjsonParseError {
  self.line_number += 1
  let trimmed = line.trim()
  if trimmed.length() == 0 {
    return None
  }
  Some(
    parse_ndjson_line(trimmed.to_owned()) catch {
      cause => raise NdjsonParseError(line=self.line_number, cause~)
    },
  )
}

///|
/// Parse every complete line in `chunk`. A partial last line is kept until
/// the next `feed` or `finish`.
pub fn NdjsonReader::feed(
  self : NdjsonReader,
  chunk : String,
) -> Array[Envelope] raise NdjsonParseError {
  let pieces = (self.pending + chunk).split("\n").to_array()
  let envelopes : Array[Envelope] = []
  for i in 0..<(pieces.length() - 1) {
    if self.read_line(pieces[i]) is Some(envelope) {
      envelopes.push(envelope)
    }
  }
  self.pending = pieces[pieces.length() - 1].to_owned()
  envelopes
}

///|
/// Parse the last line when the input does not end with `\n`.
pub fn NdjsonReader::finish(
  self : NdjsonReader,
) -> Envelope? raise NdjsonParseError {
  let rest = self.pending
  self.pending = ""
  if rest.length() == 0 {
    return None
  }
  self.read_line(rest)
}

///|
/// Write this envelope as one NDJSON line, ending with `\n`.
pub fn Envelope::write_ndjson(self : Envelope, logger : &Logger) -> Unit {
  logger.write_string(self.to_ndjson_line())
  logger.write_char('\n')
}

///|
test "NdjsonReader reads envelopes from chunks split mid-line" {
  let input =
    #|{"testRunStarted":{"timestamp":{"seconds":1,"nanos":0}}}
    #|
    #|{"testRunFinished":{"success":true,"timestamp":{"seconds":2,"nanos":0}}}
  let reader = NdjsonReader::new()
  let envelopes : Array[Envelope] = []
  let mut start = 0
  while start < input.length() {
    let end = if start + 7 < input.length() {
      start + 7
    } else {
      input.length()
    }
    envelopes.append(
      reader.feed(input.view(start_offset=start, end_offset=end).to_owned()),
    )
    start = end
  }
  if reader.finish() is Some(envelope) {
    envelopes.push(envelope)
  }
  assert_eq(envelopes.length(), 2)
  assert_true(envelopes[1] is TestRunFinished(_))
}

///|
test "NdjsonReader reports line numbers across chunks" {
  let reader = NdjsonReader::new()
  ignore(
    reader.feed(
      "{\"testRunStarted\":{\"timestamp\":{\"seconds\":1,\"nanos\":0}}}\n\n{ba",
    ),
  )
  let line = try reader.feed("d}\n") catch {
    NdjsonParseError(line~, ..) => line
  } noraise {
    _ => 0
  }
  assert_eq(line, 3)
}

///|
test "NdjsonReader reads one line at a time" {
  let reader = NdjsonReader::new()
  assert_true(reader.read_line("  ") is None)
  assert_true(
    reader.read_line(
      "{\"testRunStarted\":{\"timestamp\":{\"seconds\":1,\"nanos\":0}}}",
    )
    is Some(TestRunStarted(_)),
  )
  assert_eq(reader.line_number(), 2)
}

///|
test "Envelope::write_ndjson writes one line per envelope" {
  let envelopes = parse_ndjson(
    "{\"testRunStarted\":{\"timestamp\":{\"seconds\":1,\"nanos\":0}}}\n{\"testRunFinished\":{\"success\":true,\"timestamp\":{\"seconds\":2,\"nanos\":0}}}\n",
  )
  let buf = StringBuilder()
  for envelope in envelopes {
    envelope.write_ndjson(buf)
  }
  assert_eq(buf.to_string(), envelopes_to_ndjson(envelopes))
}

///|
test "NdjsonReader handles non-BMP characters split across chunks" {
  let line = "{\"source\":{\"uri\":\"😀.feature\",\"data\":\"😀😀\",\"mediaType\":\"text/x.cucumber.gherkin+plain\"}}"
  let reader = NdjsonReader::new()
  let envelopes = reader.feed(
    line + "\n" + line + "\n" + line.view(end_offset=20).to_owned(),
  )
  envelopes.append(reader.feed(line.view(start_offset=20).to_owned() + "\n"))
  assert_eq(envelopes.length(), 3)
  guard! envelopes[2] is Source(s)
  assert_eq(s.data, "😀😀")
}