///|
/// SVG image support for the renderer-neutral image pipeline.
///
/// Backends decode only the raster formats their codec library understands,
/// so a document that references an `.svg` asset renders as a placeholder on
/// every platform. This module supplies the missing decoder as a plugin: SVG
/// is rasterized here, in pure MoonBit, and handed back through the existing
/// `ready_decoded_rgba` completion. Nothing downstream needs to know the
/// pixels came from a vector source.
///
/// The rasterizer understands `viewBox`, `preserveAspectRatio`, `clipPath`
/// and `mask`, and the module compiles for every target (native, wasm,
/// wasm-gc), so the same plugin covers desktop and Web.

///|
/// Upper bound on the longest rasterized edge. Keeps a hostile or merely
/// oversized document from allocating an enormous bitmap.
let svg_raster_max_side : Int = 2048

///|
/// Upper bound on total rasterized pixels, independent of aspect ratio.
let svg_raster_max_pixels : Int = 4194304

///|
/// Preferred over-sampling factor. SVG is resolution independent: rasterizing
/// slightly above the intrinsic size keeps downscaled placements crisp on
/// high-density displays without exploding the bitmap on large assets.
let svg_raster_scale : Double = 2.0

///|
/// True when `source` should be treated as SVG: either its path carries an
/// `.svg`/`.svgz` suffix, or its leading bytes contain an ` Bool {
  let lowered = source.to_lower()
  lowered.has_suffix(".svg") ||
  lowered.has_suffix(".svgz") ||
  svg_bytes_contain_root_tag(bytes)
}

///|
/// Scan the leading bytes for the ASCII ` Bool {
  let limit = if bytes.length() < 2048 { bytes.length() } else { 2048 }
  let mut index = 0
  while index + 4 <= limit {
    if svg_byte(bytes, index) == 0x3C &&
      svg_byte(bytes, index + 1) == 0x73 &&
      svg_byte(bytes, index + 2) == 0x76 &&
      svg_byte(bytes, index + 3) == 0x67 {
      return true
    }
    index = index + 1
  }
  false
}

///|
fn svg_byte(bytes : Bytes, index : Int) -> Int {
  match bytes.get(index) {
    Some(byte) => byte.to_int()
    None => -1
  }
}

///|
/// Fallback replaced-element size. Matches the SVG/CSS default of 300x150 and
/// keeps a parseable document renderable when it declares neither `viewBox`
/// nor resolvable `width`/`height`.
let svg_default_size : @core.Size = @core.Size::new(width=300.0, height=150.0)

///|
/// Intrinsic size of an SVG document: `viewBox` when present, otherwise the
/// root element's `width`/`height`. Returns `None` when neither is usable,
/// which lets callers keep their own placeholder sizing.
pub fn svg_intrinsic_size(svg_text : String) -> @core.Size? {
  match @svg.parse_svg_document(svg_text) {
    Some(document) => {
      let root = document.root()
      match root.view_box {
        Some(view_box) =>
          if view_box.width > 0.0 && view_box.height > 0.0 {
            return Some(
              @core.Size::new(width=view_box.width, height=view_box.height),
            )
          }
        None => ()
      }
      // The parser records `viewport_width`/`viewport_height` only for nested
      // `` elements, so a root-level `width`/`height` pair has to be read
      // from the tag text itself.
      svg_root_viewport_size(svg_text)
    }
    None => None
  }
}

///|
/// Intrinsic size of `bytes` when they hold an SVG document, otherwise `None`.
///
/// Size probes that only have raw bytes — the markdown intrinsic-size reader,
/// for example — route SVG through the same detection the image decoder uses,
/// so a vector asset reserves the right box instead of a placeholder ratio.
pub fn svg_intrinsic_size_from_bytes(bytes : Bytes) -> @core.Size? {
  if !svg_source_is_svg("", bytes) {
    return None
  }
  svg_intrinsic_size(@utf8.decode_lossy(bytes[:], ignore_bom=false))
}

///|
/// Read `width`/`height` off the root `` start tag. Percentage and
/// unit-relative lengths yield `None` because they cannot be resolved without
/// a containing viewport.
fn svg_root_viewport_size(svg_text : String) -> @core.Size? {
  let tag = match svg_root_tag_text(svg_text) {
    Some(tag) => tag
    None => return None
  }
  let width = match svg_attr_value(tag, "width") {
    Some(raw) => svg_parse_length_literal(raw)
    None => None
  }
  let height = match svg_attr_value(tag, "height") {
    Some(raw) => svg_parse_length_literal(raw)
    None => None
  }
  match (width, height) {
    (Some(width), Some(height)) =>
      if width > 0.0 && height > 0.0 {
        Some(@core.Size::new(width~, height~))
      } else {
        None
      }
    _ => None
  }
}

///|
/// The root `` start tag, taken from the first `>`-delimited segment
/// that opens an `svg` element.
fn svg_root_tag_text(svg_text : String) -> String? {
  for segment in svg_text.split(">").to_array() {
    if segment.contains(" String? {
  for raw in tag.split(" ").to_array() {
    let parts = raw.split("=").to_array()
    if parts.length() == 2 && parts[0].to_owned() == name {
      return Some(svg_strip_quotes(parts[1].to_owned()))
    }
  }
  None
}

///|
fn svg_strip_quotes(value : String) -> String {
  let chars = value.to_array()
  let mut start = 0
  let mut end = chars.length()
  if start < end && (chars[start] == '"' || chars[start] == '\'') {
    start = start + 1
  }
  if end > start && (chars[end - 1] == '"' || chars[end - 1] == '\'') {
    end = end - 1
  }
  String::from_array(chars[start:end])
}

///|
/// Parse a leading unsigned decimal literal such as `24`, `24.5` or `24px`.
/// Percentages return `None`; any other trailing unit is ignored, which is
/// correct for the pixel lengths that appear in real-world assets.
fn svg_parse_length_literal(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 integral_digits = 0
  while index < chars.length() {
    let ch = chars[index]
    if ch < '0' || ch > '9' {
      break
    }
    whole = whole * 10.0 + (ch.to_int() - 48).to_double()
    integral_digits = integral_digits + 1
    index = index + 1
  }
  if integral_digits == 0 {
    return None
  }
  let mut fraction = 0.0
  let mut scale = 1.0
  if index < chars.length() && chars[index] == '.' {
    index = index + 1
    while index < chars.length() {
      let ch = chars[index]
      if ch < '0' || ch > '9' {
        break
      }
      scale = scale * 10.0
      fraction = fraction + (ch.to_int() - 48).to_double() / scale
      index = index + 1
    }
  }
  Some(whole + fraction)
}

///|
/// Rasterized pixel size for an intrinsic size, preserving aspect ratio while
/// respecting the oversampling preference and the memory budget.
pub fn svg_raster_target_size(intrinsic : @core.Size) -> (Int, Int) {
  let extent = if intrinsic.width > intrinsic.height {
    intrinsic.width
  } else {
    intrinsic.height
  }
  let mut scale = svg_raster_scale
  if extent > 0.0 {
    let side_bound = svg_raster_max_side.to_double() / extent
    if scale > side_bound {
      scale = side_bound
    }
  }
  let mut width = svg_pixel_dimension(intrinsic.width * scale)
  let mut height = svg_pixel_dimension(intrinsic.height * scale)
  while width * height > svg_raster_max_pixels && scale > 0.25 {
    scale = scale / 2.0
    width = svg_pixel_dimension(intrinsic.width * scale)
    height = svg_pixel_dimension(intrinsic.height * scale)
  }
  (width, height)
}

///|
fn svg_pixel_dimension(value : Double) -> Int {
  if value <= 0.0 {
    return 1
  }
  let rounded = value.round().to_int()
  if rounded < 1 {
    1
  } else {
    rounded
  }
}

///|
/// Make sure the root element carries a `viewBox`.
///
/// The renderer maps user units onto the target canvas through a
/// `viewBox` -> viewport transform. A document that declares just `width`
/// and `height` gets no such transform: its content is painted at 1:1 user
/// units into whatever canvas we allocate, so an oversampled raster ends up
/// with the artwork shrunk into one corner and the rest transparent. Real
/// assets do this — the shields.io badges are `width="120" height="20"` with
/// no `viewBox` — so derive the missing viewBox from the declared size.
///
/// The synthesis happens at the text level: the renderer's document keeps its
/// resource registries (gradients, patterns, masks) privately, so a parsed
/// document cannot be rebuilt around a mutated root without losing them.
fn svg_ensure_root_view_box(svg_text : String) -> String {
  let chars = svg_text.to_array()
  let limit = chars.length()
  // Locate the root ``, mirroring `svg_root_tag_text`'s segment semantics.
  let mut start = -1
  let mut end = -1
  let mut index = 0
  while index + 4 <= limit {
    if chars[index] == '<' &&
      chars[index + 1] == 's' &&
      chars[index + 2] == 'v' &&
      chars[index + 3] == 'g' {
      start = index
      index = index + 4
      while index < limit {
        if chars[index] == '>' {
          end = index
          break
        }
        index = index + 1
      }
      break
    }
    index = index + 1
  }
  if start < 0 || end < 0 {
    return svg_text
  }
  let tag = String::from_array(chars[start:end + 1])
  if svg_attr_value(tag, "viewBox") is Some(_) {
    return svg_text
  }
  let width = match svg_attr_value(tag, "width") {
    Some(raw) => svg_parse_length_literal(raw)
    None => None
  }
  let height = match svg_attr_value(tag, "height") {
    Some(raw) => svg_parse_length_literal(raw)
    None => None
  }
  match (width, height) {
    (Some(width), Some(height)) =>
      if width > 0.0 && height > 0.0 {
        let self_closing = chars[end - 1] == '/'
        let body_end = if self_closing { end - 1 } else { end }
        let builder = StringBuilder()
        for i in 0.." } else { "\">" })
        for i in (end + 1).. ()
  }
  svg_text
}

///|
/// Rasterize `svg_text` into premultiplied RGBA8888 pixels with
/// `row_bytes = pixel_width * 4`. Returns `None` when the document cannot be
/// parsed or the requested size is degenerate.
///
/// The premultiplied layout matches what raster backends expect from a decoded
/// image payload, so the result is interchangeable with a codec's output.
pub fn svg_rasterize_rgba(
  svg_text : String,
  pixel_width : Int,
  pixel_height : Int,
) -> Bytes? {
  if pixel_width <= 0 || pixel_height <= 0 {
    return None
  }
  match @svg.parse_svg_document(svg_ensure_root_view_box(svg_text)) {
    Some(document) => {
      svg_install_text_paths(document, svg_text)
      let pixels = svg_render_document_rgba(document, pixel_width, pixel_height)
      Some(svg_pixels_to_premultiplied_rgba(pixels, pixel_width, pixel_height))
    }
    None => None
  }
}

///|
/// Rasterize a document into straight-alpha pixels.
///
/// The renderer composites layers internally (gradients, group opacity,
/// masks), so the returned buffer already reflects the document's intended
/// stacking — no per-pixel host compositing is needed here.
fn svg_render_document_rgba(
  document : @svg.SVGDocument,
  pixel_width : Int,
  pixel_height : Int,
) -> Array[@svg.Color] {
  let result = @svg.render_svg_document(
    document,
    pixel_width,
    pixel_height,
    @svg.RenderOptions::default(),
  )
  let image = result.image
  let pixels : Array[@svg.Color] = Array::make(
    pixel_width * pixel_height,
    @svg.Color::transparent(),
  )
  for y = 0; y < pixel_height; y = y + 1 {
    let row = y * pixel_width
    for x = 0; x < pixel_width; x = x + 1 {
      pixels[row + x] = image.get_pixel(x, y)
    }
  }
  pixels
}

///|
/// Convert straight-alpha pixels into premultiplied RGBA8888 bytes.
fn svg_pixels_to_premultiplied_rgba(
  pixels : Array[@svg.Color],
  pixel_width : Int,
  pixel_height : Int,
) -> Bytes {
  let row_bytes = pixel_width * 4
  let out : Array[Byte] = Array::make(row_bytes * pixel_height, b'\x00')
  for y = 0; y < pixel_height; y = y + 1 {
    let source_row = y * pixel_width
    let target_row = y * row_bytes
    for x = 0; x < pixel_width; x = x + 1 {
      let color = pixels[source_row + x]
      let alpha = svg_clamp_channel(color.a)
      let offset = target_row + x * 4
      out[offset] = svg_premultiply(color.r, alpha).to_byte()
      out[offset + 1] = svg_premultiply(color.g, alpha).to_byte()
      out[offset + 2] = svg_premultiply(color.b, alpha).to_byte()
      out[offset + 3] = alpha.to_byte()
    }
  }
  Bytes::from_array(out)
}

///|
fn svg_clamp_channel(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 255 {
    255
  } else {
    value
  }
}

///|
fn svg_premultiply(channel : Int, alpha : Int) -> Int {
  let clamped = svg_clamp_channel(channel)
  if clamped <= 0 {
    return 0
  }
  let scaled = (clamped * alpha + 127) / 255
  if scaled > 255 {
    255
  } else {
    scaled
  }
}

///|
/// The SVG decoder plugin. Sources that are not SVG produce a failed
/// completion so a decoder chain can continue to a raster codec; SVG sources
/// either rasterize or fail with a diagnostic the renderer can surface.
pub fn svg_image_decoder() -> @render.RendererImageDecoder {
  @render.RendererImageDecoder::new(decode=(source, bytes) => {
    match bytes {
      Some(data) =>
        if !svg_source_is_svg(source, data) {
          @render.ImageResourceLoadCompletion::failed(
            source,
            diagnostic="source is not SVG",
          )
        } else {
          svg_decode_bytes(source, data)
        }
      None =>
        @render.ImageResourceLoadCompletion::failed(
          source,
          diagnostic="SVG source bytes are unavailable",
        )
    }
  })
}

///|
fn svg_decode_bytes(
  source : String,
  data : Bytes,
) -> @render.ImageResourceLoadCompletion {
  let text = @utf8.decode_lossy(data[:], ignore_bom=false)
  // A document without a usable intrinsic size still renders, at the CSS
  // default replaced-element box, rather than degrading to a placeholder.
  let intrinsic = match svg_intrinsic_size(text) {
    Some(size) => size
    None => svg_default_size
  }
  let (width, height) = svg_raster_target_size(intrinsic)
  match svg_rasterize_rgba(text, width, height) {
    Some(pixels) =>
      @render.ImageResourceLoadCompletion::ready_decoded_rgba(
        source,
        width~,
        height~,
        row_bytes=width * 4,
        pixels,
      )
    None =>
      @render.ImageResourceLoadCompletion::failed(
        source,
        diagnostic="SVG rasterization failed",
      )
  }
}