///|
/// Package-private memoization for the markdown editor hot paths.
///
/// The editor rebuilds rich text documents on every keystroke, scroll
/// frame, pointer hit test, and IME poll. These caches make that
/// rebuilding cheap by keying on *content identity* instead of position:
///
/// - Block conversions are keyed by block content plus font (never by
///   absolute source offsets), so edits upstream of a block shift the
///   block's ranges without invalidating it. A hit rebases the cached
///   ranges by the start-offset delta.
/// - Run widths are keyed by text system id + font + text, mirroring the
///   inputs of a host measurement.
/// - Measured block heights are keyed by block content + font + width,
///   so an edit that changes a block's text is self-invalidating and no
///   explicit eviction hook is needed. Heights are written by the
///   session editor paint and read by the scroll geometry, which turns
///   the parse-time height estimates into measured geometry for
///   already-rendered blocks.
/// - Display sessions (source + composition text) memoize the full parse
///   for the short window where an IME polls paints while the candidate
///   text is unchanged.
/// - The editor block parse is keyed by source, so the many block-context
///   questions a single keystroke or command asks (pair/autolink/HTML
///   guards, caret kind and containment queries) share one whole-document
///   parse instead of re-parsing per question.
/// - The markdown mini-parse is keyed by text, because the block pass
///   consults the same lines several times and each mini-parse runs a
///   mizchi parse of its input to collect reference definitions.
/// - Scroll geometry (block heights + prefix sums) is keyed by font,
///   content width, and source, and stays valid until a measured height
///   actually changes, so wheel events and repaints read O(1) totals and
///   binary-searched windows instead of walking every block.
///
/// All maps are bounded; over-cap they clear wholesale. The runtime is
/// single-threaded, so no synchronization is needed.
priv struct RichTextBlockCacheEntry {
  source_start : Int
  block : RichTextBlock
}

///|
let rich_text_block_conversions : Map[String, RichTextBlockCacheEntry] = Map([])

///|
let rich_text_run_widths : Map[String, Double] = Map([])

///|
/// Measured heights live in digest buckets: the full-document scroll
/// geometry walk queries every block, so it hashes a cheap 64-bit digest
/// per block instead of allocating and re-hashing a content-length key
/// string. Each bucket still validates its hits against the exact
/// content-identity key, so lookups remain exact.
let rich_text_block_heights : Map[UInt64, Array[(String, Double)]] = Map([])

///|
/// Number of exact (key, height) pairs across all digest buckets; the
/// bounded clear and the empty-map short circuit read this instead of
/// walking the bucket map.
let rich_text_block_heights_count : Ref[Int] = Ref(0)

///|
let rich_text_display_sessions : Map[String, MarkdownDocumentSession] = Map([])

///|
let markdown_reference_definitions_cache : Map[
  String,
  Array[MarkdownReferenceDefinition],
] = Map([])

///|
let markdown_editor_blocks_cache : Map[String, Array[MarkdownEditorBlock]] = Map([],
)

///|
let markdown_parse_cache : Map[String, Array[MarkdownBlock]] = Map([])

///|
/// Bumped whenever a block's *measured* height changes (including when the
/// height map is evicted wholesale). Scroll geometry entries record the
/// epoch they were built at, so a wheel event rebuilds the height array
/// only when geometry actually changed — steady-state scrolling performs
/// zero block walks.
let rich_text_block_heights_epoch : Ref[Int] = Ref(0)

///|
/// Persisted scroll geometry for one (font, content width, source digest):
/// the per-block pixel heights plus a Fenwick tree over them, so content
/// height and any block prefix are O(log n), the visible window is a
/// prefix descent, and a freshly measured height for one block is an
/// O(log n) point update instead of a full rebuild. An entry with an
/// empty `heights` caches the “no measured geometry yet” verdict; the
/// epoch check revives it once paints record measured heights.
priv struct MarkdownDocumentScrollGeometry {
  // Content identity this geometry was built for. The entry is shared and
  // mutated in place: an incremental edit rebases the live entry (splice
  // the re-parse window, point-update or rebuild the Fenwick tree) instead
  // of walking every block again.
  mut source_digest : UInt64
  mut source_length : Int
  // Element writes go through the array; only the tree is replaced
  // wholesale on a count-changing rebase.
  heights : Array[Double]
  /// 1-based Fenwick tree over `heights`, length `heights.length() + 1`.
  mut tree : Array[Double]
  epoch : Int
}

///|
/// Build the geometry (and its Fenwick tree) from a full height array in
/// O(n).
fn markdown_document_scroll_geometry_from_heights(
  heights : Array[Double],
  epoch~ : Int,
  source_digest~ : UInt64,
  source_length~ : Int,
) -> MarkdownDocumentScrollGeometry {
  let tree = markdown_document_scroll_geometry_build_tree(heights)
  { heights, tree, epoch, source_digest, source_length, }
}

///|
/// O(n) Fenwick build over `heights` (1-based internal layout).
fn markdown_document_scroll_geometry_build_tree(
  heights : Array[Double],
) -> Array[Double] {
  let count = heights.length()
  let limit = count + 1
  let tree : Array[Double] = []
  for _ in 0.. Double {
  let mut i = count
  if i > geometry.heights.length() {
    i = geometry.heights.length()
  }
  if i < 0 {
    i = 0
  }
  let mut sum = 0.0
  while i > 0 {
    sum = sum + geometry.tree[i]
    i = i - (i & -i)
  }
  sum
}

///|
/// Apply a new pixel height for one block in O(log n). Heights are
/// mutable in place: every holder of this geometry sees the update.
fn markdown_document_scroll_geometry_set_height(
  geometry : MarkdownDocumentScrollGeometry,
  index~ : Int,
  height~ : Double,
) -> Unit {
  if index < 0 || index >= geometry.heights.length() {
    return
  }
  let delta = height - geometry.heights[index]
  if delta == 0.0 {
    return
  }
  geometry.heights[index] = height
  let count = geometry.heights.length()
  let mut i = index + 1
  while i <= count {
    geometry.tree[i] = geometry.tree[i] + delta
    i = i + (i & -i)
  }
}

///|
/// Cheap content identity for the session source, computed once per
/// session construction so cache keys never concatenate the document.
/// The keys it feeds (definition sets, scroll geometry) are process-local
/// caches revalidated against the length and epoch carried alongside, so
/// only injectivity matters — see `markdown_document_edit_digest` for how
/// an incremental edit chains the identity without re-hashing the
/// document.
fn markdown_document_source_digest(source : String) -> UInt64 {
  rich_text_digest_string(0xcbf29ce484222325UL, source)
}

///|
/// Chain the previous identity through one exact edit: the identity stays
/// a (probabilistically injective) function of the document's content —
/// the seed digest hashed the whole text, and every later change is
/// mixed in by position and payload — while an incremental keystroke pays
/// only for the edit strings instead of a whole-source hash.
fn markdown_document_edit_digest(
  previous : UInt64,
  length~ : Int,
  start~ : Int,
  deleted~ : String,
  replacement~ : String,
) -> UInt64 {
  let hash = rich_text_digest_mix(previous, length.to_uint64())
  let hash = rich_text_digest_mix(hash, start.to_uint64())
  let hash = rich_text_digest_string(hash, deleted)
  rich_text_digest_string(hash, replacement)
}

///|
let markdown_document_scroll_geometry_cache : Map[
  String,
  MarkdownDocumentScrollGeometry,
] = Map([])

///|
fn rich_text_cache_join_key(parts : Array[String]) -> String {
  // Separator-joined in one pass. The previous `key = key + part` copied the
  // accumulated key on every iteration, which is quadratic in the number of
  // parts; a soft-wrapped paragraph contributes one part per run, so a
  // novel-sized block (one run per source line) made building a cache key the
  // dominant cost of opening the document.
  let pieces : Array[String] = []
  for part in parts {
    pieces.push(part)
    pieces.push("\u0001")
  }
  pieces.join("")
}

///|
/// Fonts repeat constantly within a frame, so keep the last encoded key
/// instead of re-serializing the FontSpec for every block conversion.
let rich_text_cache_last_font : Ref[(@core.FontSpec, String)] = Ref(
  (@core.FontSpec::new(), ""),
)

///|
fn rich_text_cache_font_key(font : @core.FontSpec) -> String {
  let last = rich_text_cache_last_font.val
  if last.0 == font {
    last.1
  } else {
    let key = @json.to_json(font).stringify()
    rich_text_cache_last_font.val = (font, key)
    key
  }
}

///|
/// Content-identity key for a parsed block. Hand-rolled to keep the
/// measured-height walk cheap over large documents, it must stay
/// injective on the exact fields that determine layout, so every payload
/// carries its length and every field a fixed tag; changing a field or
/// adding one to `MarkdownBlock` requires mirroring it here.
fn markdown_editor_block_key(block : MarkdownBlock) -> String {
  let key = StringBuilder()
  key.write_string("b")
  match block.kind {
    Heading(level) => key.write_string("h\{level};")
    Paragraph => key.write_string("p;")
    UnorderedListItem => key.write_string("u;")
    TaskListItem(checked) =>
      key.write_string("t\{if checked { 1 } else { 0 }};")
    OrderedListItem(start) => key.write_string("o\{start};")
    Blockquote => key.write_string("q;")
    CodeBlock => key.write_string("c;")
    HorizontalRule => key.write_string("r;")
    Table => key.write_string("a;")
    FootnoteDefinition(label) => {
      key.write_string("f")
      markdown_editor_block_key_string(key, Some(label))
    }
    HtmlBlock => key.write_string("x;")
    FrontMatter => key.write_string("m;")
  }
  markdown_editor_block_key_string(key, Some(block.text))
  key.write_string("i\{block.inlines.length()};")
  for inline in block.inlines {
    let inline_tag = match inline.kind {
      Plain => '0'
      Bold => '1'
      Italic => '2'
      Code => '3'
      Strikethrough => '4'
      Highlight => 'A'
      Link => '5'
      Autolink => '6'
      Image => '7'
      FootnoteReference => '8'
      HtmlInline => '9'
    }
    key.write_char(inline_tag)
    markdown_editor_block_key_string(key, Some(inline.text))
    markdown_editor_block_key_string(key, inline.target)
  }
  key.write_string("a\{block.table_alignments.length()};")
  for align in block.table_alignments {
    let align_tag = match align {
      None => 'n'
      Some(TextStart) => 's'
      Some(TextCenter) => 'e'
      Some(TextEnd) => 'd'
    }
    key.write_char(align_tag)
  }
  markdown_editor_block_key_string(key, block.code_language)
  key.to_string()
}

///|
fn markdown_editor_block_key_string(
  key : StringBuilder,
  part : String?,
) -> Unit {
  match part {
    None => key.write_char('n')
    Some(text) => key.write_string("s\{text.length()}:\{text}")
  }
}

///|
fn rich_text_source_range_shift(
  range : RichTextSourceRange?,
  delta : Int,
) -> RichTextSourceRange? {
  match range {
    Some(range) => Some({ start: range.start + delta, end: range.end + delta, })
    None => None
  }
}

///|
/// Shift every absolute source offset inside a cached rich block by the
/// same delta. Content identity (text, runs, table shape) is unchanged;
/// only the mapping back into canonical source moves.
fn markdown_editor_rich_block_shift(
  block : RichTextBlock,
  delta : Int,
) -> RichTextBlock {
  if delta == 0 {
    return block
  }
  let runs : Array[RichTextRun] = []
  for run in block.runs {
    runs.push({
      ..run,
      source_range: rich_text_source_range_shift(run.source_range, delta),
    })
  }
  let table = match block.table {
    Some(table) =>
      Some({
        ..table,
        rows: table.rows.map(row => {
          row.map(cell => {
            ..cell,
            source_range: rich_text_source_range_shift(cell.source_range, delta),
          })
        }),
      })
    None => None
  }
  {
    ..block,
    runs,
    table,
    source_range: rich_text_source_range_shift(block.source_range, delta),
    content_range: rich_text_source_range_shift(block.content_range, delta),
  }
}

///|
/// Content-identity key for a block conversion. Deliberately excludes
/// absolute source offsets; the relative content offset is kept because
/// marker prefix widths (indent, quote layers) shift it independently of
/// the block text.
fn markdown_editor_block_conversion_key(
  editor_block : MarkdownEditorBlock,
  content : String,
  source_slice : String,
  base : @core.FontSpec,
  base_dir~ : String?,
) -> String {
  rich_text_cache_join_key([
    rich_text_cache_font_key(base),
    base_dir.unwrap_or(""),
    markdown_editor_block_key(editor_block.block),
    source_slice,
    content,
    (editor_block.content_range.start - editor_block.source_range.start).to_string(),
  ])
}

///|

///|
/// Whole-source reference-definition set, keyed by (source length, source
/// digest) instead of the source string itself: a String-keyed map hash of
/// a multi-megabyte document costs O(document) per lookup, while the
/// session already knows its (length, digest) pair. The digest is computed
/// once per cache entry, never per lookup.
fn markdown_reference_definitions_cache_key(
  source_length~ : Int,
  source_digest~ : UInt64,
) -> String {
  "defs:\{source_length}:\{source_digest}"
}

///|
fn markdown_reference_definitions_cached(
  source : String,
) -> Array[MarkdownReferenceDefinition] {
  markdown_reference_definitions_cached_keyed(
    source,
    source_length=source.length(),
    source_digest=markdown_document_source_digest(source),
  )
}

///|
/// Lookup with a session-supplied (length, digest) pair, so the hot edit
/// path never hashes the document just to consult the cache.
fn markdown_reference_definitions_cached_keyed(
  source : String,
  source_length~ : Int,
  source_digest~ : UInt64,
) -> Array[MarkdownReferenceDefinition] {
  let key = markdown_reference_definitions_cache_key(
    source_length~,
    source_digest~,
  )
  match markdown_reference_definitions_cache.get(key) {
    Some(definitions) => definitions
    None => {
      // The miss resolution must stay cheap: with the full-parser union a
      // miss charged an interactive keystroke a whole-document markdown
      // parse (~700 ms on a 5 MB fixture), and the 16-entry cache evicts
      // during ordinary typing, so misses recur every few edits.
      let definitions = markdown_reference_definitions_scan_only(source)
      markdown_reference_definitions_cache_store_keyed(
        source_length~,
        source_digest~,
        definitions,
      )
      definitions
    }
  }
}

///|
fn markdown_reference_definitions_cache_store_keyed(
  source_length~ : Int,
  source_digest~ : UInt64,
  definitions : Array[MarkdownReferenceDefinition],
) -> Unit {
  let key = markdown_reference_definitions_cache_key(
    source_length~,
    source_digest~,
  )
  if markdown_reference_definitions_cache.get(key) is None {
    if markdown_reference_definitions_cache.length() >= 16 {
      markdown_reference_definitions_cache.clear()
    }
    markdown_reference_definitions_cache.set(key, definitions)
  }
}

///|
/// Memoize the editor block parse for one source. Block-context queries
/// are pure functions of the parse, and a single keystroke or command
/// asks several of them, so one parse per source serves them all. The
/// entry duplicates the document's block text plus inline arrays, so the
/// cap stays at 2; callers get a shallow copy to keep the cached array's
/// identity private (the model itself is never mutated in place).
fn parse_markdown_editor_blocks_cached(
  source : String,
) -> Array[MarkdownEditorBlock] {
  match markdown_editor_blocks_cache.get(source) {
    Some(blocks) => blocks.copy()
    None => {
      let blocks = parse_markdown_editor_blocks_uncached(source)
      markdown_editor_blocks_cache_put(source, blocks)
      blocks.copy()
    }
  }
}

///|
/// Publish a parse result produced elsewhere (the session's incremental
/// apply stitches blocks without re-parsing the whole source) so the
/// next keystroke's block-context queries resolve from the cache.
fn markdown_editor_blocks_cache_put(
  source : String,
  blocks : Array[MarkdownEditorBlock],
) -> Unit {
  if markdown_editor_blocks_cache.length() >= 2 {
    markdown_editor_blocks_cache.clear()
  }
  markdown_editor_blocks_cache.set(source, blocks)
}

///|
/// Memoize `parse_markdown` per text. The block pass consults the same
/// lines repeatedly (setext candidate, soft-wrap line, paragraph
/// fallback) and `parse_markdown` runs a full mizchi parse to collect
/// definitions per call, so without this each keystroke parses every
/// line 2–3 times. The cap is sized to keep the ±few-line reuse window
/// of the block pass hot while bounding memory to sub-line payloads.
fn parse_markdown_cached(text : String) -> Array[MarkdownBlock] {
  match markdown_parse_cache.get(text) {
    Some(blocks) => blocks.copy()
    None => {
      let blocks = parse_markdown_uncached(text)
      if markdown_parse_cache.length() >= 1024 {
        markdown_parse_cache.clear()
      }
      markdown_parse_cache.set(text, blocks)
      blocks.copy()
    }
  }
}

///|
fn markdown_editor_block_to_rich_text_with_cache(
  source : String,
  editor_block : MarkdownEditorBlock,
  base : @core.FontSpec,
  base_dir~ : String?,
  build~ : (String, MarkdownEditorBlock, @core.FontSpec, String?) -> RichTextBlock,
) -> RichTextBlock {
  let content = markdown_editor_run_source_content(source, editor_block)
  let source_slice = markdown_editor_substring(
    source,
    editor_block.source_range.start,
    editor_block.source_range.end,
  )
  let key = markdown_editor_block_conversion_key(
    editor_block,
    content,
    source_slice,
    base,
    base_dir~,
  )
  match rich_text_block_conversions.get(key) {
    Some(entry) =>
      markdown_editor_rich_block_shift(
        entry.block,
        editor_block.source_range.start - entry.source_start,
      )
    None => {
      hot_path_block_conversion_builds.val = hot_path_block_conversion_builds.val +
        1
      let block = build(source, editor_block, base, base_dir)
      if rich_text_block_conversions.length() >= 4096 {
        rich_text_block_conversions.clear()
      }
      rich_text_block_conversions.set(key, {
        source_start: editor_block.source_range.start,
        block,
      })
      block
    }
  }
}

///|
/// Content-identity key for a source-mode block. The rendered text is
/// the whole input (monospace, no inline parsing), so the visible slice
/// plus font fully determines the block up to a source rebase.
fn markdown_document_source_mode_block_to_rich_text_with_cache(
  text : String,
  editor_block : MarkdownEditorBlock,
  base : @core.FontSpec,
  build~ : (MarkdownEditorBlock, @core.FontSpec) -> RichTextBlock,
) -> RichTextBlock {
  let key = rich_text_cache_join_key([
    "source-mode",
    rich_text_cache_font_key(base),
    text,
  ])
  match rich_text_block_conversions.get(key) {
    Some(entry) =>
      markdown_editor_rich_block_shift(
        entry.block,
        editor_block.source_range.start - entry.source_start,
      )
    None => {
      let block = build(editor_block, base)
      if rich_text_block_conversions.length() >= 4096 {
        rich_text_block_conversions.clear()
      }
      rich_text_block_conversions.set(key, {
        source_start: editor_block.source_range.start,
        block,
      })
      block
    }
  }
}

///|
fn rich_text_run_width_with_cache(
  text : String,
  font : @core.FontSpec,
  text_system~ : @core.TextSystem,
  measure~ : (String, @core.FontSpec) -> Double,
) -> Double {
  let key = rich_text_cache_join_key([
    text_system.id(),
    rich_text_cache_font_key(font),
    text,
  ])
  match rich_text_run_widths.get(key) {
    Some(width) => width
    None => {
      let width = measure(text, font)
      if rich_text_run_widths.length() >= 65536 {
        rich_text_run_widths.clear()
      }
      rich_text_run_widths.set(key, width)
      width
    }
  }
}

///|
/// Measured-height keys cover the inputs that determine a block's laid
/// out pixel height: block identity (content via id+revision), base font,
/// and the laid out block width. Like block conversions, absolute source
/// offsets are excluded so upstream edits keep the entry valid.
fn rich_text_block_measured_height_key(
  base : @core.FontSpec,
  width~ : Double,
  block_id~ : Int,
  block_revision~ : Int,
  source_digest? : UInt64? = None,
) -> String {
  rich_text_cache_join_key([
    "height",
    rich_text_cache_font_key(base),
    width.to_string(),
    // Block ids are session-local and restart at 1 for a new document. Keep
    // independent documents from reusing one another's measured geometry.
    match source_digest {
      Some(digest) => digest.to_string()
      None => "legacy"
    },
    block_id.to_string(),
    block_revision.to_string(),
  ])
}

///|

///|
/// Exact (key, height) lookup inside a digest bucket. Buckets hold only
/// the handful of distinct blocks sharing a digest, so a linear scan is
/// the cheapest verification step.
fn rich_text_bucket_lookup(
  bucket : Array[(String, Double)],
  key~ : String,
) -> Double? {
  let mut found : Double? = None
  for pair in bucket {
    if pair.0 == key {
      found = Some(pair.1)
      break
    }
  }
  found
}

///|
/// FNV-1a mixing over the same identity inputs as the exact key, but
/// without materializing any string: the geometry walk hashes 100k+
/// blocks per revision, so the digest must stay allocation-light.
fn rich_text_digest_mix(hash : UInt64, value : UInt64) -> UInt64 {
  let mut hash = hash ^ value
  hash = hash * 0x100000001b3UL
  hash
}

///|
fn rich_text_digest_string(hash : UInt64, text : String) -> UInt64 {
  let mut hash = rich_text_digest_mix(hash, text.length().to_uint64())
  for ch in text {
    hash = rich_text_digest_mix(hash, ch.to_int().to_uint64())
  }
  hash
}

///|
/// Seed mixing the two walk-wide digest inputs — font identity and the
/// laid-out block width — once per geometry walk instead of re-hashing the
/// serialized font key (and re-allocating the width string) for every
/// block. The per-block digest chains the block identity into this seed.
fn rich_text_block_height_digest_seed(
  base : @core.FontSpec,
  width~ : Double,
  source_digest? : UInt64? = None,
) -> UInt64 {
  let hash = rich_text_digest_string(
    0xcbf29ce484222325UL,
    rich_text_cache_font_key(base),
  )
  let hash = rich_text_digest_string(hash, width.to_string())
  match source_digest {
    Some(digest) => rich_text_digest_mix(hash, digest)
    None => hash
  }
}

///|
/// Digest keying one measured-height entry. It hashes the block's
/// *identity* (id, revision) rather than its content: the session's block
/// assignment guarantees content-identical blocks keep id and revision
/// across incremental applies and full re-parses (both match by content
/// fingerprint), and any content change bumps the revision. This keeps the
/// geometry walk O(1) per block instead of re-hashing every block's full
/// text (O(document) per walk) while preserving the exact invalidation
/// semantics of the content-keyed digest.
fn rich_text_block_height_digest(
  seed~ : UInt64,
  block_id~ : Int,
  block_revision~ : Int,
) -> UInt64 {
  let hash = rich_text_digest_mix(seed, block_id.to_uint64())
  rich_text_digest_mix(hash, block_revision.to_uint64())
}

///|
/// Record a measured pixel height for a block. Returns `true` when the
/// table actually gained or changed an entry (the caller then applies the
/// same height to the live scroll geometry through the point-update
/// path); a repeated identical measurement returns `false`. Only the
/// wholesale eviction bumps the epoch — per-block changes no longer
/// invalidate cached scroll geometry.
fn rich_text_record_block_measured_height(
  base : @core.FontSpec,
  width~ : Double,
  block_id~ : Int,
  block_revision~ : Int,
  height~ : Double,
  source_digest? : UInt64? = None,
) -> Bool {
  if width <= 0.0 || height <= 0.0 {
    return false
  }
  let seed = rich_text_block_height_digest_seed(base, width~, source_digest~)
  let digest = rich_text_block_height_digest(seed~, block_id~, block_revision~)
  let key = rich_text_block_measured_height_key(
    base,
    width~,
    block_id~,
    block_revision~,
    source_digest~,
  )
  let old_bucket = rich_text_block_heights.get(digest)
  match old_bucket {
    Some(bucket) =>
      match rich_text_bucket_lookup(bucket, key~) {
        Some(recorded) => if recorded == height { return false }
        None => ()
      }
    None => ()
  }
  if rich_text_block_heights_count.val >= 32768 {
    rich_text_block_heights.clear()
    rich_text_block_heights_count.val = 0
    rich_text_block_heights_epoch.val = rich_text_block_heights_epoch.val + 1
  }
  let next_bucket = match old_bucket {
    Some(bucket) => {
      let filtered = bucket.filter(pair => pair.0 != key)
      filtered.push((key, height))
      filtered
    }
    None => [(key, height)]
  }
  let old_length = match old_bucket {
    Some(bucket) => bucket.length()
    None => 0
  }
  rich_text_block_heights_count.val = rich_text_block_heights_count.val -
    old_length +
    next_bucket.length()
  rich_text_block_heights.set(digest, next_bucket)
  true
}

///|
/// Read the live scroll geometry for a (font, content width) pair. The
/// entry carries its content identity (`source_digest`/`source_length`);
/// callers decide between an exact hit, an in-place rebase, or a full
/// rebuild. Returns `None` when no entry exists or the entry predates the
/// current measured-height epoch (the epoch only bumps on a wholesale
/// measured-height eviction; single-block height changes are applied in
/// place through the Fenwick point update).
fn markdown_document_scroll_geometry_get(
  base : @core.FontSpec,
  width~ : Double,
) -> MarkdownDocumentScrollGeometry? {
  let key = markdown_document_scroll_geometry_key(base, width~)
  match markdown_document_scroll_geometry_cache.get(key) {
    Some(entry) =>
      if entry.epoch == rich_text_block_heights_epoch.val {
        Some(entry)
      } else {
        None
      }
    None => None
  }
}

///|
fn markdown_document_scroll_geometry_key(
  base : @core.FontSpec,
  width~ : Double,
) -> String {
  rich_text_cache_join_key([
    "scroll-geometry",
    rich_text_cache_font_key(base),
    width.to_string(),
  ])
}

///|
fn markdown_document_scroll_geometry_put(
  base : @core.FontSpec,
  geometry : MarkdownDocumentScrollGeometry,
  width~ : Double,
) -> Unit {
  let key = markdown_document_scroll_geometry_key(base, width~)
  if markdown_document_scroll_geometry_cache.length() >= 4 &&
    !markdown_document_scroll_geometry_cache.contains(key) {
    markdown_document_scroll_geometry_cache.clear()
  }
  markdown_document_scroll_geometry_cache.set(key, geometry)
}

///|
fn rich_text_block_measured_height(
  base : @core.FontSpec,
  width~ : Double,
  seed~ : UInt64,
  block_id~ : Int,
  block_revision~ : Int,
  source_digest? : UInt64? = None,
) -> Double? {
  if width <= 0.0 {
    return None
  }
  // Before the first measured height lands (a fresh process opening a
  // document) every lookup would miss, so skip the digest entirely
  // instead of computing it once per block.
  if rich_text_block_heights_count.val == 0 {
    return None
  }
  let digest = rich_text_block_height_digest(seed~, block_id~, block_revision~)
  match rich_text_block_heights.get(digest) {
    None => None
    Some(bucket) => {
      // A digest hit is the only case that pays for the exact key, so
      // misses stay allocation-free while hits remain identity-verified.
      let key = rich_text_block_measured_height_key(
        base,
        width~,
        block_id~,
        block_revision~,
        source_digest~,
      )
      rich_text_bucket_lookup(bucket, key~)
    }
  }
}

///|
/// Memoize the full-document parse behind `MarkdownDocumentSession::new`
/// for the IME composition path, where paints and host IME polls repeat
/// with the same display text while the candidate is open. The session
/// is a pure function of its source text, so the memo is content-keyed
/// across documents as well.
fn markdown_document_session_parse_cached(
  source : String,
) -> MarkdownDocumentSession {
  match rich_text_display_sessions.get(source) {
    Some(session) => session
    None => {
      let session = MarkdownDocumentSession::new(source)
      if rich_text_display_sessions.length() >= 4 {
        rich_text_display_sessions.clear()
      }
      rich_text_display_sessions.set(source, session)
      session
    }
  }
}