///|
/// Vector text support for SVG documents rendered through the image pipeline.
///
/// The SVG renderer has no font system: `` falls back to a built-in
/// bitmap font, which reads as blocky, misplaced lettering on real assets
/// (shields.io badges lean on `text-anchor` centering the renderer ignores).
/// Hosts that can supply glyph outlines replace that fallback wholesale:
/// before rasterization every `Shape::Text` node is handed to a resolver and,
/// on success, swapped for an equivalent `Shape::Path`. Text that the resolver
/// declines keeps the bitmap fallback, so partial font coverage degrades
/// gracefully.
///
/// The resolver lives in a registry rather than a decoder parameter because
/// renderer providers must stay mutually independent: one provider's font
/// stack (for example the pure-MoonBit outline extractor in `moui_sun/text`)
/// registers the implementation at application startup, and every backend's
/// decoder picks it up without importing another provider.

///|
/// Anchor semantics of an SVG `` run, resolved from the
/// `text-anchor` attribute (with group inheritance) at scan time.
pub(all) enum SvgTextAnchor {
  Start
  Middle
  End
} derive(Eq, Debug)

///|
pub extend SvgTextAnchor with Debug::{to_repr}

///|
pub extend SvgTextAnchor with Eq::{not_equal, equal}

///|
/// One SVG `` run, in the node's own user space. `x`/`baseline_y` carry
/// the anchor point and baseline; `text` and `font_size` are the element's
/// literal content.
pub(all) struct SvgTextRun {
  text : String
  font_size : Double
  x : Double
  baseline_y : Double
  anchor : SvgTextAnchor
  /// `textLength` attribute in the run's user space; 0 when unspecified.
  text_length : Double
} derive(Debug)

///|
pub extend SvgTextRun with Debug::{to_repr}

///|
/// Glyph outlines for one `SvgTextRun`, in text-local space: the origin is
/// the run's left edge on the baseline (Start anchoring), `width` is the
/// laid-out advance, and polygons are closed contours ready for a nonzero
/// fill. Returning `None` keeps the bitmap fallback for that run.
pub(all) struct SvgTextRunOutline {
  width : Double
  polygons : Array[Array[(Double, Double)]]
} derive(Debug)

///|
pub extend SvgTextRunOutline with Debug::{to_repr}

///|
/// Host-supplied conversion from an SVG text run to glyph outline polygons.
pub type SvgTextPathResolver = (SvgTextRun) -> SvgTextRunOutline?

///|
priv struct SvgTextPathRegistry {
  mut resolver : SvgTextPathResolver?
}

///|
let svg_text_path_registry : SvgTextPathRegistry = { resolver: None, }

///|
/// Install (replace) the process-wide text resolver. Call once at application
/// startup, before SVG assets decode; later calls swap the implementation for
/// subsequent documents.
pub fn set_svg_text_path_resolver(resolver : SvgTextPathResolver) -> Unit {
  svg_text_path_registry.resolver = Some(resolver)
}

///|
/// Drop the installed resolver; SVG text falls back to the bitmap font.
pub fn clear_svg_text_path_resolver() -> Unit {
  svg_text_path_registry.resolver = None
}

///|
fn svg_current_text_path_resolver() -> SvgTextPathResolver? {
  svg_text_path_registry.resolver
}

///|
fn cursor_scanned_font_size(
  scanned : Array[SvgScannedText],
  cursor : SvgTextAnchorCursor,
) -> Double? {
  if cursor.value < scanned.length() {
    scanned[cursor.value].font_size
  } else {
    None
  }
}

///|
fn cursor_scanned_text_length(
  scanned : Array[SvgScannedText],
  cursor : SvgTextAnchorCursor,
) -> Double? {
  if cursor.value < scanned.length() {
    scanned[cursor.value].text_length
  } else {
    None
  }
}

///|
/// Presentation facts for one `` element recovered from the raw markup.
/// `font_size` is `None` when no own-or-inherited numeric `font-size` exists;
/// the node's parsed value then wins.
priv struct SvgScannedText {
  anchor : SvgTextAnchor
  font_size : Double?
  text_length : Double?
}

///|
/// Document-order cursor shared across the recursive tree walk so each
/// `` element consumes its scanned facts in turn.
priv struct SvgTextAnchorCursor {
  mut value : Int
}

///|
/// Effective `text-anchor` for every `` element, in document order.
///
/// The renderer's scene graph does not carry presentation attributes, so the
/// anchor is recovered with a bounded scan of the raw markup: container tags
/// (`svg`/`g`) push their inherited anchor, `` start tags record the
/// effective value, closers pop. Quoted `>` inside attribute values is not
/// handled; real-world badge and icon markup does not use it.
fn svg_scan_text_anchors(svg_text : String) -> Array[SvgScannedText] {
  let scanned : Array[SvgScannedText] = []
  let stack : Array[(SvgTextAnchor, Double?)] = [(Start, None)]
  let chars = svg_text.to_array()
  let limit = chars.length()
  let mut index = 0
  fn anchor_of(value : String, fallback : SvgTextAnchor) -> SvgTextAnchor {
    match value {
      "middle" => Middle
      "end" => End
      "start" => Start
      _ => fallback
    }
  }

  fn chars_equal_at(
    tag : Array[Char],
    offset : Int,
    name : Array[Char],
  ) -> Bool {
    let name_len = name.length()
    if offset + name_len > tag.length() {
      return false
    }
    for k = 0; k < name_len; k = k + 1 {
      if tag[offset + k] != name[k] {
        return false
      }
    }
    true
  }

  fn tag_attr(tag : Array[Char], name : Array[Char]) -> String? {
    // Locate `name=` inside the tag body and return the quoted value.
    let name_len = name.length()
    let mut i = 0
    while i < tag.length() {
      if chars_equal_at(tag, i, name) &&
        i + name_len < tag.length() &&
        tag[i + name_len] == '=' {
        let value_start = i + name_len + 1
        if value_start < tag.length() {
          let quote = tag[value_start]
          if quote == '"' || quote == '\'' {
            let mut end = value_start + 1
            while end < tag.length() && tag[end] != quote {
              end = end + 1
            }
            if end > value_start + 1 && end <= tag.length() {
              return Some(
                String::from_array(tag[value_start + 1:end].to_owned()),
              )
            }
          }
        }
        return None
      }
      i = i + 1
    }
    None
  }

  while index < limit {
    if chars[index] != '<' {
      index = index + 1
      continue
    }
    // Read the tag body up to the closing '>'.
    let mut end = index + 1
    while end < limit && chars[end] != '>' {
      end = end + 1
    }
    if end >= limit {
      break
    }
    let body = chars[index + 1:end].to_owned()
    index = end + 1
    if body.length() == 0 {
      continue
    }
    if body[0] == '!' || body[0] == '?' {
      continue
    }
    if body[0] == '/' {
      // Closing tag: pop containers pushed by their start tags.
      let name = String::from_array(
        svg_tag_name(body[1:body.length()].to_owned()),
      )
      if name == "g" || name == "svg" {
        if stack.length() > 1 {
          let _ = stack.pop()
        }
      }
      continue
    }
    let self_closing = body[body.length() - 1] == '/'
    let name = String::from_array(svg_tag_name(body))
    let frame = stack[stack.length() - 1]
    let anchor = match tag_attr(body, "text-anchor".to_array()) {
      Some(value) => anchor_of(value, frame.0)
      None => frame.0
    }
    let font_size = match tag_attr(body, "font-size".to_array()) {
      Some(value) => svg_parse_px_length(value)
      None => frame.1
    }
    // textLength only ever applies on the  element itself.
    let text_length = if name == "text" {
      match tag_attr(body, "textLength".to_array()) {
        Some(value) => svg_parse_px_length(value)
        None => None
      }
    } else {
      None
    }
    if name == "text" {
      scanned.push({ anchor, font_size, text_length, })
    } else if (name == "g" || name == "svg") && !self_closing {
      stack.push((anchor, font_size))
    }
  }
  scanned
}

///|
/// Numeric `font-size` literal (`110`, `12.5`, `14px`); percentages and other
/// units return `None` because they need a resolved context.
fn svg_parse_px_length(value : String) -> Double? {
  let trimmed = value.trim().to_owned()
  if trimmed == "" || trimmed.contains("%") {
    return None
  }
  let chars = trimmed.to_array()
  let mut index = 0
  let mut whole = 0.0
  let mut digits = 0
  while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
    whole = whole * 10.0 + (chars[index].to_int() - 48).to_double()
    digits = digits + 1
    index = index + 1
  }
  if digits == 0 {
    return None
  }
  let mut fraction = 0.0
  if index < chars.length() && chars[index] == '.' {
    index = index + 1
    let mut scale = 1.0
    while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
      scale = scale * 10.0
      fraction = fraction + (chars[index].to_int() - 48).to_double() / scale
      index = index + 1
    }
  }
  Some(whole + fraction)
}

///|
/// Element name of a tag body: the leading run of name characters.
fn svg_tag_name(body : Array[Char]) -> Array[Char] {
  let out : Array[Char] = []
  for ch in body {
    if (ch >= 'a' && ch <= 'z') ||
      (ch >= 'A' && ch <= 'Z') ||
      (ch >= '0' && ch <= '9') ||
      ch == '_' ||
      ch == '-' {
      out.push(ch)
    } else {
      break
    }
  }
  out
}

///|
/// Replace every `Shape::Text` node with an equivalent `Shape::Path` when the
/// installed resolver supplies outlines. Text nodes are matched to scanned
/// anchors in document order; runs the resolver declines keep their original
/// shape. Returns the number of replaced runs.
fn svg_apply_text_paths(
  node : @svg.SVGNode,
  resolver : SvgTextPathResolver,
  anchors : Array[SvgScannedText],
  cursor : SvgTextAnchorCursor,
) -> Int {
  let replaced = match node.shape {
    Text(x~, y~, text~, font_size~) => {
      let anchor = if cursor.value < anchors.length() {
        anchors[cursor.value].anchor
      } else {
        Start
      }
      // The renderer's scene graph does not inherit `font-size` from
      // container groups; the scanned value restores the intended size.
      let run_font_size = match cursor_scanned_font_size(anchors, cursor) {
        Some(size) => size
        None => font_size
      }
      let run_text_length = match cursor_scanned_text_length(anchors, cursor) {
        Some(length) => length
        None => 0.0
      }
      cursor.value = cursor.value + 1
      match
        resolver({
          text,
          font_size: run_font_size,
          x,
          baseline_y: y,
          anchor,
          text_length: run_text_length,
        }) {
        Some(outline) =>
          if outline.polygons.is_empty() {
            0
          } else {
            let start_x = match anchor {
              Start => x
              Middle => x - outline.width / 2.0
              End => x - outline.width
            }
            node.shape = Path(
              commands=svg_text_polygons_to_commands(
                outline.polygons,
                start_x,
                y,
              ),
            )
            1
          }
        None => 0
      }
    }
    Group => {
      let mut total = 0
      for child in node.children {
        total = total + svg_apply_text_paths(child, resolver, anchors, cursor)
      }
      total
    }
    _ => 0
  }
  replaced
}

///|
/// Build the renderer's path commands for text polygons placed into user
/// space. Polygon coordinates are text-local — x from the run's left edge,
/// y from the baseline — so the anchor-resolved `start_x` and the run's
/// `baseline_y` are added to every point.
fn svg_text_polygons_to_commands(
  polygons : Array[Array[(Double, Double)]],
  start_x : Double,
  baseline_y : Double,
) -> Array[@svg.PathCommand] {
  let commands : Array[@svg.PathCommand] = []
  for polygon in polygons {
    if polygon.length() < 2 {
      continue
    }
    commands.push(MoveTo(polygon[0].0 + start_x, polygon[0].1 + baseline_y))
    for i = 1; i < polygon.length(); i = i + 1 {
      commands.push(LineTo(polygon[i].0 + start_x, polygon[i].1 + baseline_y))
    }
    commands.push(ClosePath)
  }
  commands
}

///|
/// Install text outlines into a freshly parsed document when a resolver is
/// registered. Called after parse and viewBox synthesis, before rasterize.
fn svg_install_text_paths(
  document : @svg.SVGDocument,
  svg_text : String,
) -> Unit {
  match svg_current_text_path_resolver() {
    Some(resolver) => {
      let anchors = svg_scan_text_anchors(svg_text)
      let cursor : SvgTextAnchorCursor = { value: 0, }
      let _ = svg_apply_text_paths(document.root(), resolver, anchors, cursor)
    }
    None => ()
  }
}