// The adapter (#434 PR 5): pure restructuring of one joined read into the
// reader-native projection. It groups contributions into paragraphs and
// runs and assigns identities, spans, and UTF-16 intervals; it never
// decides what is text-like -- the reader's effects already did.
//
// The walk mirrors the erase order exactly: a paragraph's children
// accumulate into the shared pending buffer, the paragraph CLOSE flushes
// the buffer as one logical paragraph (so flow-level content before a
// paragraph joins it, the way the erased partition behaves), and extras
// are walked in the outer context as the siblings they erase into.

///|
priv struct ReaderProjectionBuilder {
  scan : StoryScan
  budget : ReaderProjectionBudget
  paragraphs : Array[ReaderProjectionParagraph]
  /// Contributions accumulated toward the next paragraph flush.
  pending : Array[ReaderProjectionContribution]
  /// Physical runs observed since the last flush, in first-appearance
  /// order. Registered structurally on run entry so atomless runs stay
  /// addressable.
  pending_runs : Array[ReaderProjectionRun]
  /// The open enclosures, outermost first: paragraph frames, runs, and
  /// BLOCK BOUNDARIES (table family, extras re-homing). Boundaries stop
  /// both frame arming and run visibility, mirroring the legacy walker's
  /// per-level inline state.
  enclosures : Array[ReaderProjectionEnclosure]
  /// The field-carrier event stream, in walk order.
  field_carriers : Array[ReaderFieldCarrierEvent]
  mut refusal : ReaderProjectionRefusal?
}

///|
priv enum ReaderProjectionEnclosure {
  EnclosingParagraph(ReaderProjectionParagraphFrame)
  EnclosingRun(SourceElementId)
  BlockBoundary
}

///|
priv struct ReaderProjectionParagraphFrame {
  inputs : Array[ReaderParagraphInput]
  mut armed : Bool
  cursor : ReaderProjectionCarrierCursor
}

///|
/// Projects one read's items over its story scan. `items` must come from
/// reading a JOINED story: any node without a physical identity refuses.
#warnings("-unused_value")
fn project_reader_items(
  scan : StoryScan,
  items : Array[ReaderItem],
  budget? : ReaderProjectionBudget,
) -> ReaderProjectionBuildResult raise DocxError {
  project_reader_item_segments(scan, [items], budget?)
}

///|
/// The multi-segment core: every segment's items walk through ONE shared
/// builder with a segment boundary between them, so ownership is checked
/// per segment while coordinates and carrier events stay continuous
/// across the whole part.
fn project_reader_item_segments(
  scan : StoryScan,
  segments : Array[Array[ReaderItem]],
  budget? : ReaderProjectionBudget,
) -> ReaderProjectionBuildResult raise DocxError {
  let budget = match budget {
    Some(value) => value
    None => reader_projection_budget(scan)
  }
  let builder = ReaderProjectionBuilder::{
    scan,
    budget,
    paragraphs: [],
    pending: [],
    pending_runs: [],
    enclosures: [],
    field_carriers: [],
    refusal: None,
  }
  for items in segments {
    builder.walk_items(items)
    match builder.refusal {
      Some(refusal) => return ProjectionRefused(refusal)
      None => ()
    }
    builder.finish_segment()
    match builder.refusal {
      Some(refusal) => return ProjectionRefused(refusal)
      None => ()
    }
  }
  let projection = ReaderProjection::{
    scan,
    paragraphs: builder.paragraphs,
    field_carriers: builder.field_carriers,
    field_index: empty_reader_field_index(),
  }
  assign_reader_projection_coordinates(projection)
  Projected(projection)
}

///|
/// The segment boundary: whatever remains pending was never owned by a
/// paragraph in THIS segment. Zero-width markers vanish unaddressed,
/// visible content is a refusal, and observed-but-unflushed runs must
/// not leak into the next segment's first paragraph.
fn ReaderProjectionBuilder::finish_segment(
  self : ReaderProjectionBuilder,
) -> Unit {
  for contribution in self.pending {
    let visible = match contribution.kind {
      ProjectedText(_) => contribution.value.length() > 0
      VisibleNonText => true
      Transparent | SuppressedContent => false
    }
    if visible {
      self.refusal = Some(
        UnownedVisibleContribution(source=contribution.source),
      )
      return
    }
  }
  self.pending.clear()
  self.pending_runs.clear()
}

///|
/// The run a contribution at the current position sits in: the enclosure
/// stack's top, when it is a run. A paragraph frame or block boundary on
/// top means the position is outside every run at its level, exactly as
/// each legacy inline state starts with no current run.
fn ReaderProjectionBuilder::current_run(
  self : ReaderProjectionBuilder,
) -> SourceElementId? {
  match self.enclosures.last() {
    Some(EnclosingRun(id)) => Some(id)
    _ => None
  }
}

///|
/// Arms every enclosing paragraph frame reachable through inline
/// containers -- stopping at the first block boundary -- so those frames
/// participate in the first flush of the block item about to be walked.
/// This is the legacy walker re-ensuring the outer source before each
/// `consume_inline_block_output`.
fn ReaderProjectionBuilder::arm_enclosing_frames(
  self : ReaderProjectionBuilder,
) -> Unit {
  for index = self.enclosures.length() - 1; index >= 0; index = index - 1 {
    match self.enclosures[index] {
      EnclosingParagraph(frame) => frame.armed = true
      EnclosingRun(_) => ()
      BlockBoundary => return
    }
  }
}

///|
/// Re-registers the runs of the CURRENT segment -- the enclosures above
/// the last block boundary -- into the pending runs, outermost first: the
/// legacy walker reopens its level's active runs after a flush or on
/// returning from a nested block.
fn ReaderProjectionBuilder::reopen_segment_runs(
  self : ReaderProjectionBuilder,
) -> Unit raise DocxError {
  let mut segment_start = 0
  for index, enclosure in self.enclosures {
    if enclosure is BlockBoundary {
      segment_start = index + 1
    }
  }
  for index in segment_start.. self.register_run(id)
      _ => ()
    }
  }
}

///|
fn reader_item_source(item : ReaderItem) -> ReaderNode {
  match item {
    New(tree) => tree.source
    Trace(effect) =>
      match effect {
        ProjectsText(source~, ..) => source
        ProjectsVisibleNonText(source~) => source
        TransparentBoundary(source~) => source
        Suppressed(source~) => source
      }
  }
}

///|
/// The authoritative ordered carrier events of one paragraph: every
/// direct physical child of every input, in document order -- exactly
/// what the legacy walker emits before visiting each carrier. The item
/// walk only decides WHEN each event fires (a cursor advances on item
/// position hints, so a nested paragraph's group interleaves at its
/// carrier's position); item sources never decide the SET or ORDER,
/// because field synthesis re-sources items at the field-begin carrier.
priv struct ReaderProjectionCarrierCursor {
  events : Array[ReaderFieldCarrierEvent]
  positions : Map[Int, Int]
  mut next : Int
}

///|
fn ReaderProjectionBuilder::carrier_cursor(
  self : ReaderProjectionBuilder,
  inputs : Array[ReaderParagraphInput],
) -> ReaderProjectionCarrierCursor raise DocxError {
  // the events enumerate the PHYSICAL direct children of every input
  // paragraph, from the scan: the legacy walk events every direct child
  // it visits, including constructs the tolerant parse erased outright
  // (a no-fallback MC block leaves no DOM child at all). The cooked item
  // walk only times when the cursor fires.
  let elements = self.scan.elements()
  let events : Array[ReaderFieldCarrierEvent] = []
  let positions : Map[Int, Int] = Map([])
  for input in inputs {
    match input.paragraph.source_element_id {
      Some(SourceElementId(paragraph_identity)) => {
        let mut child = elements[paragraph_identity].first_child_index
        while child != -1 {
          self.budget.charge_retained()
          positions[child] = events.length()
          events.push({ paragraph_identity, carrier_identity: child, })
          child = elements[child].next_sibling_index
        }
      }
      None => ()
    }
  }
  { events, positions, next: 0, }
}

///|
/// Fires every event up to and including the carrier this item's source
/// sits under, when that carrier lies ahead of the cursor. A synthetic
/// source pointing at an earlier carrier advances nothing; the close
/// flush drains whatever remains.
fn ReaderProjectionBuilder::advance_carrier_cursor(
  self : ReaderProjectionBuilder,
  cursor : ReaderProjectionCarrierCursor,
  item : ReaderItem,
) -> Unit raise DocxError {
  guard reader_item_source(item).source_element_id
    is Some(SourceElementId(source_index)) else {
    return
  }
  self.advance_carrier_cursor_by_source(cursor, source_index)
}

///|
fn ReaderProjectionBuilder::advance_carrier_cursor_by_source(
  self : ReaderProjectionBuilder,
  cursor : ReaderProjectionCarrierCursor,
  source_index : Int,
) -> Unit raise DocxError {
  let elements = self.scan.elements()
  if source_index < 0 || source_index >= elements.length() {
    return
  }
  let mut current = source_index
  let mut target : Int? = None
  while current >= 0 {
    self.budget.charge_visit()
    match cursor.positions.get(current) {
      Some(position) => {
        target = Some(position)
        break
      }
      None => ()
    }
    current = elements[current].parent_index
  }
  match target {
    Some(position) =>
      while cursor.next <= position {
        self.budget.charge_retained()
        self.field_carriers.push(cursor.events[cursor.next])
        cursor.next = cursor.next + 1
      }
    None => ()
  }
}

///|
/// The legacy walk emits a carrier event BEFORE visiting the carrier. A
/// block item reached through a synthetic wrapper (a field-synthesized
/// hyperlink sourced at the begin carrier) must therefore advance every
/// enclosing paragraph's cursor from its OWN physical source before its
/// group fires -- the item-level hint saw only the wrapper's source.
/// Scoped through inline enclosures up to the first block boundary: a
/// nested table's cell paragraphs never advance an outer-level cursor.
fn ReaderProjectionBuilder::advance_enclosing_cursors(
  self : ReaderProjectionBuilder,
  source : ReaderNode,
) -> Unit raise DocxError {
  guard source.source_element_id is Some(SourceElementId(source_index)) else {
    return
  }
  for index = self.enclosures.length() - 1; index >= 0; index = index - 1 {
    match self.enclosures[index] {
      EnclosingParagraph(frame) =>
        self.advance_carrier_cursor_by_source(frame.cursor, source_index)
      EnclosingRun(_) => ()
      BlockBoundary => return
    }
  }
}

///|
fn ReaderProjectionBuilder::drain_carrier_cursor(
  self : ReaderProjectionBuilder,
  cursor : ReaderProjectionCarrierCursor,
) -> Unit raise DocxError {
  while cursor.next < cursor.events.length() {
    self.budget.charge_retained()
    self.field_carriers.push(cursor.events[cursor.next])
    cursor.next = cursor.next + 1
  }
}

///|
fn ReaderProjectionBuilder::identity_of(
  self : ReaderProjectionBuilder,
  node : ReaderNode,
) -> SourceElementId? {
  match node.source_element_id {
    Some(SourceElementId(index) as id) => {
      if index < 0 || index >= self.scan.elements().length() {
        if self.refusal is None {
          self.refusal = Some(SourceIdentityOutOfRange(id))
        }
        return None
      }
      Some(id)
    }
    None => {
      if self.refusal is None {
        self.refusal = Some(MissingSourceIdentity(node_name=node.name()))
      }
      None
    }
  }
}

///|
fn ReaderProjectionBuilder::atom_span(
  self : ReaderProjectionBuilder,
  id : SourceElementId,
) -> ReaderSourceSpan {
  let SourceElementId(index) = id
  reader_atom_span(self.scan.elements()[index])
}

///|
fn ReaderProjectionBuilder::element_span(
  self : ReaderProjectionBuilder,
  id : SourceElementId,
) -> ReaderSourceSpan {
  let SourceElementId(index) = id
  reader_element_span(self.scan.elements()[index])
}

///|
fn ReaderProjectionBuilder::push_effect(
  self : ReaderProjectionBuilder,
  effect : ReaderEffect,
) -> Unit raise DocxError {
  self.budget.charge_contribution()
  let (kind, value, source_node) = match effect {
    ProjectsText(source~, kind~, value~) => (ProjectedText(kind), value, source)
    ProjectsVisibleNonText(source~) => (VisibleNonText, "", source)
    TransparentBoundary(source~) => (Transparent, "", source)
    Suppressed(source~) => (SuppressedContent, "", source)
  }
  guard self.identity_of(source_node) is Some(id) else { return }
  let source_span = match kind {
    ProjectedText(_) => self.atom_span(id)
    _ => self.element_span(id)
  }
  let run_source = self.current_run()
  match run_source {
    // an attributed run is always in its paragraph's run records, even
    // when its structural registration was drained by an earlier flush
    Some(run) => self.register_run(run)
    None => ()
  }
  self.pending.push({
    kind,
    value,
    source: id,
    run_source,
    source_span,
    projection_start: 0,
    projection_end: 0,
    logical_run_index: -1,
    field_identity: None,
    field_region: OutsideField,
    field_refusal: None,
  })
}

///|
fn ReaderProjectionBuilder::register_run(
  self : ReaderProjectionBuilder,
  id : SourceElementId,
) -> Unit raise DocxError {
  // a physical run must BE a run element: a synthesized field-checkbox
  // run whose carrier is a tolerated non-run container (a hyperlink or a
  // control holding the begin marker) has no run-shaped address
  let SourceElementId(index) = id
  let element = self.scan.elements()[index]
  if !(is_wml_uri(element.uri) && element.local_name == "r") {
    if self.refusal is None {
      self.refusal = Some(
        SourceShapeMismatch(
          source=id,
          expected="w:r",
          actual=element.local_name,
        ),
      )
    }
    return
  }
  for run in self.pending_runs {
    if run.source == id {
      return
    }
  }
  self.budget.charge_retained()
  self.pending_runs.push({
    source: id,
    logical_index: -1,
    field_region: OutsideField,
    field_refusal: None,
  })
}

///|
fn ReaderProjectionBuilder::walk_items(
  self : ReaderProjectionBuilder,
  items : Array[ReaderItem],
) -> Unit raise DocxError {
  for item in items {
    if self.refusal is Some(_) {
      return
    }
    self.budget.charge_visit()
    match item {
      Trace(effect) => self.push_effect(effect)
      New(tree) => {
        for effect in tree.local_effects {
          self.push_effect(effect)
        }
        match tree.shape {
          ReaderParagraph(inputs~, children~, extras~, properties=_) => {
            // a block item at an inline position: the enclosing frames
            // participate in its first flush, and their cursors pass the
            // carrier containing this paragraph BEFORE its group fires
            self.arm_enclosing_frames()
            self.advance_enclosing_cursors(tree.source)
            let frame = ReaderProjectionParagraphFrame::{
              inputs,
              armed: true,
              cursor: self.carrier_cursor(inputs),
            }
            self.enclosures.push(EnclosingParagraph(frame))
            for child in children {
              if self.refusal is Some(_) {
                break
              }
              self.advance_carrier_cursor(frame.cursor, child)
              self.walk_items([child])
            }
            self.drain_carrier_cursor(frame.cursor)
            ignore(self.enclosures.pop())
            self.flush_paragraph(inputs)
            // extras erase as siblings AFTER the paragraph and re-home
            // block content: walked behind a boundary so nothing leaks
            // across in either direction
            self.enclosures.push(BlockBoundary)
            self.walk_items(extras)
            ignore(self.enclosures.pop())
            self.reopen_segment_runs()
          }
          ReaderRun(children~, properties=_) => {
            guard self.identity_of(tree.source) is Some(id) else { return }
            self.register_run(id)
            self.enclosures.push(EnclosingRun(id))
            self.walk_items(children)
            ignore(self.enclosures.pop())
          }
          ReaderHyperlink(children~, ..) => self.walk_items(children)
          ReaderTable(children~, properties=_) => {
            self.arm_enclosing_frames()
            self.advance_enclosing_cursors(tree.source)
            self.enclosures.push(BlockBoundary)
            self.walk_items(children)
            ignore(self.enclosures.pop())
            // returning to this level restores its active runs, like the
            // legacy reset after consuming a nested block's output
            self.reopen_segment_runs()
          }
          ReaderTableRow(children~, is_header=_) => {
            self.arm_enclosing_frames()
            self.advance_enclosing_cursors(tree.source)
            self.enclosures.push(BlockBoundary)
            self.walk_items(children)
            ignore(self.enclosures.pop())
            self.reopen_segment_runs()
          }
          ReaderTableCell(children~, ..) => {
            self.arm_enclosing_frames()
            self.advance_enclosing_cursors(tree.source)
            self.enclosures.push(BlockBoundary)
            self.walk_items(children)
            ignore(self.enclosures.pop())
            self.reopen_segment_runs()
          }
          ReaderText(_)
          | ReaderTab
          | ReaderCheckbox(_)
          | ReaderNoteReference(..)
          | ReaderCommentReference(_)
          | ReaderImage(_)
          | ReaderBreak(_)
          | ReaderBookmarkStart(_) => ()
        }
      }
    }
  }
}

///|
fn ReaderProjectionBuilder::flush_paragraph(
  self : ReaderProjectionBuilder,
  inputs : Array[ReaderParagraphInput],
) -> Unit raise DocxError {
  self.budget.charge_retained()
  let sources : Array[ReaderProjectionParagraphSource] = []
  for enclosure in self.enclosures {
    match enclosure {
      EnclosingParagraph(frame) =>
        if frame.armed {
          for input in frame.inputs {
            guard self.identity_of(input.paragraph) is Some(id) else { return }
            self.budget.charge_retained()
            sources.push({ source: id, source_span: self.element_span(id), })
          }
          frame.armed = false
        }
      _ => ()
    }
  }
  for input in inputs {
    guard self.identity_of(input.paragraph) is Some(id) else { return }
    self.budget.charge_retained()
    sources.push({ source: id, source_span: self.element_span(id), })
  }
  let contributions : Array[ReaderProjectionContribution] = []
  contributions.append(self.pending)
  self.pending.clear()
  let runs : Array[ReaderProjectionRun] = []
  runs.append(self.pending_runs)
  self.pending_runs.clear()
  self.paragraphs.push({
    sources,
    runs,
    contributions,
    logical_index: -1,
    projection_start: 0,
    projection_end: 0,
  })
  // runs are reopened by the CALLER once the paragraph's entire output --
  // extras included -- has been consumed, matching the legacy walker's
  // reopen-after-consume; reopening here would leak an outer run into the
  // extras' own paragraphs
}