// The reader-native projection (#434): what the reader actually did,
// with physical provenance attached. Built by `reader_projection_adapter.mbt`
// from one joined read. Since PR 7 this is the ONLY projection semantics;
// the legacy byte-level walker is deleted.

///|
#warnings("-unused_field")
priv struct ReaderSourceSpan {
  byte_start : Int
  byte_end : Int
}

///|
fn reader_element_span(element : ScannedElement) -> ReaderSourceSpan {
  { byte_start: element.byte_start, byte_end: element.byte_end, }
}

///|
fn reader_atom_span(element : ScannedElement) -> ReaderSourceSpan {
  if is_wml_uri(element.uri) &&
    element.local_name == "t" &&
    element.text_map is Some(_) {
    { byte_start: element.content_start, byte_end: element.content_end, }
  } else {
    reader_element_span(element)
  }
}

///|
/// The four-way contribution kind, one level finer than the legacy
/// TransparentSeam/HardBarrier pair: a barrier that is VISIBLE (a break, a
/// reference, an image, a checkbox) is distinguished from suppressed
/// content, and text keeps its atom kind.
priv enum ReaderProjectionContributionKind {
  ProjectedText(ReaderProjectedTextKind)
  VisibleNonText
  Transparent
  SuppressedContent
}

///|
/// One reader decision in story order. Only `ProjectedText` occupies UTF-16
/// width; everything else is a zero-width position marker. `source` always
/// indexes the story's scan.
#warnings("-unused_field")
priv struct ReaderProjectionContribution {
  kind : ReaderProjectionContributionKind
  value : String
  source : SourceElementId
  /// The innermost physical run this contribution sits in, when any.
  run_source : SourceElementId?
  source_span : ReaderSourceSpan
  /// Half-open UTF-16 interval in the reader's story text; zero-width
  /// contributions get an empty interval at the position they sit.
  mut projection_start : Int
  mut projection_end : Int
  /// Index of this contribution's run within its paragraph by order of
  /// first appearance, or -1 when it belongs to no run.
  mut logical_run_index : Int
  /// Field annotations, populated by `classify_reader_projection_fields`.
  mut field_identity : Int?
  mut field_region : ReaderFieldRegion
  mut field_refusal : Int?
}

///|
/// One physical run observed inside a logical paragraph, in order of first
/// appearance. Registered structurally on entering the run, so an atomless
/// run stays addressable without any effect.
#warnings("-unused_field")
priv struct ReaderProjectionRun {
  source : SourceElementId
  /// Position among the paragraph's runs in first-appearance order -- the
  /// r[j] a surgery address names. Atomless runs participate, exactly as
  /// the legacy walker's zero-width run-open seams made them.
  mut logical_index : Int
  /// The run's own field region and refusal, populated by
  /// `classify_reader_projection_fields` through a run slot: a run with
  /// NO contributions still sits somewhere, and surgery must refuse an
  /// instruction-region, malformed-field or refused-field run even when
  /// nothing inside it projected. The refusal matters separately from
  /// the region: truncation is discovered at story END, after regions
  /// are assigned, so a truncated field's result run keeps FieldResult
  /// and carries the refusal alone.
  mut field_region : ReaderFieldRegion
  mut field_refusal : Int?
}

///|
/// One physical source paragraph of a logical paragraph: deleted
/// paragraph-mark prefixes stay separate entries even though their content
/// joined the terminal paragraph.
#warnings("-unused_field")
priv struct ReaderProjectionParagraphSource {
  source : SourceElementId
  source_span : ReaderSourceSpan
}

///|
#warnings("-unused_field")
priv struct ReaderProjectionParagraph {
  sources : Array[ReaderProjectionParagraphSource]
  runs : Array[ReaderProjectionRun]
  contributions : Array[ReaderProjectionContribution]
  mut logical_index : Int
  mut projection_start : Int
  mut projection_end : Int
}

///|
#warnings("-unused_field")
priv struct ReaderProjection {
  scan : StoryScan
  paragraphs : Array[ReaderProjectionParagraph]
  /// One event per direct physical child of each visited paragraph, in
  /// the walk's document order (nested paragraphs' groups interleave at
  /// their positions) -- the stream the field classification consumes.
  field_carriers : Array[ReaderFieldCarrierEvent]
  /// Populated by `classify_reader_projection_fields`.
  mut field_index : ReaderFieldIndex
}

///|
/// Why a read's output could not be projected. Provenance failures are
/// typed refusals -- the adapter never invents an identity.
priv enum ReaderProjectionRefusal {
  MissingSourceIdentity(node_name~ : String)
  SourceIdentityOutOfRange(SourceElementId)
  /// A node whose physical element is not the shape its role requires --
  /// e.g. a synthesized run whose carrier is not a `w:r`.
  SourceShapeMismatch(
    source~ : SourceElementId,
    expected~ : String,
    actual~ : String
  )
  /// Visible content that no logical paragraph ever owns (flow-level
  /// content after the final paragraph). Zero-width markers in the same
  /// position are dropped instead: nothing addresses them.
  UnownedVisibleContribution(source~ : SourceElementId)
}

///|
priv enum ReaderProjectionBuildResult {
  Projected(ReaderProjection)
  ProjectionRefused(ReaderProjectionRefusal)
}

///|
fn ReaderProjectionRefusal::describe(self : ReaderProjectionRefusal) -> String {
  match self {
    MissingSourceIdentity(node_name~) =>
      "a \{node_name} node carries no physical identity"
    SourceIdentityOutOfRange(SourceElementId(index)) =>
      "source identity \{index} is outside the scan"
    SourceShapeMismatch(source=SourceElementId(index), expected~, actual~) =>
      "source element \{index} is a \{actual}, not the \{expected} its role requires"
    UnownedVisibleContribution(source=SourceElementId(index)) =>
      "visible content at source element \{index} is owned by no paragraph"
  }
}

///|
/// The `"\n\n"` BodyReader writes between paragraphs, in UTF-16 units --
/// the same constant the legacy projection uses.
let reader_projection_separator_units : Int = 2

///|
/// Assigns logical indices, first-appearance run numbering, and UTF-16
/// story intervals -- the exact convention of
/// the legacy coordinate-assignment convention.
fn assign_reader_projection_coordinates(projection : ReaderProjection) -> Unit {
  let mut offset = 0
  for index, paragraph in projection.paragraphs {
    paragraph.logical_index = index
    paragraph.projection_start = offset
    // seed the numbering from the runs list: an atomless run occupies its
    // first-appearance position even though no contribution names it
    let run_indices : Map[Int, Int] = Map([])
    for run in paragraph.runs {
      let SourceElementId(identity) = run.source
      run.logical_index = run_indices.length()
      run_indices[identity] = run.logical_index
    }
    for contribution in paragraph.contributions {
      contribution.logical_run_index = match contribution.run_source {
        Some(SourceElementId(identity)) =>
          match run_indices.get(identity) {
            Some(assigned) => assigned
            None => {
              let assigned = run_indices.length()
              run_indices[identity] = assigned
              assigned
            }
          }
        None => -1
      }
      contribution.projection_start = offset
      offset = offset + contribution.value.length()
      contribution.projection_end = offset
    }
    paragraph.projection_end = offset
    offset = offset + reader_projection_separator_units
  }
}

///|
/// Caller-owned cumulative limits for one projection build. Extras and
/// transformed subtrees can multiply occurrences, so the adapter carries
/// its own caps rather than inheriting the reader's.
priv struct ReaderProjectionBudget {
  mut visits_left : Int
  mut contributions_left : Int
  mut retained_left : Int
}

///|
/// Input-linear defaults: generous multiples of the physical element
/// count, far above any real document's fan-out.
fn reader_projection_budget(scan : StoryScan) -> ReaderProjectionBudget {
  let elements = scan.elements().length()
  let scaled = fn(factor : Int) -> Int {
    let value = elements * factor + 1024
    if value < 0 {
      2147483647
    } else {
      value
    }
  }
  {
    visits_left: scaled(64),
    contributions_left: scaled(16),
    retained_left: scaled(16),
  }
}

///|
fn ReaderProjectionBudget::charge_visit(
  self : ReaderProjectionBudget,
) -> Unit raise DocxError {
  self.visits_left = self.visits_left - 1
  if self.visits_left < 0 {
    raise @core.docx_xml_resource_limit_error(DocxXmlTokens)
  }
}

///|
fn ReaderProjectionBudget::charge_contribution(
  self : ReaderProjectionBudget,
) -> Unit raise DocxError {
  self.contributions_left = self.contributions_left - 1
  if self.contributions_left < 0 {
    raise @core.docx_xml_resource_limit_error(DocxXmlTokens)
  }
}

///|
fn ReaderProjectionBudget::charge_retained(
  self : ReaderProjectionBudget,
) -> Unit raise DocxError {
  self.retained_left = self.retained_left - 1
  if self.retained_left < 0 {
    raise @core.docx_xml_resource_limit_error(DocxXmlTokens)
  }
}