///|
pub(all) enum OutputFormat {
  PlainText
  Markdown
  Html
  StructuredJson
} derive(Eq, Debug)

///|
pub(all) struct Conversion {
  content : String
  diagnostics : Array[Diagnostic]
  exceeded_limit : Bool
} derive(Eq, Debug, ToJson)

///|
/// Identify representation losses before selecting an output format.
pub fn Document::conversion_diagnostics(
  self : Document,
  format : OutputFormat,
  include_hidden? : Bool = false,
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  let seen : Map[String, Bool] = Map([])
  for paragraph in self.paragraphs {
    if paragraph.region != Body {
      continue
    }
    if format == Markdown &&
      paragraph.style.alignment != Left &&
      !seen.contains("markdown_alignment") {
      seen["markdown_alignment"] = true
      diagnostics.push(
        validation_issue(
          "markdown_alignment",
          "Markdown does not preserve paragraph alignment",
          paragraph.span,
        ),
      )
    }
    for run in paragraph.runs {
      if !include_hidden && run.style.hidden {
        continue
      }
      if format == Markdown {
        if run.style.underline && !seen.contains("markdown_underline") {
          seen["markdown_underline"] = true
          diagnostics.push(
            validation_issue(
              "markdown_underline",
              "Underline is flattened in Markdown output",
              run.span,
            ),
          )
        }
        if run.style.superscript != 0 && !seen.contains("markdown_script") {
          seen["markdown_script"] = true
          diagnostics.push(
            validation_issue(
              "markdown_script",
              "Superscript and subscript are flattened in Markdown output",
              run.span,
            ),
          )
        }
        if (run.style.foreground > 0 || run.style.background > 0) &&
          !seen.contains("markdown_color") {
          seen["markdown_color"] = true
          diagnostics.push(
            validation_issue(
              "markdown_color",
              "Font colors are not preserved in Markdown output",
              run.span,
            ),
          )
        }
      }
      if (format == Markdown || format == Html) &&
        run.link is Some(link) &&
        !safe_link(link) &&
        !seen.contains("inactive_link") {
        seen["inactive_link"] = true
        diagnostics.push(
          validation_issue(
            "inactive_link",
            "A link with an unsupported scheme is rendered as text",
            run.span,
          ),
        )
      }
    }
  }
  if format == Markdown {
    for table in self.tables() {
      if !table.is_rectangular() && !seen.contains("ragged_markdown_table") {
        seen["ragged_markdown_table"] = true
        diagnostics.push(
          validation_issue(
            "ragged_markdown_table",
            "Unequal table row widths may render differently between Markdown readers",
            { start: 0, end: 0, },
          ),
        )
      }
    }
  }
  diagnostics
}

///|
/// Convert a parsed document with an explicit output limit and loss report.
/// Structured JSON keeps parse diagnostics in the interchange representation.
pub fn ParseResult::convert(
  self : ParseResult,
  format : OutputFormat,
  options? : TextOptions = TextOptions::default(),
  max_output_units? : Int = 64 * 1024 * 1024,
) -> Conversion {
  let diagnostics = self.diagnostics.copy()
  if max_output_units <= 0 {
    diagnostics.push({
      code: "invalid_output_limit",
      severity: Error,
      message: "Output limit must be positive",
      span: { start: 0, end: 0, },
    })
    return { content: "", diagnostics, exceeded_limit: true, }
  }
  for
    issue in self.document.conversion_diagnostics(
      format,
      include_hidden=options.include_hidden,
    ) {
    diagnostics.push(issue)
  }
  let content = match format {
    PlainText => self.document.to_text(options~)
    Markdown => self.document.to_markdown(options~)
    Html => self.document.to_html(options~)
    StructuredJson => self.to_json_text()
  }
  if content.length() > max_output_units {
    diagnostics.push({
      code: "encoded_output_limit",
      severity: Error,
      message: "Converted output exceeds its UTF-16 unit limit",
      span: { start: 0, end: 0, },
    })
    { content: "", diagnostics, exceeded_limit: true, }
  } else {
    { content, diagnostics, exceeded_limit: false, }
  }
}