// The write side of #95: header and footer STORIES for the document's single
// section. `write_docx_with_annotations` takes header/footer specs, serializes
// each into its own `word/headerN.xml` / `word/footerN.xml` part with a
// `w:hdr` / `w:ftr` root, wires each part as a relationship of the MAIN
// document part, and emits the matching `w:headerReference` /
// `w:footerReference` in `w:sectPr` — content types, relationships and
// references all derived from one list so they cannot drift apart.
//
// Two OOXML details this writer takes care of, because a document that
// silently ignores them looks broken in Word rather than failing loudly:
//   * a `first` variant only takes effect when the section carries
//     `w:titlePg` (emitted by `blank_section_properties`);
//   * an `even` variant only takes effect when `word/settings.xml` carries
//     `w:evenAndOddHeaders`, so the settings part is written when — and only
//     when — an even-page story exists.
//
// Story content is PLAIN, exactly like comment and note bodies: paragraphs
// and tables, no hyperlinks, images or note references. Those would need
// relationships in the STORY'S OWN `word/_rels/headerN.xml.rels`, which this
// writer does not emit; fail closed instead of emitting references that would
// dangle. FIELDS are allowed and are the point — a live `PAGE` field is what
// makes a footer a page number instead of static text.

///|
/// One header or footer story attached to the written document's section.
/// Construct with `header_footer_spec`, which fail-closes on everything the
/// story can get wrong in isolation.
pub struct HeaderFooterSpec {
  priv variant : String
  priv body : Array[DocumentElement]
}

///|
/// The OOXML header/footer reference types, in the CANONICAL emission order
/// this writer uses. Deterministic regardless of the order specs arrive in,
/// so `read(write(x))` sees a stable variant sequence.
let header_footer_variants : Array[String] = ["default", "first", "even"]

///|
/// Validates and builds one header/footer story. `variant` is the OOXML
/// reference type (`default`, `first` or `even`); the body is non-empty and
/// block-level (paragraphs and tables), plain content all the way down.
pub fn header_footer_spec(
  variant~ : String,
  body : Array[DocumentElement],
) -> HeaderFooterSpec raise DocxError {
  if !header_footer_variants.contains(variant) {
    raise Unsupported(
      message="unsupported header/footer variant '\{variant}' (default, first, or even)",
    )
  }
  check_story_body(body, what="header/footer")
  { variant, body, }
}

///|
/// The story body shape rules, applied at CONSTRUCTION for early attributable
/// errors and REPEATED at write time (the array stays caller-mutable):
/// non-empty, block-level, and plain content — no hyperlinks, images or note
/// references, which would need relationships the story part does not get.
fn check_story_body(
  body : Array[DocumentElement],
  what~ : String,
) -> Unit raise DocxError {
  if body.length() == 0 {
    raise Unsupported(
      message="a \{what} story needs a non-empty body (at least one paragraph)",
    )
  }
  for block in body {
    guard block is (Paragraph(..) | Table(..)) else {
      raise Unsupported(
        message="\{what} stories hold paragraphs and tables in this writer (got \{block_kind_name(block)})",
      )
    }
    check_story_content(block, what~)
  }
}

///|
fn check_story_content(
  element : DocumentElement,
  what~ : String,
) -> Unit raise DocxError {
  match element {
    Hyperlink(..) =>
      raise Unsupported(
        message="\{what} stories are plain content: hyperlinks cannot be serialized into a header/footer part (they would need relationships that part does not get)",
      )
    Image(_) =>
      raise Unsupported(
        message="\{what} stories are plain content: images cannot be serialized into a header/footer part (they would need relationships that part does not get)",
      )
    NoteReference(..) =>
      raise Unsupported(
        message="\{what} stories cannot carry note references (notes live in the main document story)",
      )
    CommentReference(_) =>
      raise Unsupported(
        message="\{what} stories cannot carry comment references",
      )
    Document(children~, ..)
    | Paragraph(children~, ..)
    | Run(children~, ..)
    | Table(children~, ..)
    | TableRow(children~, ..)
    | TableCell(children~, ..) =>
      for child in children {
        check_story_content(child, what~)
      }
    _ => ()
  }
}

///|
/// Rejects duplicate variants within one kind: a section may declare each of
/// default/first/even at most once (the reader raises on a repeat, so a
/// duplicate here would produce a package our own reader refuses).
fn check_header_footer_variants(
  specs : Array[HeaderFooterSpec],
  kind~ : String,
) -> Unit raise DocxError {
  let seen : Array[String] = []
  for index, spec in specs {
    if seen.contains(spec.variant) {
      raise Unsupported(
        message="\{kind}s[\{index}] repeats the '\{spec.variant}' variant; a section declares each variant at most once",
      )
    }
    seen.push(spec.variant)
    check_story_body(spec.body, what=kind) catch {
      Unsupported(message~) =>
        raise Unsupported(message="\{kind}s[\{index}]: \{message}")
      err => raise err
    }
  }
}

///|
/// Serializes the header/footer stories of one kind into their parts and the
/// matching `sectPr` references, allocating one MAIN-part relationship each.
/// Returns the parts (`(part name under word/, xml)`, in allocation order)
/// and the reference elements, in the canonical variant order.
///
/// Story bodies reuse the body writers and the shared `WriteContext` (styles
/// and numbering are package-wide), then a relationship/media delta guard
/// catches anything that slipped past `check_story_body` through an alias.
fn write_header_footer_parts(
  specs : Array[HeaderFooterSpec],
  ctx : WriteContext,
  kind~ : String,
) -> (Array[(String, String)], Array[XmlNode]) raise DocxError {
  let parts : Array[(String, String)] = []
  let references : Array[XmlNode] = []
  if specs.length() == 0 {
    return (parts, references)
  }
  let root_name = if kind == "header" { "w:hdr" } else { "w:ftr" }
  let relationship_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/" +
    kind
  for variant in header_footer_variants {
    for spec in specs {
      guard spec.variant == variant else { continue }
      let relationships_before = ctx.document_relationships.length()
      let media_before = ctx.media.length()
      let blocks : Array[XmlNode] = []
      for block in spec.body {
        blocks.push(XmlElement(write_block(block, ctx)))
      }
      if ctx.document_relationships.length() > relationships_before ||
        ctx.media.length() > media_before {
        raise Unsupported(
          message="\{kind} stories are plain content: hyperlinks and images cannot be serialized into a \{kind} part (they would need relationships that part does not get)",
        )
      }
      let part_name = "\{kind}\{parts.length() + 1}.xml"
      parts.push(
        (
          part_name,
          @xml.write_xml_string(@xml.xml_element(root_name, children=blocks), namespaces={
            "w": WORDPROCESSINGML_NAMESPACE,
          }),
        ),
      )
      let id = ctx.allocate_relationship(
        relationship_type,
        part_name,
        external=false,
      )
      references.push(
        XmlElement(
          @xml.xml_element("w:\{kind}Reference", attributes={
            "w:type": variant,
            "r:id": id,
          }),
        ),
      )
    }
  }
  (parts, references)
}

///|
/// `word/settings.xml` carrying `w:evenAndOddHeaders`, written only when an
/// even-page story exists: without it Word ignores the `even` variant
/// entirely and renders the default story on every page.
fn even_and_odd_headers_settings_xml() -> String {
  @xml.write_xml_string(
    @xml.xml_element("w:settings", children=[
      XmlElement(@xml.xml_element("w:evenAndOddHeaders")),
    ]),
    namespaces={ "w": WORDPROCESSINGML_NAMESPACE },
  )
}

///|
fn has_variant(specs : Array[HeaderFooterSpec], variant : String) -> Bool {
  for spec in specs {
    if spec.variant == variant {
      return true
    }
  }
  false
}