///|
/// One citation note from the lightweight claim/quote/url input format.
pub(all) struct CitationNote {
  claim : String
  quote : String
  url : String
  line : Int
} derive(ToJson)

///|
pub extend CitationNote with ToJson::{to_json}

///|
/// Parsed citation notes and line-based input diagnostics.
pub(all) struct CitationNotes {
  notes : Array[CitationNote]
  diagnostics : Array[Diagnostic]
} derive(ToJson)

///|
pub extend CitationNotes with ToJson::{to_json}

///|
/// Parse the lightweight plain-text format. Each record has one `claim:`,
/// `quote:`, and `url:` line; blank lines or `---` separate records.
/// Field values are single-line strings.
pub fn parse_citation_notes(input : String) -> CitationNotes {
  let notes : Array[CitationNote] = []
  let diagnostics : Array[Diagnostic] = []
  let mut claim : String? = None
  let mut quote : String? = None
  let mut url : String? = None
  let mut record_line = 1
  let mut record_invalid = false

  let lines = input.split("\n").to_array()
  for index = 0; index < lines.length(); index = index + 1 {
    let line_number = index + 1
    let raw_line = lines[index]
    let line = raw_line.trim()
    if line == "" || line == "---" {
      finish_citation_note(
        notes, diagnostics, record_line, claim, quote, url, record_invalid,
      )
      claim = None
      quote = None
      url = None
      record_line = line_number + 1
      record_invalid = false
      continue
    }
    if line.has_prefix("#") {
      continue
    }
    if claim is None && quote is None && url is None && !record_invalid {
      record_line = line_number
    }
    match line.split_once(":") {
      None => {
        add_diagnostic(
          diagnostics, line_number, "INVALID_NOTE_LINE", "expected claim:, quote:, or url:",
        )
        record_invalid = true
      }
      Some((key_view, value_view)) => {
        let key = key_view.trim().to_owned()
        let value = value_view.trim().to_owned()
        if value == "" {
          add_diagnostic(
            diagnostics,
            line_number,
            "EMPTY_NOTE_FIELD",
            "field value cannot be empty: " + key,
          )
          record_invalid = true
          continue
        }
        match key {
          "claim" =>
            if claim is Some(_) {
              add_diagnostic(
                diagnostics, line_number, "DUPLICATE_NOTE_FIELD", "duplicate claim field",
              )
              record_invalid = true
            } else {
              claim = Some(value)
            }
          "quote" =>
            if quote is Some(_) {
              add_diagnostic(
                diagnostics, line_number, "DUPLICATE_NOTE_FIELD", "duplicate quote field",
              )
              record_invalid = true
            } else {
              quote = Some(value)
            }
          "url" =>
            if url is Some(_) {
              add_diagnostic(
                diagnostics, line_number, "DUPLICATE_NOTE_FIELD", "duplicate url field",
              )
              record_invalid = true
            } else {
              url = Some(value)
            }
          _ => {
            add_diagnostic(
              diagnostics,
              line_number,
              "UNKNOWN_NOTE_FIELD",
              "unknown note field: " + key,
            )
            record_invalid = true
          }
        }
      }
    }
  }
  finish_citation_note(
    notes, diagnostics, record_line, claim, quote, url, record_invalid,
  )
  if notes.is_empty() && diagnostics.is_empty() {
    add_diagnostic(
      diagnostics, 1, "EMPTY_NOTE_INPUT", "no citation notes found",
    )
  }
  { notes, diagnostics, }
}

///|
fn finish_citation_note(
  notes : Array[CitationNote],
  diagnostics : Array[Diagnostic],
  line : Int,
  claim : String?,
  quote : String?,
  url : String?,
  invalid : Bool,
) -> Unit {
  if claim is None && quote is None && url is None && !invalid {
    return
  }
  if claim is None {
    add_diagnostic(
      diagnostics, line, "MISSING_NOTE_FIELD", "missing claim field",
    )
  }
  if quote is None {
    add_diagnostic(
      diagnostics, line, "MISSING_NOTE_FIELD", "missing quote field",
    )
  }
  if url is None {
    add_diagnostic(diagnostics, line, "MISSING_NOTE_FIELD", "missing url field")
  }
  if !invalid {
    match (claim, quote, url) {
      (Some(claim), Some(quote), Some(url)) =>
        notes.push({ claim, quote, url, line, })
      _ => ()
    }
  }
}