///|
/// A shared diagnostic sink for bounded DOCX reads. Reader messages may
/// contain attacker-controlled XML names and relationship targets, so both
/// retained message count and retained text are bounded before insertion into
/// the stable deduplication sets.
priv struct ReaderDiagnosticCollector {
  messages : Array[Message]
  warning_texts : StableStringSet
  error_texts : StableStringSet
  max_messages : Int
  max_chars : Int
  mut received_count : Int
  mut truncated : Bool
}

///|
priv enum ReaderDiagnosticSeverity {
  ReaderWarning
  ReaderError
}

///|
fn ReaderDiagnosticCollector::new(
  max_messages : Int,
  max_chars : Int,
) -> ReaderDiagnosticCollector {
  {
    messages: [],
    warning_texts: SortedSet([]),
    error_texts: SortedSet([]),
    max_messages: max_messages.max(0),
    max_chars: max_chars.max(0),
    received_count: 0,
    truncated: false,
  }
}

///|
fn bounded_reader_diagnostic_text(value : String, maximum : Int) -> String {
  bounded_reader_diagnostic_parts([value], maximum)
}

///|
/// Concatenates diagnostic fragments while retaining at most `maximum`
/// Unicode scalars. The input is traversed only until the bound plus one
/// character is known, so a large relationship target or XML token is never
/// copied in full merely to truncate it afterward.
fn bounded_reader_diagnostic_parts(
  parts : ArrayView[String],
  maximum : Int,
) -> String {
  if maximum <= 0 {
    return ""
  }
  let retained : Array[Char] = []
  let mut truncated = false
  for part in parts {
    if truncated {
      break
    }
    for character in part {
      if retained.length() >= maximum {
        truncated = true
        break
      }
      retained.push(character)
    }
  }
  let output = StringBuilder::new()
  let content_length = if truncated { maximum - 1 } else { retained.length() }
  for index in 0.. ignore
  }
  if truncated {
    output.write_string("…") |> ignore
  }
  output.to_string()
}

///|
fn reader_diagnostic_text(parts : ArrayView[String]) -> String {
  let output = StringBuilder::new()
  for part in parts {
    output.write_string(part) |> ignore
  }
  output.to_string()
}

///|
fn ReaderDiagnosticCollector::add_parts(
  self : ReaderDiagnosticCollector,
  severity : ReaderDiagnosticSeverity,
  parts : ArrayView[String],
) -> Unit {
  // Count attempted diagnostics, including duplicates and messages rejected by
  // the retention cap. Callers can then tell whether a nested operation
  // already diagnosed its own failure without depending on retained arrays.
  self.received_count += 1
  let bounded = bounded_reader_diagnostic_parts(parts, self.max_chars)
  let seen = match severity {
    ReaderWarning => self.warning_texts
    ReaderError => self.error_texts
  }
  if seen.contains(bounded) {
    return
  }
  if self.max_messages <= 0 || self.messages.length() >= self.max_messages - 1 {
    self.truncated = true
    return
  }
  // Only accepted diagnostics enter a set, keeping set growth under the same
  // cardinality bound as the retained message array.
  seen.add(bounded)
  self.messages.push(
    match severity {
      ReaderWarning => Warning(bounded)
      ReaderError => Error(bounded)
    },
  )
}

///|
fn push_reader_message_parts(
  target : Array[Message],
  collector : ReaderDiagnosticCollector?,
  severity : ReaderDiagnosticSeverity,
  parts : ArrayView[String],
) -> Unit {
  match collector {
    Some(value) => value.add_parts(severity, parts)
    None => {
      let text = reader_diagnostic_text(parts)
      target.push(
        match severity {
          ReaderWarning => Warning(text)
          ReaderError => Error(text)
        },
      )
    }
  }
}

///|
fn push_reader_warning_parts(
  target : Array[Message],
  collector : ReaderDiagnosticCollector?,
  parts : ArrayView[String],
) -> Unit {
  push_reader_message_parts(target, collector, ReaderWarning, parts)
}

///|
fn push_reader_error_parts(
  target : Array[Message],
  collector : ReaderDiagnosticCollector?,
  parts : ArrayView[String],
) -> Unit {
  push_reader_message_parts(target, collector, ReaderError, parts)
}

///|
fn ReaderDiagnosticCollector::received(self : ReaderDiagnosticCollector) -> Int {
  self.received_count
}

///|
fn ReaderDiagnosticCollector::finish(
  self : ReaderDiagnosticCollector,
) -> Array[Message] {
  if self.truncated && self.messages.length() < self.max_messages {
    self.messages.push(
      Warning(
        bounded_reader_diagnostic_text(
          "additional DOCX diagnostics were omitted",
          self.max_chars,
        ),
      ),
    )
  }
  self.messages
}

///|
fn push_reader_message(
  target : Array[Message],
  collector : ReaderDiagnosticCollector?,
  message : Message,
) -> Unit {
  match message {
    Warning(value) => push_reader_warning_parts(target, collector, [value])
    Error(value) => push_reader_error_parts(target, collector, [value])
  }
}