///|
/// Check if JS-side text metrics provider is available
extern "js" fn has_paint_text_provider() -> Bool =
  #|() => typeof globalThis.__craterMeasureTextIntrinsic === "function"

///|
/// Call font-family-aware text metrics provider
extern "js" fn call_font_aware_text_provider(
  text : String,
  font_size : Double,
  line_height : Double,
  white_space : String,
  writing_mode : String,
  available_width : Double,
  available_height : Double,
  font_family : String,
  is_bold : Bool,
) -> String =
  #|(text, fontSize, lineHeight, whiteSpace, writingMode, availableWidth, availableHeight, fontFamily, isBold) => {
  #|  // Try multi-font provider first
  #|  const multiFn = globalThis.__craterMeasureTextIntrinsicMultiFull;
  #|  if (typeof multiFn === "function") {
  #|    try {
  #|      const result = multiFn(text, fontSize, lineHeight, whiteSpace, writingMode, availableWidth, availableHeight, fontFamily, isBold);
  #|      if (result) {
  #|        const mw = Number(result.minWidth ?? result.min_width);
  #|        const xw = Number(result.maxWidth ?? result.max_width);
  #|        const mh = Number(result.minHeight ?? result.min_height);
  #|        const xh = Number(result.maxHeight ?? result.max_height);
  #|        if (Number.isFinite(mw) && Number.isFinite(xw) && Number.isFinite(mh) && Number.isFinite(xh))
  #|          return `${mw},${xw},${mh},${xh}`;
  #|      }
  #|    } catch {}
  #|  }
  #|  // Fallback to default provider
  #|  const fn = isBold ? (globalThis.__craterMeasureTextIntrinsicBold || globalThis.__craterMeasureTextIntrinsic) : globalThis.__craterMeasureTextIntrinsic;
  #|  if (typeof fn !== "function") return "";
  #|  try {
  #|    const result = fn(text, fontSize, lineHeight, whiteSpace, writingMode, fontFamily, availableWidth, availableHeight);
  #|    let minWidth, maxWidth, minHeight, maxHeight;
  #|    if (result && typeof result === "object") {
  #|      minWidth = Number(result.minWidth ?? result.min_width);
  #|      maxWidth = Number(result.maxWidth ?? result.max_width);
  #|      minHeight = Number(result.minHeight ?? result.min_height);
  #|      maxHeight = Number(result.maxHeight ?? result.max_height);
  #|    } else return "";
  #|    if (!Number.isFinite(minWidth) || !Number.isFinite(maxWidth)) return "";
  #|    return `${minWidth},${maxWidth},${minHeight},${maxHeight}`;
  #|  } catch { return ""; }
  #|}

///|
/// Call JS-side text metrics provider (returns CSV: "minWidth,maxWidth,minHeight,maxHeight")
extern "js" fn call_paint_text_provider(
  text : String,
  font_size : Double,
  line_height : Double,
  white_space : String,
  writing_mode : String,
  font_family : String,
  available_width : Double,
  available_height : Double,
) -> String =
  #|(text, fontSize, lineHeight, whiteSpace, writingMode, fontFamily, availableWidth, availableHeight) => {
  #|  const fn = globalThis.__craterMeasureTextIntrinsic;
  #|  if (typeof fn !== "function") return "";
  #|  try {
  #|    const result = fn(text, fontSize, lineHeight, whiteSpace, writingMode, fontFamily, availableWidth, availableHeight);
  #|    let minWidth, maxWidth, minHeight, maxHeight;
  #|    if (Array.isArray(result) && result.length >= 4) {
  #|      minWidth = Number(result[0]); maxWidth = Number(result[1]);
  #|      minHeight = Number(result[2]); maxHeight = Number(result[3]);
  #|    } else if (result && typeof result === "object") {
  #|      minWidth = Number(result.minWidth ?? result.min_width);
  #|      maxWidth = Number(result.maxWidth ?? result.max_width ?? minWidth);
  #|      minHeight = Number(result.minHeight ?? result.min_height);
  #|      maxHeight = Number(result.maxHeight ?? result.max_height ?? minHeight);
  #|    } else { return ""; }
  #|    if (!Number.isFinite(minWidth) || !Number.isFinite(maxWidth) || !Number.isFinite(minHeight) || !Number.isFinite(maxHeight)) return "";
  #|    return `${minWidth},${maxWidth},${minHeight},${maxHeight}`;
  #|  } catch { return ""; }
  #|}

///|
/// Check if JS-side outline commands provider is available (avoids SVG string roundtrip)
extern "js" fn has_outline_commands() -> Bool =
  #|() => typeof globalThis.__craterGlyphOutlineCommands === "function"

///|
/// Get glyph outline commands as JSON string: [[type, ...args], ...]
extern "js" fn js_glyph_outline_commands(
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
) -> String =
  #|(cp, fs, bold) => {
  #|  const fn = bold ? (globalThis.__craterGlyphOutlineCommandsBold || globalThis.__craterGlyphOutlineCommands) : globalThis.__craterGlyphOutlineCommands;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs) || ""); } catch { return ""; }
  #|}

///|
/// Get glyph outline commands for a specific font family as JSON string
extern "js" fn js_outline_commands_for_family(
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
  font_family : String,
) -> String =
  #|(cp, fs, bold, ff) => {
  #|  const fn = globalThis.__craterOutlineCommandsForFamily;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs, bold, ff) || ""); } catch { return ""; }
  #|}

///|
/// Check if JS-side glyph functions are available (mizchi/font loaded)
extern "js" fn has_glyph_functions() -> Bool =
  #|() => typeof globalThis.__craterGlyphToSvgPath === "function"

///|
/// Get SVG path string for a glyph
extern "js" fn js_glyph_to_svg_path(
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
) -> String =
  #|(cp, fs, bold) => {
  #|  const fn = bold ? (globalThis.__craterGlyphToSvgPathBold || globalThis.__craterGlyphToSvgPath) : globalThis.__craterGlyphToSvgPath;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs) || ""); } catch { return ""; }
  #|}

///|
/// Get advance width for a glyph
extern "js" fn js_glyph_advance(
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
) -> Double =
  #|(cp, fs, bold) => {
  #|  const fn = bold ? (globalThis.__craterGlyphAdvanceBold || globalThis.__craterGlyphAdvance) : globalThis.__craterGlyphAdvance;
  #|  if (typeof fn !== "function") return fs * 0.5;
  #|  try { const v = Number(fn(cp, fs)); return Number.isFinite(v) ? v : fs * 0.5; } catch { return fs * 0.5; }
  #|}

///|
/// Get kerning between two codepoints
extern "js" fn js_kern_advance(
  cp1 : Int,
  cp2 : Int,
  font_size : Double,
  is_bold : Bool,
) -> Double =
  #|(cp1, cp2, fs, bold) => {
  #|  const fn = bold ? (globalThis.__craterKernAdvanceBold || globalThis.__craterKernAdvance) : globalThis.__craterKernAdvance;
  #|  if (typeof fn !== "function") return 0;
  #|  try { const v = Number(fn(cp1, cp2, fs)); return Number.isFinite(v) ? v : 0; } catch { return 0; }
  #|}

///|
/// Get font ascent ratio (ascent / unitsPerEm)
extern "js" fn js_font_ascent_ratio() -> Double =
  #|() => {
  #|  const fn = globalThis.__craterFontAscentRatio;
  #|  if (typeof fn === "function") { const v = Number(fn()); return Number.isFinite(v) ? v : 0.8; }
  #|  return 0.8;
  #|}

///|
/// Multi-font glyph: get SVG path for a codepoint using font_family
extern "js" fn js_glyph_for_family(
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
  font_family : String,
) -> String =
  #|(cp, fs, bold, ff) => {
  #|  const fn = globalThis.__craterGlyphForFamily;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs, bold, ff) || ""); } catch { return ""; }
  #|}

///|
extern "js" fn js_advance_for_family(
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
  font_family : String,
) -> Double =
  #|(cp, fs, bold, ff) => {
  #|  const fn = globalThis.__craterAdvanceForFamily;
  #|  if (typeof fn !== "function") return fs * 0.5;
  #|  try { const v = Number(fn(cp, fs, bold, ff)); return Number.isFinite(v) ? v : fs * 0.5; } catch { return fs * 0.5; }
  #|}

///|
extern "js" fn js_kern_for_family(
  cp1 : Int,
  cp2 : Int,
  font_size : Double,
  is_bold : Bool,
  font_family : String,
) -> Double =
  #|(cp1, cp2, fs, bold, ff) => {
  #|  const fn = globalThis.__craterKernForFamily;
  #|  if (typeof fn !== "function") return 0;
  #|  try { const v = Number(fn(cp1, cp2, fs, bold, ff)); return Number.isFinite(v) ? v : 0; } catch { return 0; }
  #|}

///|
extern "js" fn js_ascent_for_family(font_family : String) -> Double =
  #|(ff) => {
  #|  const fn = globalThis.__craterAscentForFamily;
  #|  if (typeof fn !== "function") return 0.8;
  #|  try { const v = Number(fn(ff)); return Number.isFinite(v) ? v : 0.8; } catch { return 0.8; }
  #|}

///|
/// Numeric-weight-aware glyph outline JSON. Falls back to the boolean-bold
/// dispatch when no by-weight hook is installed, so legacy JS providers keep
/// working.
extern "js" fn js_glyph_outline_commands_by_weight(
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
) -> String =
  #|(cp, fs, weight) => {
  #|  const byWeight = globalThis.__craterGlyphOutlineCommandsByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { return String(byWeight(cp, fs, weight) || ""); } catch { return ""; }
  #|  }
  #|  const bold = weight >= 600;
  #|  const fn = bold ? (globalThis.__craterGlyphOutlineCommandsBold || globalThis.__craterGlyphOutlineCommands) : globalThis.__craterGlyphOutlineCommands;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs) || ""); } catch { return ""; }
  #|}

///|
extern "js" fn js_glyph_to_svg_path_by_weight(
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
) -> String =
  #|(cp, fs, weight) => {
  #|  const byWeight = globalThis.__craterGlyphToSvgPathByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { return String(byWeight(cp, fs, weight) || ""); } catch { return ""; }
  #|  }
  #|  const bold = weight >= 600;
  #|  const fn = bold ? (globalThis.__craterGlyphToSvgPathBold || globalThis.__craterGlyphToSvgPath) : globalThis.__craterGlyphToSvgPath;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs) || ""); } catch { return ""; }
  #|}

///|
extern "js" fn js_glyph_advance_by_weight(
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
) -> Double =
  #|(cp, fs, weight) => {
  #|  const byWeight = globalThis.__craterGlyphAdvanceByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { const v = Number(byWeight(cp, fs, weight)); return Number.isFinite(v) ? v : fs * 0.5; } catch { return fs * 0.5; }
  #|  }
  #|  const bold = weight >= 600;
  #|  const fn = bold ? (globalThis.__craterGlyphAdvanceBold || globalThis.__craterGlyphAdvance) : globalThis.__craterGlyphAdvance;
  #|  if (typeof fn !== "function") return fs * 0.5;
  #|  try { const v = Number(fn(cp, fs)); return Number.isFinite(v) ? v : fs * 0.5; } catch { return fs * 0.5; }
  #|}

///|
extern "js" fn js_kern_advance_by_weight(
  cp1 : Int,
  cp2 : Int,
  font_size : Double,
  font_weight : Double,
) -> Double =
  #|(cp1, cp2, fs, weight) => {
  #|  const byWeight = globalThis.__craterKernAdvanceByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { const v = Number(byWeight(cp1, cp2, fs, weight)); return Number.isFinite(v) ? v : 0; } catch { return 0; }
  #|  }
  #|  const bold = weight >= 600;
  #|  const fn = bold ? (globalThis.__craterKernAdvanceBold || globalThis.__craterKernAdvance) : globalThis.__craterKernAdvance;
  #|  if (typeof fn !== "function") return 0;
  #|  try { const v = Number(fn(cp1, cp2, fs)); return Number.isFinite(v) ? v : 0; } catch { return 0; }
  #|}

///|
extern "js" fn js_outline_commands_for_family_by_weight(
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
  font_family : String,
) -> String =
  #|(cp, fs, weight, ff) => {
  #|  const byWeight = globalThis.__craterOutlineCommandsForFamilyByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { return String(byWeight(cp, fs, weight, ff) || ""); } catch { return ""; }
  #|  }
  #|  const fn = globalThis.__craterOutlineCommandsForFamily;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs, weight >= 600, ff) || ""); } catch { return ""; }
  #|}

///|
extern "js" fn js_glyph_for_family_by_weight(
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
  font_family : String,
) -> String =
  #|(cp, fs, weight, ff) => {
  #|  const byWeight = globalThis.__craterGlyphForFamilyByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { return String(byWeight(cp, fs, weight, ff) || ""); } catch { return ""; }
  #|  }
  #|  const fn = globalThis.__craterGlyphForFamily;
  #|  if (typeof fn !== "function") return "";
  #|  try { return String(fn(cp, fs, weight >= 600, ff) || ""); } catch { return ""; }
  #|}

///|
extern "js" fn js_advance_for_family_by_weight(
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
  font_family : String,
) -> Double =
  #|(cp, fs, weight, ff) => {
  #|  const byWeight = globalThis.__craterAdvanceForFamilyByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { const v = Number(byWeight(cp, fs, weight, ff)); return Number.isFinite(v) ? v : fs * 0.5; } catch { return fs * 0.5; }
  #|  }
  #|  const fn = globalThis.__craterAdvanceForFamily;
  #|  if (typeof fn !== "function") return fs * 0.5;
  #|  try { const v = Number(fn(cp, fs, weight >= 600, ff)); return Number.isFinite(v) ? v : fs * 0.5; } catch { return fs * 0.5; }
  #|}

///|
extern "js" fn js_kern_for_family_by_weight(
  cp1 : Int,
  cp2 : Int,
  font_size : Double,
  font_weight : Double,
  font_family : String,
) -> Double =
  #|(cp1, cp2, fs, weight, ff) => {
  #|  const byWeight = globalThis.__craterKernForFamilyByWeight;
  #|  if (typeof byWeight === "function") {
  #|    try { const v = Number(byWeight(cp1, cp2, fs, weight, ff)); return Number.isFinite(v) ? v : 0; } catch { return 0; }
  #|  }
  #|  const fn = globalThis.__craterKernForFamily;
  #|  if (typeof fn !== "function") return 0;
  #|  try { const v = Number(fn(cp1, cp2, fs, weight >= 600, ff)); return Number.isFinite(v) ? v : 0; } catch { return 0; }
  #|}

///|
extern "js" fn has_bold_text_provider() -> Bool =
  #|() => typeof globalThis.__craterMeasureTextIntrinsicBold === "function"

///|
extern "js" fn call_bold_text_provider(
  text : String,
  font_size : Double,
  line_height : Double,
  white_space : String,
  writing_mode : String,
  font_family : String,
  available_width : Double,
  available_height : Double,
) -> String =
  #|(text, fontSize, lineHeight, whiteSpace, writingMode, fontFamily, availableWidth, availableHeight) => {
  #|  const fn = globalThis.__craterMeasureTextIntrinsicBold;
  #|  if (typeof fn !== "function") return "";
  #|  try {
  #|    const result = fn(text, fontSize, lineHeight, whiteSpace, writingMode, fontFamily, availableWidth, availableHeight);
  #|    let minWidth, maxWidth, minHeight, maxHeight;
  #|    if (result && typeof result === "object") {
  #|      minWidth = Number(result.minWidth ?? result.min_width);
  #|      maxWidth = Number(result.maxWidth ?? result.max_width ?? minWidth);
  #|      minHeight = Number(result.minHeight ?? result.min_height);
  #|      maxHeight = Number(result.maxHeight ?? result.max_height ?? minHeight);
  #|    } else { return ""; }
  #|    if (!Number.isFinite(minWidth) || !Number.isFinite(maxWidth) || !Number.isFinite(minHeight) || !Number.isFinite(maxHeight)) return "";
  #|    return `${minWidth},${maxWidth},${minHeight},${maxHeight}`;
  #|  } catch { return ""; }
  #|}

///|
fn ensure_paint_text_provider() -> Unit {
  if has_paint_text_provider() {
    let provider = @renderer.TextMetricsProvider::new(fn(
      text,
      font_size,
      line_height,
      white_space,
      writing_mode,
      font_weight,
      font_family,
    ) {
      let ws = white_space.to_string()
      let wm = writing_mode.to_string()
      let is_bold = font_weight >= 600.0
      let ff = font_family
      {
        func: fn(available_width, available_height) {
          let payload = if @rendering.should_use_font_aware_text_provider(ff) {
            let multi = call_font_aware_text_provider(
              text, font_size, line_height, ws, wm, available_width, available_height,
              ff, is_bold,
            )
            if multi.length() > 0 {
              multi
            } else if is_bold && has_bold_text_provider() {
              call_bold_text_provider(
                text, font_size, line_height, ws, wm, ff, available_width, available_height,
              )
            } else {
              call_paint_text_provider(
                text, font_size, line_height, ws, wm, ff, available_width, available_height,
              )
            }
          } else if is_bold && has_bold_text_provider() {
            call_bold_text_provider(
              text, font_size, line_height, ws, wm, ff, available_width, available_height,
            )
          } else {
            call_paint_text_provider(
              text, font_size, line_height, ws, wm, ff, available_width, available_height,
            )
          }
          @rendering.resolve_text_intrinsic_size_from_provider_payload(
            payload, text, font_size, line_height,
          )
        },
      }
    })
    @renderer.set_text_metrics_provider(provider)
  }
}

///|
let paint_provider_initialized : Ref[Bool] = { val: false }

///|
/// Reset paint provider state on new WebSocket session.
/// Must be called when JS runtime is reset to avoid stale glyph provider.
pub fn reset_paint_provider() -> Unit {
  paint_provider_initialized.val = false
  @glyph.clear_glyph_provider()
  @renderer.clear_text_metrics_provider()
}

///|
extern "js" fn js_now_ms() -> Int =
  #|() => Date.now()

///|
extern "js" fn js_log_stderr(msg : String) -> Unit =
  #|(msg) => { console.error("[paint] " + msg); }

///|
priv struct PaintCaptureFrame {
  width : Int
  height : Int
  framebuffer : @tui_paint_export.Framebuffer
  palette : @tui_paint_export.DynamicPalette
  timing : @rendering.PaintCaptureTiming
  visual : @tui_paint_export.VisualStats?
  paint_node : @paint_model.PaintNode
}

///|
/// Set up MoonBit TextMetricsProvider from JS globalThis.__craterMeasureTextIntrinsic.
/// The JS-side provider is set up by start-with-font.ts at BiDi server startup.
fn ensure_paint_provider_initialized() -> Unit {
  if paint_provider_initialized.val {
    return
  }
  paint_provider_initialized.val = true
  let has_provider = has_paint_text_provider()
  js_log_stderr("has_paint_text_provider=" + has_provider.to_string())
  ensure_paint_text_provider()
  // Set up glyph provider for text rendering
  // Prefer outline commands (JSON array) over SVG string to avoid parse_path roundtrip
  let use_outline_commands = has_outline_commands()
  if use_outline_commands || has_glyph_functions() {
    if use_outline_commands {
      js_log_stderr(
        "Installing glyph provider (outline commands, no SVG roundtrip)",
      )
    } else {
      js_log_stderr("Installing glyph provider (SVG string fallback)")
    }
    let ascent_ratio = js_font_ascent_ratio()
    js_log_stderr("ascent_ratio=" + ascent_ratio.to_string())
    let glyph_provider : @glyph.GlyphProvider = {
      ascent_ratio,
      get_kern: fn(cp1, cp2, font_size, is_bold) {
        js_kern_advance(cp1, cp2, font_size, is_bold)
      },
      get_glyph: fn(codepoint, font_size, is_bold) {
        resolve_js_glyph_commands(
          use_outline_commands, codepoint, font_size, is_bold,
        )
      },
      get_glyph_by_weight: Some(fn(codepoint, font_size, font_weight) {
        resolve_js_glyph_commands_by_weight(
          use_outline_commands, codepoint, font_size, font_weight,
        )
      }),
      get_kern_by_weight: Some(fn(cp1, cp2, font_size, font_weight) {
        js_kern_advance_by_weight(cp1, cp2, font_size, font_weight)
      }),
      get_glyph_for_family: Some(fn(
        codepoint,
        font_size,
        is_bold,
        font_family,
      ) {
        resolve_js_family_glyph_commands(
          use_outline_commands, codepoint, font_size, is_bold, font_family,
        )
      }),
      get_kern_for_family: Some(fn(cp1, cp2, font_size, is_bold, font_family) {
        js_kern_for_family(cp1, cp2, font_size, is_bold, font_family)
      }),
      get_glyph_for_family_by_weight: Some(fn(
        codepoint,
        font_size,
        font_weight,
        font_family,
      ) {
        resolve_js_family_glyph_commands_by_weight(
          use_outline_commands, codepoint, font_size, font_weight, font_family,
        )
      }),
      get_kern_for_family_by_weight: Some(fn(
        cp1,
        cp2,
        font_size,
        font_weight,
        font_family,
      ) {
        js_kern_for_family_by_weight(
          cp1, cp2, font_size, font_weight, font_family,
        )
      }),
      get_ascent_for_family: Some(fn(font_family) {
        js_ascent_for_family(font_family)
      }),
    }
    @glyph.set_glyph_provider(glyph_provider)
  }
}

///|
fn resolve_js_glyph_commands(
  use_outline_commands : Bool,
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
) -> (Array[@svg.PathCommand], Double) {
  let advance = js_glyph_advance(codepoint, font_size, is_bold)
  if use_outline_commands {
    let json = js_glyph_outline_commands(codepoint, font_size, is_bold)
    if json.length() == 0 || json == "[]" {
      return ([], advance)
    }
    let commands = @rendering.parse_glyph_outline_commands_json(json)
    (commands, advance)
  } else {
    let svg_path = js_glyph_to_svg_path(codepoint, font_size, is_bold)
    if svg_path.length() == 0 {
      return ([], advance)
    }
    let commands = @svg.parse_path(svg_path)
    (commands, advance)
  }
}

///|
fn resolve_js_glyph_commands_by_weight(
  use_outline_commands : Bool,
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
) -> (Array[@svg.PathCommand], Double) {
  let advance = js_glyph_advance_by_weight(codepoint, font_size, font_weight)
  if use_outline_commands {
    let json = js_glyph_outline_commands_by_weight(
      codepoint, font_size, font_weight,
    )
    if json.length() == 0 || json == "[]" {
      return ([], advance)
    }
    let commands = @rendering.parse_glyph_outline_commands_json(json)
    (commands, advance)
  } else {
    let svg_path = js_glyph_to_svg_path_by_weight(
      codepoint, font_size, font_weight,
    )
    if svg_path.length() == 0 {
      return ([], advance)
    }
    let commands = @svg.parse_path(svg_path)
    (commands, advance)
  }
}

///|
fn resolve_js_family_glyph_commands_by_weight(
  use_outline_commands : Bool,
  codepoint : Int,
  font_size : Double,
  font_weight : Double,
  font_family : String,
) -> (Array[@svg.PathCommand], Double)? {
  let advance = js_advance_for_family_by_weight(
    codepoint, font_size, font_weight, font_family,
  )
  if use_outline_commands {
    let json = js_outline_commands_for_family_by_weight(
      codepoint, font_size, font_weight, font_family,
    )
    if json.length() == 0 || json == "[]" {
      return None
    }
    let commands = @rendering.parse_glyph_outline_commands_json(json)
    Some((commands, advance))
  } else {
    let svg_path = js_glyph_for_family_by_weight(
      codepoint, font_size, font_weight, font_family,
    )
    if svg_path.length() == 0 {
      return None
    }
    let commands = @svg.parse_path(svg_path)
    Some((commands, advance))
  }
}

///|
fn resolve_js_family_glyph_commands(
  use_outline_commands : Bool,
  codepoint : Int,
  font_size : Double,
  is_bold : Bool,
  font_family : String,
) -> (Array[@svg.PathCommand], Double)? {
  let advance = js_advance_for_family(
    codepoint, font_size, is_bold, font_family,
  )
  if use_outline_commands {
    let json = js_outline_commands_for_family(
      codepoint, font_size, is_bold, font_family,
    )
    if json.length() == 0 || json == "[]" {
      return None
    }
    let commands = @rendering.parse_glyph_outline_commands_json(json)
    Some((commands, advance))
  } else {
    let svg_path = js_glyph_for_family(
      codepoint, font_size, is_bold, font_family,
    )
    if svg_path.length() == 0 {
      return None
    }
    let commands = @svg.parse_path(svg_path)
    Some((commands, advance))
  }
}

///|
fn capture_current_paint_html() -> String {
  let html_json = evaluate_js(capture_paint_source_html_expr())
  @protocol.extract_string_value_from_evaluate_result(html_json)
}

///|
fn BidiProtocol::resolve_actual_paint_viewport_dimensions(
  self : BidiProtocol,
  ctx_id : String,
) -> (Int, Int) {
  let viewport_width = self.resolve_effective_viewport_width(ctx_id)
  let viewport_height = self.resolve_effective_viewport_height(ctx_id)
  (viewport_width, viewport_height)
}

///|
fn BidiProtocol::build_actual_paint_frame(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  html : String,
  origin? : String = "viewport",
  send_errors? : Bool = true,
  measure_visual? : Bool = false,
) -> PaintCaptureFrame? {
  ensure_paint_provider_initialized()
  let (viewport_width, viewport_height) = self.resolve_actual_paint_viewport_dimensions(
    ctx_id,
  )
  let ctx = @browser_helpers.create_render_context(
    viewport_width, viewport_height, false,
  )
  let t0 = js_now_ms()
  let (node, layout) = if origin == "document" {
    @renderer.render_to_node_and_layout_full_document(html, ctx)
  } else {
    @renderer.render_to_node_and_layout(html, ctx)
  }
  let t1 = js_now_ms()
  let (width, height) = if origin == "document" {
    @rendering.actual_paint_document_dimensions_from_node_and_layout(
      node, layout, viewport_width, viewport_height,
    )
  } else {
    (viewport_width, viewport_height)
  }
  let paint_node = match
    @tui_paint_viewport.from_node_and_layout_with_viewport_rect(
      node,
      layout,
      0.0,
      0.0,
      width.to_double(),
      height.to_double(),
      0.0,
      0.0,
    ) {
    Some(node) => node
    None => {
      if send_errors {
        self.send_error(
          request_id, "unknown error", "Failed to build paint tree for current context",
        )
      }
      return None
    }
  }
  let t2 = js_now_ms()
  let (fb, palette) = @tui_paint_export.raster_to_framebuffer(
    paint_node, width, height,
  )
  let visual = if measure_visual {
    Some(@tui_paint_export.framebuffer_visual_stats(fb, palette))
  } else {
    None
  }
  let t3 = js_now_ms()
  Some({
    width,
    height,
    framebuffer: fb,
    palette,
    timing: {
      node_layout_ms: t1 - t0,
      paint_tree_ms: t2 - t1,
      raster_ms: t3 - t2,
      png_encode_ms: 0,
      rgba_encode_ms: 0,
      total_ms: t3 - t0,
    },
    visual,
    paint_node,
  })
}

///|
/// Pre-rasterize common ASCII glyphs. Initializes providers if needed.
pub fn warmup_glyph_cache() -> Unit {
  ensure_paint_provider_initialized()
  let t_pre = js_now_ms()
  let common_sizes : Array[Double] = [
    13.33, // 10pt (most common body text)
     16.0, // 16px (browser default)
  ]
  let n = @glyph.pre_rasterize_glyphs(common_sizes)
  let t_pre_done = js_now_ms()
  js_log_stderr(
    "Pre-rasterized " +
    n.to_string() +
    " glyphs in " +
    (t_pre_done - t_pre).to_string() +
    "ms",
  )
}

///|
/// Test-only actual paint capture for VRT.
fn BidiProtocol::handle_capture_paint_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_capture_paint_data(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_capture_paint_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Json? {
  let params = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }

  let ctx_id = match params.get("context") {
    Some(String(id)) => id
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing context")
      return None
    }
  }

  match @rendering.normalize_capture_paint_data_options(params) {
    Ok(_) => ()
    Err(error) => {
      self.send_error(request_id, error.name(), error.message())
      return None
    }
  }

  let _session = match self.manager.get_session(ctx_id) {
    Some(session) => session
    None => {
      self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
      return None
    }
  }
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)

  let html = match params.get("html") {
    Some(String(html)) => html
    _ => capture_current_paint_html()
  }
  let frame = match
    self.build_actual_paint_frame(
      request_id,
      ctx_id,
      html,
      send_errors=true,
      measure_visual=true,
    ) {
    Some(frame) => frame
    None => return None
  }
  let visual = match frame.visual {
    Some(visual) => visual
    None =>
      @tui_paint_export.framebuffer_visual_stats(
        frame.framebuffer,
        frame.palette,
      )
  }
  let t_encode0 = js_now_ms()
  let data = @tui_paint_export.framebuffer_to_rgba_base64(
    frame.framebuffer,
    frame.palette,
  )
  let t_encode1 = js_now_ms()
  let timing : @rendering.PaintCaptureTiming = {
    ..frame.timing,
    rgba_encode_ms: t_encode1 - t_encode0,
    total_ms: frame.timing.total_ms + (t_encode1 - t_encode0),
  }
  js_log_stderr(@rendering.capture_paint_data_log_message(timing, visual))
  Some(
    @rendering.capture_paint_data_to_json(
      frame.width,
      frame.height,
      data,
      timing,
      visual,
    ),
  )
}

///|
/// Whether a paint node matches a simple `selector`. The selector grammar is
/// limited to what the renderer's node ids encode (`make_node_id`): a bare tag
/// (`div`), an id (`#main` / `div#main`), or the element's FIRST class
/// (`.card` / `div.card`). Compound / descendant / attribute selectors are not
/// supported because the paint tree only carries one `tag#id`-or-`tag.class`
/// identifier per node.
fn paint_node_matches_selector(
  node : @paint_model.PaintNode,
  selector : String,
) -> Bool {
  let sel = selector.trim().to_owned()
  if sel == "" {
    return false
  }
  // Node id is `tag`, `tag#id`, or `tag.firstClass`.
  let id = node.id
  if sel == node.tag || sel == id {
    return true
  }
  if sel.has_prefix("#") {
    // `#main` matches a node whose id is `#main`.
    return id.has_suffix("#" + sel.substring(start=1))
  }
  if sel.has_prefix(".") {
    // `.card` matches a node whose id is `.card`.
    return id.has_suffix("." + sel.substring(start=1))
  }
  false
}

///|
/// Depth-first search for the first paint node matching `selector`, returning
/// its document-relative border-box rect.
fn paint_node_find_rect(
  node : @paint_model.PaintNode,
  selector : String,
) -> (Double, Double, Double, Double)? {
  if paint_node_matches_selector(node, selector) {
    return Some((node.x, node.y, node.width, node.height))
  }
  for child in node.children {
    match paint_node_find_rect(child, selector) {
      Some(rect) => return Some(rect)
      None => ()
    }
  }
  None
}

///|
/// browsingContext.renderSelector: render the page, then return just the
/// pixels of the element matched by `selector`, cropped (with optional
/// `padding`) out of the full framebuffer, plus its bounding box. Enables
/// component-level VRT without cropping a full-page screenshot client-side.
fn BidiProtocol::handle_render_selector(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_render_selector(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_render_selector(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Json? {
  let params = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }
  let ctx_id = match params.get("context") {
    Some(String(id)) => id
    _ => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return None
    }
  }
  let selector = match params.get("selector") {
    Some(String(sel)) => sel
    _ => {
      self.send_error(
        request_id, "invalid argument", "selector must be a string",
      )
      return None
    }
  }
  let padding = match params.get("padding") {
    Some(Number(v, ..)) => v
    _ => 0.0
  }
  let _session = match self.manager.get_session(ctx_id) {
    Some(session) => session
    None => {
      self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
      return None
    }
  }
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  // Render the whole live document (origin="document"): this disables
  // viewport-skeleton culling and produces a full-page, document-relative
  // framebuffer. The element rect is then resolved from that same paint tree
  // (paint_node_find_rect) rather than the runtime DOM's getBoundingClientRect
  // — the runtime rect is computed from inline style only and does not reflect
  // an element's normal-flow position, so a below-the-fold element would
  // otherwise report y=0 and crop blank (GitHub issue #253). Sourcing the rect
  // and the framebuffer from the same paint tree keeps them in one coordinate
  // space.
  let html = capture_current_paint_html()
  let frame = match
    self.build_actual_paint_frame(
      request_id,
      ctx_id,
      html,
      origin="document",
      send_errors=true,
      measure_visual=false,
    ) {
    Some(frame) => frame
    None => return None
  }
  let (bx, by, bw, bh) = match
    paint_node_find_rect(frame.paint_node, selector) {
    Some(rect) => rect
    None => {
      self.send_error(
        request_id,
        "no such node",
        "selector matched no element: " + selector,
      )
      return None
    }
  }
  let x0 = (bx - padding).floor().to_int()
  let y0 = (by - padding).floor().to_int()
  let x1 = (bx + bw + padding).ceil().to_int()
  let y1 = (by + bh + padding).ceil().to_int()
  let cropped = frame.framebuffer.crop(x0, y0, x1 - x0, y1 - y0)
  let data = @tui_paint_export.framebuffer_to_rgba_base64(
    cropped,
    frame.palette,
  )
  let bounding_box : Map[String, Json] = {
    "x": Json::number(bx),
    "y": Json::number(by),
    "width": Json::number(bw),
    "height": Json::number(bh),
  }
  let out : Map[String, Json] = {
    "width": Json::number(cropped.width.to_double()),
    "height": Json::number(cropped.height.to_double()),
    "data": Json::string(data),
    "boundingBox": make_object(bounding_box),
  }
  Some(make_object(out))
}

///|
fn BidiProtocol::try_resolve_actual_capture_screenshot_data(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  params : Map[String, Json],
) -> String? {
  if !@rendering.can_use_actual_paint_for_screenshot_data(params) {
    return None
  }
  let html = capture_current_paint_html()
  if html.trim().length() == 0 {
    return None
  }
  let origin = match params.get("origin") {
    Some(String("document")) => "document"
    _ => "viewport"
  }
  let frame = match
    self.build_actual_paint_frame(
      request_id,
      ctx_id,
      html,
      origin~,
      send_errors=false,
      measure_visual=false,
    ) {
    Some(frame) => frame
    None => return None
  }
  let t_encode0 = js_now_ms()
  let data = @tui_paint_png.framebuffer_to_png_base64(
    frame.framebuffer,
    frame.palette,
  )
  let t_encode1 = js_now_ms()
  let timing : @rendering.PaintCaptureTiming = {
    ..frame.timing,
    png_encode_ms: t_encode1 - t_encode0,
    total_ms: frame.timing.total_ms + (t_encode1 - t_encode0),
  }
  js_log_stderr(@rendering.capture_screenshot_data_log_message(timing))
  Some(data)
}

///|
fn capture_paint_source_html_expr() -> String {
  (
    #|(() => {
    #|  const ctxId = String(globalThis.__bidiCurrentContext || "default-context");
    #|  const ctxWindow = globalThis.__bidiContextWindows && globalThis.__bidiContextWindows.has(ctxId)
    #|    ? globalThis.__bidiContextWindows.get(ctxId)
    #|    : null;
    #|  const captureSource = ctxWindow && ctxWindow.__craterPaintCaptureSource
    #|    ? ctxWindow.__craterPaintCaptureSource
    #|    : globalThis.__craterPaintCaptureSource;
    #|  if (captureSource === "live") {
    #|    const live =
  ) +
  serialize_live_document_html_expr() +
  (
    #|;
    #|    if (live) return live;
    #|  }
    #|  if (ctxWindow && typeof ctxWindow.__lastHTML === "string") {
    #|    return ctxWindow.__lastHTML;
    #|  }
    #|  return globalThis.__lastHTML || '';
    #|})()
  )
}

///|
/// Return PaintNode tree as JSON for native paint rendering.
fn BidiProtocol::handle_capture_paint_tree(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_capture_paint_tree(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_capture_paint_tree(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Json? {
  let params = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }
  let ctx_id = match params.get("context") {
    Some(String(id)) => id
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing context")
      return None
    }
  }
  let _session = match self.manager.get_session(ctx_id) {
    Some(session) => session
    None => {
      self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
      return None
    }
  }
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  let origin = match @rendering.normalize_capture_paint_tree_options(params) {
    Ok(origin) => origin
    Err(error) => {
      self.send_error(request_id, error.name(), error.message())
      return None
    }
  }
  let width = self.resolve_effective_viewport_width(ctx_id)
  let height = self.resolve_effective_viewport_height(ctx_id)
  let html_json = evaluate_js(serialize_live_document_html_expr())
  let html = @protocol.extract_string_value_from_evaluate_result(html_json)
  let ctx = @browser_helpers.create_render_context(width, height, false)
  let (node, layout) = if origin == "document" {
    @renderer.render_to_node_and_layout_full_document(html, ctx)
  } else {
    @renderer.render_to_node_and_layout(html, ctx)
  }
  let (paint_width, paint_height) = if origin == "document" {
    @rendering.actual_paint_document_dimensions_from_node_and_layout(
      node, layout, width, height,
    )
  } else {
    (width, height)
  }
  let paint_node = match
    @tui_paint_viewport.from_node_and_layout_with_viewport_rect(
      node,
      layout,
      0.0,
      0.0,
      paint_width.to_double(),
      paint_height.to_double(),
      0.0,
      0.0,
    ) {
    Some(node) => node
    None => {
      self.send_error(request_id, "unknown error", "Failed to build paint tree")
      return None
    }
  }
  let json_str = @tui_paint_export.to_json_string(paint_node)
  Some(
    @rendering.capture_paint_tree_to_json(paint_width, paint_height, json_str),
  )
}