///|
/// SVG Layout Engine - Standalone Types
/// Independent of HTML/CSS layout system

///|
/// RGB Color (0-255 range)
pub(all) struct Color {
  r : Int
  g : Int
  b : Int
  a : Int // Alpha (0-255, 255 = opaque)
}

///|
pub fn Color::rgb(r : Int, g : Int, b : Int) -> Color {
  { r, g, b, a: 255 }
}

///|
pub fn Color::rgba(r : Int, g : Int, b : Int, a : Int) -> Color {
  { r, g, b, a }
}

///|
pub fn Color::transparent() -> Color {
  { r: 0, g: 0, b: 0, a: 0 }
}

///|
pub fn Color::black() -> Color {
  { r: 0, g: 0, b: 0, a: 255 }
}

///|
pub fn Color::white() -> Color {
  { r: 255, g: 255, b: 255, a: 255 }
}

///|
pub fn Color::is_transparent(self : Color) -> Bool {
  self.a == 0
}

///|
/// 2D affine transformation matrix
/// | a c e |
/// | b d f |
/// | 0 0 1 |
pub(all) struct Transform {
  a : Double
  b : Double
  c : Double
  d : Double
  e : Double
  f : Double
}

///|
/// ViewBox specification for SVG coordinate system mapping
pub(all) struct ViewBox {
  min_x : Double
  min_y : Double
  width : Double
  height : Double
}

///|
/// preserveAspectRatio alignment values
pub(all) enum Align {
  None // No forced uniform scaling
  XMinYMin
  XMidYMin
  XMaxYMin
  XMinYMid
  XMidYMid // Default
  XMaxYMid
  XMinYMax
  XMidYMax
  XMaxYMax
} derive(Debug, Eq)

///|
/// preserveAspectRatio meet/slice
pub(all) enum MeetOrSlice {
  Meet // Scale to fit entirely (default)
  Slice // Scale to cover entirely
} derive(Debug, Eq)

///|
/// Complete preserveAspectRatio setting
pub(all) struct PreserveAspectRatio {
  align : Align
  meet_or_slice : MeetOrSlice
}

///|
pub fn PreserveAspectRatio::default() -> PreserveAspectRatio {
  { align: XMidYMid, meet_or_slice: Meet }
}

///|
/// Calculate the transform matrix to map viewBox coordinates to viewport
/// viewport_width/height: the actual pixel dimensions of the SVG element
pub fn ViewBox::get_transform(
  self : ViewBox,
  viewport_width : Double,
  viewport_height : Double,
  preserve_aspect_ratio : PreserveAspectRatio,
) -> Transform {
  // Handle zero-size viewBox
  if self.width <= 0.0 || self.height <= 0.0 {
    return Transform::identity()
  }
  // Calculate scale factors
  let scale_x = viewport_width / self.width
  let scale_y = viewport_height / self.height
  // Determine actual scale based on preserveAspectRatio
  let (sx, sy, tx, ty) = match preserve_aspect_ratio.align {
    None =>
      // No uniform scaling - stretch to fit
      (scale_x, scale_y, -self.min_x * scale_x, -self.min_y * scale_y)
    _ => {
      // Uniform scaling
      let scale = match preserve_aspect_ratio.meet_or_slice {
        Meet => if scale_x < scale_y { scale_x } else { scale_y } // Fit entirely
        Slice => if scale_x > scale_y { scale_x } else { scale_y } // Cover entirely
      }
      // Calculate translation based on alignment
      let (align_x, align_y) = get_alignment_factors(
        preserve_aspect_ratio.align,
      )
      // Extra space after scaling
      let extra_x = viewport_width - self.width * scale
      let extra_y = viewport_height - self.height * scale
      let translate_x = -self.min_x * scale + extra_x * align_x
      let translate_y = -self.min_y * scale + extra_y * align_y
      (scale, scale, translate_x, translate_y)
    }
  }
  // Combine: first translate by viewBox min, then scale
  Transform::{ a: sx, b: 0.0, c: 0.0, d: sy, e: tx, f: ty }
}

///|
/// Get alignment factors (0.0 = min, 0.5 = mid, 1.0 = max)
fn get_alignment_factors(align : Align) -> (Double, Double) {
  match align {
    None => (0.0, 0.0)
    XMinYMin => (0.0, 0.0)
    XMidYMin => (0.5, 0.0)
    XMaxYMin => (1.0, 0.0)
    XMinYMid => (0.0, 0.5)
    XMidYMid => (0.5, 0.5)
    XMaxYMid => (1.0, 0.5)
    XMinYMax => (0.0, 1.0)
    XMidYMax => (0.5, 1.0)
    XMaxYMax => (1.0, 1.0)
  }
}

///|
/// SVG path commands (full SVG 1.1 spec)
pub(all) enum PathCommand {
  // Absolute commands
  MoveTo(Double, Double) // M x y
  LineTo(Double, Double) // L x y
  HorizontalLineTo(Double) // H x
  VerticalLineTo(Double) // V y
  CurveTo(Double, Double, Double, Double, Double, Double) // C x1 y1 x2 y2 x y
  SmoothCurveTo(Double, Double, Double, Double) // S x2 y2 x y
  QuadraticCurveTo(Double, Double, Double, Double) // Q x1 y1 x y
  SmoothQuadraticCurveTo(Double, Double) // T x y
  ArcTo(Double, Double, Double, Bool, Bool, Double, Double) // A rx ry rotation large-arc sweep x y
  ClosePath // Z
  // Relative commands
  MoveToRel(Double, Double) // m dx dy
  LineToRel(Double, Double) // l dx dy
  HorizontalLineToRel(Double) // h dx
  VerticalLineToRel(Double) // v dy
  CurveToRel(Double, Double, Double, Double, Double, Double) // c dx1 dy1 dx2 dy2 dx dy
  SmoothCurveToRel(Double, Double, Double, Double) // s dx2 dy2 dx dy
  QuadraticCurveToRel(Double, Double, Double, Double) // q dx1 dy1 dx dy
  SmoothQuadraticCurveToRel(Double, Double) // t dx dy
  ArcToRel(Double, Double, Double, Bool, Bool, Double, Double) // a rx ry rotation large-arc sweep dx dy
} derive(Debug)

///|
/// SVG shape primitives
pub(all) enum Shape {
  Rect(
    x~ : Double,
    y~ : Double,
    width~ : Double,
    height~ : Double,
    rx~ : Double,
    ry~ : Double
  )
  Circle(cx~ : Double, cy~ : Double, r~ : Double)
  Ellipse(cx~ : Double, cy~ : Double, rx~ : Double, ry~ : Double)
  Line(x1~ : Double, y1~ : Double, x2~ : Double, y2~ : Double)
  Polyline(points~ : Array[(Double, Double)])
  Polygon(points~ : Array[(Double, Double)])
  Path(commands~ : Array[PathCommand])
  Text(x~ : Double, y~ : Double, text~ : String, font_size~ : Double)
  Image(
    x~ : Double,
    y~ : Double,
    width~ : Double,
    height~ : Double,
    href~ : String
  )
  Group // Container for children
} derive(Debug)

///|
/// Gradient stop (position 0.0-1.0 and color)
pub(all) struct GradientStop {
  offset : Double // 0.0 to 1.0
  color : Color
}

///|
/// Linear gradient definition
pub(all) struct LinearGradient {
  x1 : Double // Start point x (0.0-1.0 or absolute)
  y1 : Double // Start point y
  x2 : Double // End point x
  y2 : Double // End point y
  stops : Array[GradientStop]
  spread_method : SpreadMethod
  units : GradientUnits
  transform : Transform
}

///|
pub fn LinearGradient::new(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  stops : Array[GradientStop],
) -> LinearGradient {
  {
    x1,
    y1,
    x2,
    y2,
    stops,
    spread_method: Pad,
    units: ObjectBoundingBox,
    transform: Transform::identity(),
  }
}

///|
/// Simple horizontal gradient (left to right)
pub fn LinearGradient::horizontal(
  start_color : Color,
  end_color : Color,
) -> LinearGradient {
  {
    x1: 0.0,
    y1: 0.0,
    x2: 1.0,
    y2: 0.0,
    stops: [
      { offset: 0.0, color: start_color },
      { offset: 1.0, color: end_color },
    ],
    spread_method: Pad,
    units: ObjectBoundingBox,
    transform: Transform::identity(),
  }
}

///|
/// Simple vertical gradient (top to bottom)
pub fn LinearGradient::vertical(
  start_color : Color,
  end_color : Color,
) -> LinearGradient {
  {
    x1: 0.0,
    y1: 0.0,
    x2: 0.0,
    y2: 1.0,
    stops: [
      { offset: 0.0, color: start_color },
      { offset: 1.0, color: end_color },
    ],
    spread_method: Pad,
    units: ObjectBoundingBox,
    transform: Transform::identity(),
  }
}

///|
/// Interpolate color at position t (0.0-1.0)
pub fn LinearGradient::color_at(self : LinearGradient, t : Double) -> Color {
  if self.stops.length() == 0 {
    return Color::black()
  }
  if self.stops.length() == 1 {
    return self.stops[0].color
  }
  // Apply spread method
  let t = match self.spread_method {
    Pad => if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
    Repeat => {
      let t2 = t - t.floor()
      if t2 < 0.0 {
        t2 + 1.0
      } else {
        t2
      }
    }
    Reflect => {
      let t2 = t - t.floor()
      let t2 = if t2 < 0.0 { t2 + 1.0 } else { t2 }
      let cycle = t.floor().to_int() % 2
      if cycle == 1 {
        1.0 - t2
      } else {
        t2
      }
    }
  }
  // Find surrounding stops
  let mut prev_stop = self.stops[0]
  let mut next_stop = self.stops[self.stops.length() - 1]
  for i in 0..<(self.stops.length() - 1) {
    if t >= self.stops[i].offset && t <= self.stops[i + 1].offset {
      prev_stop = self.stops[i]
      next_stop = self.stops[i + 1]
      break
    }
  }
  // Interpolate
  let range = next_stop.offset - prev_stop.offset
  if range < 0.0001 {
    return prev_stop.color
  }
  let local_t = (t - prev_stop.offset) / range
  Color::rgba(
    (prev_stop.color.r.to_double() * (1.0 - local_t) +
    next_stop.color.r.to_double() * local_t).to_int(),
    (prev_stop.color.g.to_double() * (1.0 - local_t) +
    next_stop.color.g.to_double() * local_t).to_int(),
    (prev_stop.color.b.to_double() * (1.0 - local_t) +
    next_stop.color.b.to_double() * local_t).to_int(),
    (prev_stop.color.a.to_double() * (1.0 - local_t) +
    next_stop.color.a.to_double() * local_t).to_int(),
  )
}

///|
/// How gradient extends beyond its bounds
pub(all) enum SpreadMethod {
  Pad // Extend with end colors (default)
  Repeat // Repeat pattern
  Reflect // Mirror pattern
} derive(Debug, Eq)

///|
/// Build a lookup table for fast gradient color sampling
/// Returns an array of colors for t values from 0.0 to 1.0
pub fn LinearGradient::build_lut(
  self : LinearGradient,
  size : Int,
) -> Array[Color] {
  let lut = Array::make(size, Color::black())
  for i in 0.. Color {
  let size = lut.length()
  if size == 0 {
    return Color::black()
  }
  let idx = (t * (size - 1).to_double()).to_int()
  let idx = if idx < 0 { 0 } else if idx >= size { size - 1 } else { idx }
  lut[idx]
}

///|
/// Gradient units coordinate space
pub(all) enum GradientUnits {
  UserSpaceOnUse
  ObjectBoundingBox
} derive(Debug, Eq)

///|
/// Paint style (fill or stroke)
pub(all) enum PaintFallback {
  NoPaint
  SolidColor(Color)
  CurrentColor
}

///|
/// Paint style (fill or stroke)
pub(all) enum Paint {
  None
  SolidColor(Color)
  LinearGrad(LinearGradient)
  RadialGrad(RadialGradient)
  CurrentColor
  PaintServerRef(String, PaintFallback)
}

///|
/// Stroke properties
pub(all) struct StrokeStyle {
  paint : Paint
  width : Double
  linecap : LineCap
  linejoin : LineJoin
  miterlimit : Double
  dasharray : Array[Double]?
  dashoffset : Double
}

///|
pub fn StrokeStyle::default() -> StrokeStyle {
  {
    paint: None,
    width: 1.0,
    linecap: Butt,
    linejoin: Miter,
    miterlimit: 4.0,
    dasharray: None,
    dashoffset: 0.0,
  }
}

///|
pub(all) enum LineCap {
  Butt
  Round
  Square
} derive(Debug, Eq)

///|
pub(all) enum LineJoin {
  Miter
  Round
  Bevel
} derive(Debug, Eq)

///|
/// Fill rule for paths and polygons
pub(all) enum FillRule {
  NonZero // Default
  EvenOdd
} derive(Debug, Eq)

///|
/// SVG node (element in the scene graph)
pub(all) struct SVGNode {
  mut id : String
  mut shape : Shape
  mut transform : Transform
  mut view_box : ViewBox?
  mut viewport_width : Double?
  mut viewport_height : Double?
  mut preserve_aspect_ratio : PreserveAspectRatio
  mut preserve_aspect_ratio_is_set : Bool
  mut fill : Paint
  mut fill_is_set : Bool
  mut color : Color?
  mut color_is_set : Bool
  mut paint_order : PaintOrder
  mut fill_rule : FillRule
  mut fill_opacity : Double
  mut stroke : StrokeStyle
  mut stroke_paint_is_set : Bool
  mut stroke_width_is_set : Bool
  mut stroke_opacity : Double
  mut opacity : Double
  mut marker_start : String?
  mut marker_start_is_set : Bool
  mut marker_mid : String?
  mut marker_mid_is_set : Bool
  mut marker_end : String?
  mut marker_end_is_set : Bool
  mut z_index : Int // Layer order (higher = front)
  mut node_dirty : Bool // Per-node dirty flag
  mut prev_bounds : BoundingBox? // Previous bounding box for dirty rect
  mut filters : Array[Filter] // Filter effects to apply
  mut mask_id : String? // Reference to mask by ID
  mut clip_path_id : String? // Reference to clip path by ID
  mut clip_overflow : Bool
  children : Array[SVGNode]
}

///|
pub fn SVGNode::new(shape : Shape) -> SVGNode {
  {
    id: "",
    shape,
    transform: Transform::identity(),
    view_box: None,
    viewport_width: None,
    viewport_height: None,
    preserve_aspect_ratio: PreserveAspectRatio::default(),
    preserve_aspect_ratio_is_set: false,
    fill: SolidColor(Color::black()),
    fill_is_set: false,
    color: None,
    color_is_set: false,
    paint_order: PaintOrder::default(),
    fill_rule: NonZero,
    fill_opacity: 1.0,
    stroke: StrokeStyle::default(),
    stroke_paint_is_set: false,
    stroke_width_is_set: false,
    stroke_opacity: 1.0,
    opacity: 1.0,
    marker_start: None,
    marker_start_is_set: false,
    marker_mid: None,
    marker_mid_is_set: false,
    marker_end: None,
    marker_end_is_set: false,
    z_index: 0,
    node_dirty: true,
    prev_bounds: None,
    filters: [],
    mask_id: None,
    clip_path_id: None,
    clip_overflow: true,
    children: [],
  }
}

///|
/// Add a filter to the node
pub fn SVGNode::add_filter(self : SVGNode, filter : Filter) -> Unit {
  self.filters.push(filter)
  self.mark_dirty()
}

///|
/// Clear all filters from the node
pub fn SVGNode::clear_filters(self : SVGNode) -> Unit {
  self.filters.clear()
  self.mark_dirty()
}

///|
/// Set mask reference by ID
pub fn SVGNode::set_mask(self : SVGNode, mask_id : String) -> Unit {
  self.mask_id = Some(mask_id)
  self.mark_dirty()
}

///|
/// Clear mask reference
pub fn SVGNode::clear_mask(self : SVGNode) -> Unit {
  self.mask_id = None
  self.mark_dirty()
}

///|
/// Set clip path reference by ID
pub fn SVGNode::set_clip_path(self : SVGNode, clip_path_id : String) -> Unit {
  self.clip_path_id = Some(clip_path_id)
  self.mark_dirty()
}

///|
/// Clear clip path reference
pub fn SVGNode::clear_clip_path(self : SVGNode) -> Unit {
  self.clip_path_id = None
  self.mark_dirty()
}

///|
/// Mark a node as dirty (needs re-render)
pub fn SVGNode::mark_dirty(self : SVGNode) -> Unit {
  self.node_dirty = true
}

///|
/// Clear the dirty flag
pub fn SVGNode::clear_dirty(self : SVGNode) -> Unit {
  self.node_dirty = false
}

///|

///|
/// Helper function for min of two doubles
fn min(a : Double, b : Double) -> Double {
  if a < b {
    a
  } else {
    b
  }
}

///|
/// Helper function for max of two doubles
fn max(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
/// Bounding box for a shape
pub(all) struct BoundingBox {
  min_x : Double
  min_y : Double
  max_x : Double
  max_y : Double
}

///|
pub fn BoundingBox::empty() -> BoundingBox {
  {
    min_x: @double.infinity,
    min_y: @double.infinity,
    max_x: @double.neg_infinity,
    max_y: @double.neg_infinity,
  }
}

///|
pub fn BoundingBox::from_rect(
  x : Double,
  y : Double,
  w : Double,
  h : Double,
) -> BoundingBox {
  { min_x: x, min_y: y, max_x: x + w, max_y: y + h }
}

///|
pub fn BoundingBox::width(self : BoundingBox) -> Double {
  self.max_x - self.min_x
}

///|
pub fn BoundingBox::height(self : BoundingBox) -> Double {
  self.max_y - self.min_y
}

///|
pub fn BoundingBox::is_empty(self : BoundingBox) -> Bool {
  self.min_x > self.max_x || self.min_y > self.max_y
}

///|
pub fn BoundingBox::union(
  self : BoundingBox,
  other : BoundingBox,
) -> BoundingBox {
  if self.is_empty() {
    other
  } else if other.is_empty() {
    self
  } else {
    {
      min_x: min(self.min_x, other.min_x),
      min_y: min(self.min_y, other.min_y),
      max_x: max(self.max_x, other.max_x),
      max_y: max(self.max_y, other.max_y),
    }
  }
}

///|
pub fn BoundingBox::expand_by_point(
  self : BoundingBox,
  x : Double,
  y : Double,
) -> BoundingBox {
  {
    min_x: min(self.min_x, x),
    min_y: min(self.min_y, y),
    max_x: max(self.max_x, x),
    max_y: max(self.max_y, y),
  }
}

///|
/// Check if two bounding boxes intersect
pub fn BoundingBox::intersects(self : BoundingBox, other : BoundingBox) -> Bool {
  if self.is_empty() || other.is_empty() {
    false
  } else {
    self.min_x <= other.max_x &&
    self.max_x >= other.min_x &&
    self.min_y <= other.max_y &&
    self.max_y >= other.min_y
  }
}

///|
/// Check if this bounding box contains a point
pub fn BoundingBox::contains_point(
  self : BoundingBox,
  x : Double,
  y : Double,
) -> Bool {
  x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
}

///|
/// Clipping rectangle for camera/viewport
pub(all) struct ClipRect {
  x : Int
  y : Int
  width : Int
  height : Int
}

///|
pub fn ClipRect::new(x : Int, y : Int, width : Int, height : Int) -> ClipRect {
  { x, y, width, height }
}

///|
pub fn ClipRect::from_size(width : Int, height : Int) -> ClipRect {
  { x: 0, y: 0, width, height }
}

///|
/// Check if a point is inside the clip rect
pub fn ClipRect::contains(self : ClipRect, x : Int, y : Int) -> Bool {
  x >= self.x &&
  x < self.x + self.width &&
  y >= self.y &&
  y < self.y + self.height
}

///|
/// Convert to BoundingBox
pub fn ClipRect::to_bbox(self : ClipRect) -> BoundingBox {
  {
    min_x: self.x.to_double(),
    min_y: self.y.to_double(),
    max_x: (self.x + self.width).to_double(),
    max_y: (self.y + self.height).to_double(),
  }
}

///|
/// Camera for 2D scene navigation
/// Supports pan (translation) and zoom
pub(all) struct Camera {
  mut x : Double // Camera position X (center)
  mut y : Double // Camera position Y (center)
  mut zoom : Double // Zoom level (1.0 = 100%)
  viewport_width : Int // Viewport width in pixels
  viewport_height : Int // Viewport height in pixels
}

///|
pub fn Camera::new(viewport_width : Int, viewport_height : Int) -> Camera {
  { x: 0.0, y: 0.0, zoom: 1.0, viewport_width, viewport_height }
}

///|
/// Move camera by delta
pub fn Camera::pan(self : Camera, dx : Double, dy : Double) -> Unit {
  self.x = self.x + dx
  self.y = self.y + dy
}

///|
/// Set camera position
pub fn Camera::set_position(self : Camera, x : Double, y : Double) -> Unit {
  self.x = x
  self.y = y
}

///|
/// Set zoom level
pub fn Camera::set_zoom(self : Camera, zoom : Double) -> Unit {
  self.zoom = if zoom < 0.1 { 0.1 } else if zoom > 10.0 { 10.0 } else { zoom }
}

///|
/// Zoom by factor (multiply current zoom)
pub fn Camera::zoom_by(self : Camera, factor : Double) -> Unit {
  self.set_zoom(self.zoom * factor)
}

///|
/// Get the world-space bounding box visible through this camera
pub fn Camera::get_visible_bounds(self : Camera) -> BoundingBox {
  let half_w = self.viewport_width.to_double() / 2.0 / self.zoom
  let half_h = self.viewport_height.to_double() / 2.0 / self.zoom
  {
    min_x: self.x - half_w,
    min_y: self.y - half_h,
    max_x: self.x + half_w,
    max_y: self.y + half_h,
  }
}

///|
/// Convert world coordinates to screen coordinates
pub fn Camera::world_to_screen(
  self : Camera,
  wx : Double,
  wy : Double,
) -> (Int, Int) {
  let sx = (wx - self.x) * self.zoom + self.viewport_width.to_double() / 2.0
  let sy = (wy - self.y) * self.zoom + self.viewport_height.to_double() / 2.0
  (sx.to_int(), sy.to_int())
}

///|
/// Convert screen coordinates to world coordinates
pub fn Camera::screen_to_world(
  self : Camera,
  sx : Int,
  sy : Int,
) -> (Double, Double) {
  let wx = (sx.to_double() - self.viewport_width.to_double() / 2.0) / self.zoom +
    self.x
  let wy = (sy.to_double() - self.viewport_height.to_double() / 2.0) / self.zoom +
    self.y
  (wx, wy)
}

///|
/// Get transform matrix for this camera
pub fn Camera::get_transform(self : Camera) -> Transform {
  // Translate to center, scale by zoom, then translate by camera position
  let tx = self.viewport_width.to_double() / 2.0 - self.x * self.zoom
  let ty = self.viewport_height.to_double() / 2.0 - self.y * self.zoom
  { a: self.zoom, b: 0.0, c: 0.0, d: self.zoom, e: tx, f: ty }
}

// ============================================================================
// Hit Testing
// ============================================================================

///|
/// Check if a point is inside a rectangle
fn hit_test_rect(
  px : Double,
  py : Double,
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> Bool {
  if width <= 0.0 || height <= 0.0 {
    return false
  }
  px >= x && px <= x + width && py >= y && py <= y + height
}

///|
/// Check if a point is inside a circle
fn hit_test_circle(
  px : Double,
  py : Double,
  cx : Double,
  cy : Double,
  r : Double,
) -> Bool {
  if r <= 0.0 {
    return false
  }
  let dx = px - cx
  let dy = py - cy
  dx * dx + dy * dy <= r * r
}

///|
/// Check if a point is inside an ellipse
fn hit_test_ellipse(
  px : Double,
  py : Double,
  cx : Double,
  cy : Double,
  rx : Double,
  ry : Double,
) -> Bool {
  if rx <= 0.0 || ry <= 0.0 {
    return false
  }
  let dx = (px - cx) / rx
  let dy = (py - cy) / ry
  dx * dx + dy * dy <= 1.0
}

///|
/// Check if a point is inside a polygon using ray casting algorithm
fn hit_test_polygon(
  px : Double,
  py : Double,
  points : Array[(Double, Double)],
) -> Bool {
  let n = points.length()
  if n < 3 {
    return false
  }
  let mut inside = false
  let mut j = n - 1
  for i in 0.. py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi {
      inside = !inside
    }
    j = i
  }
  inside
}

///|
/// Check if a point is on a line (with tolerance)
fn hit_test_line(
  px : Double,
  py : Double,
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  tolerance : Double,
) -> Bool {
  // Distance from point to line segment
  let dx = x2 - x1
  let dy = y2 - y1
  let len_sq = dx * dx + dy * dy
  if len_sq < 0.0001 {
    // Line is a point
    let d = (px - x1) * (px - x1) + (py - y1) * (py - y1)
    return d <= tolerance * tolerance
  }
  // Project point onto line
  let t = ((px - x1) * dx + (py - y1) * dy) / len_sq
  let t_clamped = if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
  let proj_x = x1 + t_clamped * dx
  let proj_y = y1 + t_clamped * dy
  let dist_sq = (px - proj_x) * (px - proj_x) + (py - proj_y) * (py - proj_y)
  dist_sq <= tolerance * tolerance
}

///|
/// Hit test a shape (without transform)
pub fn hit_test_shape(px : Double, py : Double, shape : Shape) -> Bool {
  match shape {
    Rect(x~, y~, width~, height~, ..) =>
      hit_test_rect(px, py, x, y, width, height)
    Circle(cx~, cy~, r~) => hit_test_circle(px, py, cx, cy, r)
    Ellipse(cx~, cy~, rx~, ry~) => hit_test_ellipse(px, py, cx, cy, rx, ry)
    Line(x1~, y1~, x2~, y2~) => hit_test_line(px, py, x1, y1, x2, y2, 2.0)
    Polyline(points~) => {
      // Hit test each line segment
      for i in 0..<(points.length() - 1) {
        let (x1, y1) = points[i]
        let (x2, y2) = points[i + 1]
        if hit_test_line(px, py, x1, y1, x2, y2, 2.0) {
          return true
        }
      }
      false
    }
    Polygon(points~) => hit_test_polygon(px, py, points)
    Path(_) => false // TODO: convert path to polygon for hit testing
    Text(x~, y~, text~, font_size~) => {
      // Approximate text bounding box
      let char_width = font_size * 0.6
      let width = char_width * text.length().to_double()
      hit_test_rect(px, py, x, y - font_size, width, font_size)
    }
    Image(x~, y~, width~, height~, ..) =>
      hit_test_rect(px, py, x, y, width, height)
    Group => false
  }
}

///|
/// Hit test an SVGNode at a point (in world coordinates)
/// Returns true if the point is inside the node's shape
pub fn SVGNode::hit_test(self : SVGNode, px : Double, py : Double) -> Bool {
  // Apply inverse transform to the point
  let inv = self.transform.inverse()
  let (local_x, local_y) = inv.apply(px, py)
  hit_test_shape(local_x, local_y, self.shape)
}

///|
/// Find all nodes at a point (recursive, returns in front-to-back order)
pub fn SVGNode::hit_test_all(
  self : SVGNode,
  px : Double,
  py : Double,
) -> Array[SVGNode] {
  let results : Array[SVGNode] = []
  hit_test_recursive(self, px, py, Transform::identity(), results)
  results
}

///|
fn hit_test_recursive(
  node : SVGNode,
  px : Double,
  py : Double,
  parent_transform : Transform,
  results : Array[SVGNode],
) -> Unit {
  let world_transform = parent_transform.multiply(node.transform)
  let inv = world_transform.inverse()
  let (local_x, local_y) = inv.apply(px, py)
  // Check children first (front-to-back: last child is on top)
  for i = node.children.length() - 1; i >= 0; i = i - 1 {
    hit_test_recursive(node.children[i], px, py, world_transform, results)
  }
  // Then check this node
  if hit_test_shape(local_x, local_y, node.shape) {
    results.push(node)
  }
}

// ============================================================================
// Animation / Tween System
// ============================================================================

///|
/// Easing functions for smooth animations
pub(all) enum Easing {
  Linear
  EaseIn // Quadratic ease in
  EaseOut // Quadratic ease out
  EaseInOut // Quadratic ease in-out
  EaseInCubic
  EaseOutCubic
  EaseInOutCubic
} derive(Debug, Eq)

///|
/// Apply easing function to a normalized time (0.0 to 1.0)
pub fn Easing::apply(self : Easing, t : Double) -> Double {
  let t_clamped = if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
  match self {
    Linear => t_clamped
    EaseIn => t_clamped * t_clamped
    EaseOut => t_clamped * (2.0 - t_clamped)
    EaseInOut =>
      if t_clamped < 0.5 {
        2.0 * t_clamped * t_clamped
      } else {
        -1.0 + (4.0 - 2.0 * t_clamped) * t_clamped
      }
    EaseInCubic => t_clamped * t_clamped * t_clamped
    EaseOutCubic => {
      let t1 = t_clamped - 1.0
      t1 * t1 * t1 + 1.0
    }
    EaseInOutCubic =>
      if t_clamped < 0.5 {
        4.0 * t_clamped * t_clamped * t_clamped
      } else {
        let t1 = 2.0 * t_clamped - 2.0
        (t1 * t1 * t1 + 2.0) / 2.0
      }
  }
}

///|
/// Animatable property types
pub(all) enum AnimProperty {
  TranslateX(Double) // Target x translation
  TranslateY(Double) // Target y translation
  Translate(Double, Double) // Target (x, y) translation
  ScaleX(Double) // Target x scale
  ScaleY(Double) // Target y scale
  Scale(Double, Double) // Target (sx, sy) scale
  ScaleUniform(Double) // Target uniform scale
  Rotation(Double) // Target rotation in radians
  Opacity(Double) // Target opacity
  FillColor(Color) // Target fill color
}

///|
/// A single tween animation
pub(all) struct Tween {
  target_id : String // ID of the node to animate
  property : AnimProperty
  mut start_value : AnimProperty // Captured at start
  duration : Double // Duration in seconds
  mut elapsed : Double // Elapsed time in seconds
  easing : Easing
  mut started : Bool // Has the animation captured start values?
  mut completed : Bool
}

///|
/// Create a new tween
pub fn Tween::new(
  target_id : String,
  property : AnimProperty,
  duration : Double,
  easing : Easing,
) -> Tween {
  {
    target_id,
    property,
    start_value: property, // Will be overwritten when started
    duration,
    elapsed: 0.0,
    easing,
    started: false,
    completed: false,
  }
}

///|
/// Check if the tween is complete
pub fn Tween::is_complete(self : Tween) -> Bool {
  self.completed
}

///|
/// Update the tween with delta time
/// Returns true if still running, false if complete
pub fn Tween::update(self : Tween, dt : Double, node : SVGNode) -> Bool {
  if self.completed {
    return false
  }
  // Capture start values on first update
  if !self.started {
    self.start_value = capture_property(node, self.property)
    self.started = true
  }
  self.elapsed = self.elapsed + dt
  let t = if self.duration <= 0.0 { 1.0 } else { self.elapsed / self.duration }
  if t >= 1.0 {
    // Complete: set final value
    apply_property(node, self.property, 1.0)
    self.completed = true
    false
  } else {
    // Interpolate
    let eased_t = self.easing.apply(t)
    apply_interpolated_property(node, self.start_value, self.property, eased_t)
    true
  }
}

///|
/// Capture current property value from node
fn capture_property(node : SVGNode, property : AnimProperty) -> AnimProperty {
  let (tx, ty) = node.transform.get_translate()
  let (sx, sy) = node.transform.get_scale()
  let rotation = node.transform.get_rotation()
  match property {
    TranslateX(_) => TranslateX(tx)
    TranslateY(_) => TranslateY(ty)
    Translate(_, _) => Translate(tx, ty)
    ScaleX(_) => ScaleX(sx)
    ScaleY(_) => ScaleY(sy)
    Scale(_, _) => Scale(sx, sy)
    ScaleUniform(_) => ScaleUniform((sx + sy) / 2.0)
    Rotation(_) => Rotation(rotation)
    Opacity(_) => Opacity(node.opacity)
    FillColor(_) =>
      match node.fill {
        SolidColor(c) => FillColor(c)
        _ => FillColor(Color::black())
      }
  }
}

///|
/// Apply final property value to node
fn apply_property(node : SVGNode, property : AnimProperty, t : Double) -> Unit {
  let _ = t // t=1.0 means final value
  match property {
    TranslateX(x) => {
      let (_, ty) = node.transform.get_translate()
      node.transform = Transform::translate(x, ty)
    }
    TranslateY(y) => {
      let (tx, _) = node.transform.get_translate()
      node.transform = Transform::translate(tx, y)
    }
    Translate(x, y) => node.transform = Transform::translate(x, y)
    ScaleX(sx) => {
      let (_, sy) = node.transform.get_scale()
      node.transform = Transform::scale(sx, sy)
    }
    ScaleY(sy) => {
      let (sx, _) = node.transform.get_scale()
      node.transform = Transform::scale(sx, sy)
    }
    Scale(sx, sy) => node.transform = Transform::scale(sx, sy)
    ScaleUniform(s) => node.transform = Transform::scale(s, s)
    Rotation(r) => node.transform = Transform::rotate(r)
    Opacity(o) => node.opacity = o
    FillColor(c) => node.fill = SolidColor(c)
  }
}

///|
/// Apply interpolated property value to node
fn apply_interpolated_property(
  node : SVGNode,
  start : AnimProperty,
  end : AnimProperty,
  t : Double,
) -> Unit {
  match (start, end) {
    (TranslateX(x0), TranslateX(x1)) => {
      let x = lerp(x0, x1, t)
      let (_, ty) = node.transform.get_translate()
      node.transform = Transform::translate(x, ty)
    }
    (TranslateY(y0), TranslateY(y1)) => {
      let y = lerp(y0, y1, t)
      let (tx, _) = node.transform.get_translate()
      node.transform = Transform::translate(tx, y)
    }
    (Translate(x0, y0), Translate(x1, y1)) => {
      let x = lerp(x0, x1, t)
      let y = lerp(y0, y1, t)
      node.transform = Transform::translate(x, y)
    }
    (ScaleX(sx0), ScaleX(sx1)) => {
      let sx = lerp(sx0, sx1, t)
      let (_, sy) = node.transform.get_scale()
      node.transform = Transform::scale(sx, sy)
    }
    (ScaleY(sy0), ScaleY(sy1)) => {
      let sy = lerp(sy0, sy1, t)
      let (sx, _) = node.transform.get_scale()
      node.transform = Transform::scale(sx, sy)
    }
    (Scale(sx0, sy0), Scale(sx1, sy1)) => {
      let sx = lerp(sx0, sx1, t)
      let sy = lerp(sy0, sy1, t)
      node.transform = Transform::scale(sx, sy)
    }
    (ScaleUniform(s0), ScaleUniform(s1)) => {
      let s = lerp(s0, s1, t)
      node.transform = Transform::scale(s, s)
    }
    (Rotation(r0), Rotation(r1)) => {
      let r = lerp(r0, r1, t)
      node.transform = Transform::rotate(r)
    }
    (Opacity(o0), Opacity(o1)) => node.opacity = lerp(o0, o1, t)
    (FillColor(c0), FillColor(c1)) => {
      let c = lerp_color(c0, c1, t)
      node.fill = SolidColor(c)
    }
    _ => () // Mismatched types, do nothing
  }
}

///|
/// Linear interpolation
fn lerp(a : Double, b : Double, t : Double) -> Double {
  a + (b - a) * t
}

///|
/// Color interpolation
fn lerp_color(c0 : Color, c1 : Color, t : Double) -> Color {
  {
    r: lerp(c0.r.to_double(), c1.r.to_double(), t).to_int(),
    g: lerp(c0.g.to_double(), c1.g.to_double(), t).to_int(),
    b: lerp(c0.b.to_double(), c1.b.to_double(), t).to_int(),
    a: lerp(c0.a.to_double(), c1.a.to_double(), t).to_int(),
  }
}

// ============================================================================
// Event System
// ============================================================================

///|
/// Mouse/pointer event data
pub(all) struct PointerEvent {
  x : Double // World coordinates
  y : Double
  button : Int // 0=left, 1=middle, 2=right
  target : String // ID of the target node
  mut propagation_stopped : Bool
}

///|
pub fn PointerEvent::new(
  x : Double,
  y : Double,
  button : Int,
  target : String,
) -> PointerEvent {
  { x, y, button, target, propagation_stopped: false }
}

///|
/// Stop event propagation (prevent bubbling)
pub fn PointerEvent::stop_propagation(self : PointerEvent) -> Unit {
  self.propagation_stopped = true
}

///|
/// Event handler type
pub(all) struct EventHandler {
  call : (PointerEvent) -> Unit
}

///|
/// Event types
pub(all) enum EventType {
  Click
  MouseDown
  MouseUp
  MouseMove
  MouseEnter
  MouseLeave
  DragStart
  DragMove
  DragEnd
} derive(Debug, Eq)

///|
/// Event listener entry
pub(all) struct EventListener {
  event_type : EventType
  handler : EventHandler
}

///|
/// Event manager for handling input events
pub(all) struct EventManager {
  listeners : Map[String, Array[EventListener]] // node_id -> listeners
  mut hovered_node : String? // Currently hovered node
  mut dragging_node : String? // Currently dragging node
  mut drag_start_x : Double
  mut drag_start_y : Double
}

///|
pub fn EventManager::new() -> EventManager {
  {
    listeners: {},
    hovered_node: None,
    dragging_node: None,
    drag_start_x: 0.0,
    drag_start_y: 0.0,
  }
}

///|
/// Register an event listener for a node
pub fn EventManager::on(
  self : EventManager,
  node_id : String,
  event_type : EventType,
  handler : EventHandler,
) -> Unit {
  let listener = EventListener::{ event_type, handler }
  match self.listeners.get(node_id) {
    Some(arr) => arr.push(listener)
    None => self.listeners.set(node_id, [listener])
  }
}

///|
/// Remove all listeners for a node
pub fn EventManager::off_all(self : EventManager, node_id : String) -> Unit {
  let _ = self.listeners.remove(node_id)
}

///|
/// Dispatch a click event at coordinates
pub fn EventManager::dispatch_click(
  self : EventManager,
  x : Double,
  y : Double,
  button : Int,
  scene : Scene,
) -> Unit {
  let hit_nodes = scene.root.hit_test_all(x, y)
  for node in hit_nodes {
    let event = PointerEvent::new(x, y, button, node.id)
    self.dispatch_to_node(node.id, Click, event)
    if event.propagation_stopped {
      break
    }
  }
}

///|
/// Dispatch mouse down event
pub fn EventManager::dispatch_mouse_down(
  self : EventManager,
  x : Double,
  y : Double,
  button : Int,
  scene : Scene,
) -> Unit {
  let hit_nodes = scene.root.hit_test_all(x, y)
  for node in hit_nodes {
    let event = PointerEvent::new(x, y, button, node.id)
    self.dispatch_to_node(node.id, MouseDown, event)
    // Start drag
    if !event.propagation_stopped {
      self.dragging_node = Some(node.id)
      self.drag_start_x = x
      self.drag_start_y = y
      let drag_event = PointerEvent::new(x, y, button, node.id)
      self.dispatch_to_node(node.id, DragStart, drag_event)
    }
    if event.propagation_stopped {
      break
    }
  }
}

///|
/// Dispatch mouse up event
pub fn EventManager::dispatch_mouse_up(
  self : EventManager,
  x : Double,
  y : Double,
  button : Int,
  scene : Scene,
) -> Unit {
  // End drag if dragging
  if self.dragging_node is Some(node_id) {
    let event = PointerEvent::new(x, y, button, node_id)
    self.dispatch_to_node(node_id, DragEnd, event)
    self.dragging_node = None
  }
  // Dispatch mouse up
  let hit_nodes = scene.root.hit_test_all(x, y)
  for node in hit_nodes {
    let event = PointerEvent::new(x, y, button, node.id)
    self.dispatch_to_node(node.id, MouseUp, event)
    if event.propagation_stopped {
      break
    }
  }
}

///|
/// Dispatch mouse move event (handles hover and drag)
pub fn EventManager::dispatch_mouse_move(
  self : EventManager,
  x : Double,
  y : Double,
  scene : Scene,
) -> Unit {
  // Handle drag
  if self.dragging_node is Some(node_id) {
    let event = PointerEvent::new(x, y, 0, node_id)
    self.dispatch_to_node(node_id, DragMove, event)
  }
  // Handle hover (enter/leave)
  let hit_nodes = scene.root.hit_test_all(x, y)
  let new_hovered = if hit_nodes.length() > 0 {
    Some(hit_nodes[0].id)
  } else {
    None
  }
  // Check for mouse leave
  match (self.hovered_node, new_hovered) {
    (Some(old_id), Some(new_id)) =>
      if old_id != new_id {
        let leave_event = PointerEvent::new(x, y, 0, old_id)
        self.dispatch_to_node(old_id, MouseLeave, leave_event)
        let enter_event = PointerEvent::new(x, y, 0, new_id)
        self.dispatch_to_node(new_id, MouseEnter, enter_event)
      }
    (Some(old_id), None) => {
      let leave_event = PointerEvent::new(x, y, 0, old_id)
      self.dispatch_to_node(old_id, MouseLeave, leave_event)
    }
    (None, Some(new_id)) => {
      let enter_event = PointerEvent::new(x, y, 0, new_id)
      self.dispatch_to_node(new_id, MouseEnter, enter_event)
    }
    (None, None) => ()
  }
  self.hovered_node = new_hovered
  // Dispatch mouse move to hovered node
  match new_hovered {
    Some(node_id) => {
      let event = PointerEvent::new(x, y, 0, node_id)
      self.dispatch_to_node(node_id, MouseMove, event)
    }
    None => ()
  }
}

///|
fn EventManager::dispatch_to_node(
  self : EventManager,
  node_id : String,
  event_type : EventType,
  event : PointerEvent,
) -> Unit {
  match self.listeners.get(node_id) {
    Some(listeners) =>
      for listener in listeners {
        if listener.event_type == event_type {
          (listener.handler.call)(event)
        }
      }
    None => ()
  }
}

// ============================================================================
// Collision Detection
// ============================================================================

///|
/// Check collision between two circles
pub fn collide_circle_circle(
  cx1 : Double,
  cy1 : Double,
  r1 : Double,
  cx2 : Double,
  cy2 : Double,
  r2 : Double,
) -> Bool {
  let dx = cx2 - cx1
  let dy = cy2 - cy1
  let dist_sq = dx * dx + dy * dy
  let radii_sum = r1 + r2
  dist_sq <= radii_sum * radii_sum
}

///|
/// Check collision between two axis-aligned rectangles
pub fn collide_rect_rect(
  x1 : Double,
  y1 : Double,
  w1 : Double,
  h1 : Double,
  x2 : Double,
  y2 : Double,
  w2 : Double,
  h2 : Double,
) -> Bool {
  x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2
}

///|
/// Check collision between circle and rectangle
pub fn collide_circle_rect(
  cx : Double,
  cy : Double,
  r : Double,
  rx : Double,
  ry : Double,
  rw : Double,
  rh : Double,
) -> Bool {
  // Find the closest point on the rectangle to the circle center
  let closest_x = if cx < rx { rx } else if cx > rx + rw { rx + rw } else { cx }
  let closest_y = if cy < ry { ry } else if cy > ry + rh { ry + rh } else { cy }
  // Check if the closest point is within the circle
  let dx = cx - closest_x
  let dy = cy - closest_y
  dx * dx + dy * dy <= r * r
}

///|
/// Check collision between two shapes
pub fn collide_shapes(shape1 : Shape, shape2 : Shape) -> Bool {
  match (shape1, shape2) {
    (Circle(cx~, cy~, r~), Circle(..)) => {
      let (cx2, cy2, r2) = match shape2 {
        Circle(cx~, cy~, r~) => (cx, cy, r)
        _ => (0.0, 0.0, 0.0)
      }
      collide_circle_circle(cx, cy, r, cx2, cy2, r2)
    }
    (Rect(x~, y~, width~, height~, ..), Rect(..)) => {
      let (x2, y2, w2, h2) = match shape2 {
        Rect(x~, y~, width~, height~, ..) => (x, y, width, height)
        _ => (0.0, 0.0, 0.0, 0.0)
      }
      collide_rect_rect(x, y, width, height, x2, y2, w2, h2)
    }
    (Circle(cx~, cy~, r~), Rect(x~, y~, width~, height~, ..)) =>
      collide_circle_rect(cx, cy, r, x, y, width, height)
    (Rect(x~, y~, width~, height~, ..), Circle(cx~, cy~, r~)) =>
      collide_circle_rect(cx, cy, r, x, y, width, height)
    (Ellipse(cx~, cy~, rx~, ry~), Ellipse(..)) => {
      // Approximate with average radius
      let r1 = (rx + ry) / 2.0
      let (cx2, cy2, r2) = match shape2 {
        Ellipse(cx~, cy~, rx~, ry~) => (cx, cy, (rx + ry) / 2.0)
        _ => (0.0, 0.0, 0.0)
      }
      collide_circle_circle(cx, cy, r1, cx2, cy2, r2)
    }
    _ => {
      // Fall back to bounding box collision for other shapes
      let bbox1 = get_shape_bbox(shape1)
      let bbox2 = get_shape_bbox(shape2)
      bbox1.intersects(bbox2)
    }
  }
}

///|
/// Get bounding box for a shape
fn get_shape_bbox(shape : Shape) -> BoundingBox {
  match shape {
    Rect(x~, y~, width~, height~, ..) =>
      BoundingBox::from_rect(x, y, width, height)
    Circle(cx~, cy~, r~) =>
      BoundingBox::from_rect(cx - r, cy - r, r * 2.0, r * 2.0)
    Ellipse(cx~, cy~, rx~, ry~) =>
      BoundingBox::from_rect(cx - rx, cy - ry, rx * 2.0, ry * 2.0)
    Image(x~, y~, width~, height~, ..) =>
      BoundingBox::from_rect(x, y, width, height)
    Line(x1~, y1~, x2~, y2~) => {
      let min_x = if x1 < x2 { x1 } else { x2 }
      let max_x = if x1 > x2 { x1 } else { x2 }
      let min_y = if y1 < y2 { y1 } else { y2 }
      let max_y = if y1 > y2 { y1 } else { y2 }
      { min_x, min_y, max_x, max_y }
    }
    Polyline(points~) | Polygon(points~) => {
      if points.is_empty() {
        return BoundingBox::empty()
      }
      let mut min_x = points[0].0
      let mut max_x = points[0].0
      let mut min_y = points[0].1
      let mut max_y = points[0].1
      for p in points {
        if p.0 < min_x {
          min_x = p.0
        }
        if p.0 > max_x {
          max_x = p.0
        }
        if p.1 < min_y {
          min_y = p.1
        }
        if p.1 > max_y {
          max_y = p.1
        }
      }
      { min_x, min_y, max_x, max_y }
    }
    _ => BoundingBox::empty()
  }
}

///|
/// Check collision between two SVGNodes (considering transforms)
pub fn SVGNode::collides_with(self : SVGNode, other : SVGNode) -> Bool {
  // Get transformed bounding boxes for quick rejection
  let bbox1 = self.transform.apply_bbox(get_shape_bbox(self.shape))
  let bbox2 = other.transform.apply_bbox(get_shape_bbox(other.shape))
  if !bbox1.intersects(bbox2) {
    return false
  }
  // For now, use bounding box collision
  // More precise collision would require transforming shapes
  true
}

// ============================================================================
// Object Pooling
// ============================================================================

///|
/// Generic object pool for reusing objects
pub(all) struct ObjectPool[T] {
  available : Array[T]
  factory : () -> T
  reset : (T) -> Unit
}

///|
pub fn[T] ObjectPool::new(
  factory : () -> T,
  reset : (T) -> Unit,
  initial_size : Int,
) -> ObjectPool[T] {
  let pool : ObjectPool[T] = { available: [], factory, reset }
  for _ in 0.. T {
  if self.available.is_empty() {
    (self.factory)()
  } else {
    self.available.pop().unwrap()
  }
}

///|
/// Release an object back to the pool
pub fn[T] ObjectPool::release(self : ObjectPool[T], obj : T) -> Unit {
  (self.reset)(obj)
  self.available.push(obj)
}

///|
/// Get the number of available objects
pub fn[T] ObjectPool::available_count(self : ObjectPool[T]) -> Int {
  self.available.length()
}

// ============================================================================
// RadialGradient
// ============================================================================

///|
/// Radial gradient definition
pub(all) struct RadialGradient {
  cx : Double // Center x (0.0-1.0 relative)
  cy : Double // Center y
  fx : Double // Focal point x
  fy : Double // Focal point y
  r : Double // Radius
  stops : Array[GradientStop]
  spread_method : SpreadMethod
  units : GradientUnits
  transform : Transform
}

///|
pub fn RadialGradient::new(
  cx : Double,
  cy : Double,
  r : Double,
  stops : Array[GradientStop],
) -> RadialGradient {
  {
    cx,
    cy,
    fx: cx,
    fy: cy,
    r,
    stops,
    spread_method: Pad,
    units: ObjectBoundingBox,
    transform: Transform::identity(),
  }
}

///|
/// Get color at a point (px, py) relative to the gradient bounds
pub fn RadialGradient::color_at(
  self : RadialGradient,
  px : Double,
  py : Double,
  width : Double,
  height : Double,
) -> Color {
  // Convert to gradient space
  let gx = self.cx * width
  let gy = self.cy * height
  let gr = self.r * (if width < height { width } else { height })
  // Distance from center
  let dx = px - gx
  let dy = py - gy
  let dist = (dx * dx + dy * dy).sqrt()
  // Normalized position (0.0 at center, 1.0 at radius)
  let mut t = if gr > 0.0 { dist / gr } else { 0.0 }
  // Apply spread method
  t = apply_spread(t, self.spread_method)
  // Interpolate color
  interpolate_gradient_color(t, self.stops)
}

///|
fn apply_spread(t : Double, spread : SpreadMethod) -> Double {
  match spread {
    Pad => if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
    Repeat => t - t.floor()
    Reflect => {
      let t2 = t - t.floor()
      if t.to_int() % 2 == 0 {
        t2
      } else {
        1.0 - t2
      }
    }
  }
}

///|
/// Build a lookup table for fast radial gradient color sampling
pub fn RadialGradient::build_lut(
  self : RadialGradient,
  size : Int,
) -> Array[Color] {
  let lut = Array::make(size, Color::black())
  for i in 0.. Color {
  if stops.is_empty() {
    return Color::black()
  }
  if stops.length() == 1 {
    return stops[0].color
  }
  // Find surrounding stops
  let mut prev_stop = stops[0]
  let mut next_stop = stops[stops.length() - 1]
  for stop in stops {
    if stop.offset <= t {
      prev_stop = stop
    }
    if stop.offset >= t && next_stop.offset < stop.offset {
      next_stop = stop
    }
  }
  for stop in stops {
    if stop.offset >= t {
      next_stop = stop
      break
    }
  }
  // Interpolate
  if prev_stop.offset >= next_stop.offset {
    return prev_stop.color
  }
  let local_t = (t - prev_stop.offset) / (next_stop.offset - prev_stop.offset)
  lerp_color(prev_stop.color, next_stop.color, local_t)
}

// ============================================================================
// Particle System
// ============================================================================

///|
/// Single particle state
pub(all) struct Particle {
  mut x : Double
  mut y : Double
  mut vx : Double // Velocity x
  mut vy : Double // Velocity y
  mut life : Double // Remaining life (seconds)
  mut max_life : Double // Initial life
  mut size : Double
  mut color : Color
  mut active : Bool
}

///|
pub fn Particle::new() -> Particle {
  {
    x: 0.0,
    y: 0.0,
    vx: 0.0,
    vy: 0.0,
    life: 0.0,
    max_life: 1.0,
    size: 5.0,
    color: Color::white(),
    active: false,
  }
}

///|
/// Particle emitter configuration
pub(all) struct EmitterConfig {
  emit_rate : Double // Particles per second
  life_min : Double // Minimum particle life
  life_max : Double // Maximum particle life
  speed_min : Double // Minimum initial speed
  speed_max : Double // Maximum initial speed
  angle_min : Double // Minimum emission angle (radians)
  angle_max : Double // Maximum emission angle
  size_start : Double // Initial size
  size_end : Double // Final size (interpolated)
  color_start : Color
  color_end : Color
  gravity_x : Double
  gravity_y : Double
}

///|
pub fn EmitterConfig::default() -> EmitterConfig {
  {
    emit_rate: 10.0,
    life_min: 0.5,
    life_max: 2.0,
    speed_min: 50.0,
    speed_max: 100.0,
    angle_min: 0.0,
    angle_max: 6.28318, // 2π
    size_start: 10.0,
    size_end: 2.0,
    color_start: Color::white(),
    color_end: Color::transparent(),
    gravity_x: 0.0,
    gravity_y: 0.0,
  }
}

///|
/// Particle emitter
pub(all) struct ParticleEmitter {
  x : Double
  y : Double
  config : EmitterConfig
  particles : Array[Particle]
  mut emit_accumulator : Double
  mut active : Bool
}

///|
pub fn ParticleEmitter::new(
  x : Double,
  y : Double,
  config : EmitterConfig,
  max_particles : Int,
) -> ParticleEmitter {
  let particles : Array[Particle] = []
  for _ in 0.. Unit {
  // Emit new particles
  if self.active {
    self.emit_accumulator = self.emit_accumulator + dt * self.config.emit_rate
    while self.emit_accumulator >= 1.0 {
      self.emit_one()
      self.emit_accumulator = self.emit_accumulator - 1.0
    }
  }
  // Update existing particles
  for p in self.particles {
    if p.active {
      // Apply velocity
      p.x = p.x + p.vx * dt
      p.y = p.y + p.vy * dt
      // Apply gravity
      p.vx = p.vx + self.config.gravity_x * dt
      p.vy = p.vy + self.config.gravity_y * dt
      // Update life
      p.life = p.life - dt
      if p.life <= 0.0 {
        p.active = false
      } else {
        // Interpolate size and color based on life
        let t = 1.0 - p.life / p.max_life
        p.size = lerp(self.config.size_start, self.config.size_end, t)
        p.color = lerp_color(self.config.color_start, self.config.color_end, t)
      }
    }
  }
}

///|
fn ParticleEmitter::emit_one(self : ParticleEmitter) -> Unit {
  // Find inactive particle
  for p in self.particles {
    if !p.active {
      p.active = true
      p.x = self.x
      p.y = self.y
      // Random life
      p.life = random_range(self.config.life_min, self.config.life_max)
      p.max_life = p.life
      // Random angle and speed
      let angle = random_range(self.config.angle_min, self.config.angle_max)
      let speed = random_range(self.config.speed_min, self.config.speed_max)
      p.vx = @math.cos(angle) * speed
      p.vy = @math.sin(angle) * speed
      // Initial size and color
      p.size = self.config.size_start
      p.color = self.config.color_start
      return
    }
  }
}

///|
/// Simple pseudo-random number generator
pub(all) struct SimpleRNG {
  mut state : Int
}

///|
pub fn SimpleRNG::new(seed : Int) -> SimpleRNG {
  { state: seed }
}

///|
pub fn SimpleRNG::next(self : SimpleRNG) -> Double {
  // Linear congruential generator
  self.state = (self.state * 1103515245 + 12345) & 0x7FFFFFFF
  self.state.to_double() / 2147483647.0
}

///|
pub fn SimpleRNG::range(self : SimpleRNG, min : Double, max : Double) -> Double {
  let t = self.next()
  min + t * (max - min)
}

///|
/// Default global RNG instance
let default_rng : SimpleRNG = SimpleRNG::new(12345)

///|
fn random_range(min : Double, max : Double) -> Double {
  default_rng.range(min, max)
}

///|
/// Get active particle count
pub fn ParticleEmitter::active_count(self : ParticleEmitter) -> Int {
  let mut count = 0
  for p in self.particles {
    if p.active {
      count = count + 1
    }
  }
  count
}

// ============================================================================
// Path Animation
// ============================================================================

///|
/// Path follower for animating along a path
pub(all) struct PathFollower {
  path : Array[PathCommand]
  polyline : Array[(Double, Double)] // Flattened path points
  lengths : Array[Double] // Cumulative lengths
  total_length : Double
  mut progress : Double // 0.0 to 1.0
  mut loop_anim : Bool
}

///|
pub fn PathFollower::new(path_data : String) -> PathFollower {
  let path = parse_path(path_data)
  let polyline = flatten_path(path)
  let (lengths, total) = compute_path_lengths(polyline)
  {
    path,
    polyline,
    lengths,
    total_length: total,
    progress: 0.0,
    loop_anim: false,
  }
}

///|
fn flatten_path(path : Array[PathCommand]) -> Array[(Double, Double)] {
  // Simplified flattening - converts path to line segments
  let points : Array[(Double, Double)] = []
  let mut cx = 0.0
  let mut cy = 0.0
  for cmd in path {
    match cmd {
      MoveTo(x, y) => {
        cx = x
        cy = y
        points.push((cx, cy))
      }
      LineTo(x, y) => {
        cx = x
        cy = y
        points.push((cx, cy))
      }
      MoveToRel(dx, dy) => {
        cx = cx + dx
        cy = cy + dy
        points.push((cx, cy))
      }
      LineToRel(dx, dy) => {
        cx = cx + dx
        cy = cy + dy
        points.push((cx, cy))
      }
      HorizontalLineTo(x) => {
        cx = x
        points.push((cx, cy))
      }
      VerticalLineTo(y) => {
        cy = y
        points.push((cx, cy))
      }
      ClosePath => if points.length() > 0 { points.push(points[0]) }
      _ => () // Skip complex curves for now
    }
  }
  points
}

///|
fn compute_path_lengths(
  points : Array[(Double, Double)],
) -> (Array[Double], Double) {
  let lengths : Array[Double] = [0.0]
  let mut total = 0.0
  for i in 1.. (Double, Double) {
  if self.polyline.is_empty() {
    return (0.0, 0.0)
  }
  if self.total_length <= 0.0 {
    return self.polyline[0]
  }
  let target_len = self.progress * self.total_length
  // Find segment
  for i in 1..= target_len {
      let prev_len = self.lengths[i - 1]
      let seg_len = self.lengths[i] - prev_len
      if seg_len <= 0.0 {
        return self.polyline[i - 1]
      }
      let t = (target_len - prev_len) / seg_len
      let (x1, y1) = self.polyline[i - 1]
      let (x2, y2) = self.polyline[i]
      return (lerp(x1, x2, t), lerp(y1, y2, t))
    }
  }
  self.polyline[self.polyline.length() - 1]
}

///|
/// Update progress with delta time and speed
pub fn PathFollower::update(
  self : PathFollower,
  dt : Double,
  speed : Double,
) -> Bool {
  if self.total_length <= 0.0 {
    return false
  }
  self.progress = self.progress + dt * speed / self.total_length
  if self.progress >= 1.0 {
    if self.loop_anim {
      self.progress = self.progress - 1.0
      true
    } else {
      self.progress = 1.0
      false
    }
  } else {
    true
  }
}

// ============================================================================
// Blend Modes (mix-blend-mode / isolation)
// ============================================================================

///|
/// CSS mix-blend-mode values
pub(all) enum BlendMode {
  Normal
  Multiply
  Screen
  Overlay
  Darken
  Lighten
  ColorDodge
  ColorBurn
  HardLight
  SoftLight
  Difference
  Exclusion
  Hue
  Saturation
  ColorMode // 'color' in CSS
  Luminosity
} derive(Debug, Eq)

///|
/// CSS isolation values
pub(all) enum Isolation {
  Auto
  Isolate
} derive(Debug, Eq)

///|
/// Blend two colors using the specified blend mode
pub fn blend_with_mode(
  backdrop : Color,
  source : Color,
  mode : BlendMode,
) -> Color {
  let bd_r = backdrop.r.to_double() / 255.0
  let bd_g = backdrop.g.to_double() / 255.0
  let bd_b = backdrop.b.to_double() / 255.0
  let bd_a = backdrop.a.to_double() / 255.0
  let src_r = source.r.to_double() / 255.0
  let src_g = source.g.to_double() / 255.0
  let src_b = source.b.to_double() / 255.0
  let src_a = source.a.to_double() / 255.0
  // Apply blend mode to RGB
  let (blended_r, blended_g, blended_b) = match mode {
    Normal => (src_r, src_g, src_b)
    Multiply => (bd_r * src_r, bd_g * src_g, bd_b * src_b)
    Screen =>
      (
        1.0 - (1.0 - bd_r) * (1.0 - src_r),
        1.0 - (1.0 - bd_g) * (1.0 - src_g),
        1.0 - (1.0 - bd_b) * (1.0 - src_b),
      )
    Overlay =>
      (
        blend_overlay_channel(bd_r, src_r),
        blend_overlay_channel(bd_g, src_g),
        blend_overlay_channel(bd_b, src_b),
      )
    Darken =>
      (
        if bd_r < src_r {
          bd_r
        } else {
          src_r
        },
        if bd_g < src_g {
          bd_g
        } else {
          src_g
        },
        if bd_b < src_b {
          bd_b
        } else {
          src_b
        },
      )
    Lighten =>
      (
        if bd_r > src_r {
          bd_r
        } else {
          src_r
        },
        if bd_g > src_g {
          bd_g
        } else {
          src_g
        },
        if bd_b > src_b {
          bd_b
        } else {
          src_b
        },
      )
    ColorDodge =>
      (
        blend_color_dodge_channel(bd_r, src_r),
        blend_color_dodge_channel(bd_g, src_g),
        blend_color_dodge_channel(bd_b, src_b),
      )
    ColorBurn =>
      (
        blend_color_burn_channel(bd_r, src_r),
        blend_color_burn_channel(bd_g, src_g),
        blend_color_burn_channel(bd_b, src_b),
      )
    HardLight =>
      (
        blend_overlay_channel(src_r, bd_r), // HardLight is Overlay with args swapped
        blend_overlay_channel(src_g, bd_g),
        blend_overlay_channel(src_b, bd_b),
      )
    SoftLight =>
      (
        blend_soft_light_channel(bd_r, src_r),
        blend_soft_light_channel(bd_g, src_g),
        blend_soft_light_channel(bd_b, src_b),
      )
    Difference =>
      ((bd_r - src_r).abs(), (bd_g - src_g).abs(), (bd_b - src_b).abs())
    Exclusion =>
      (
        bd_r + src_r - 2.0 * bd_r * src_r,
        bd_g + src_g - 2.0 * bd_g * src_g,
        bd_b + src_b - 2.0 * bd_b * src_b,
      )
    Hue => blend_hue(bd_r, bd_g, bd_b, src_r, src_g, src_b)
    Saturation => blend_saturation(bd_r, bd_g, bd_b, src_r, src_g, src_b)
    ColorMode => blend_color_mode(bd_r, bd_g, bd_b, src_r, src_g, src_b)
    Luminosity => blend_luminosity(bd_r, bd_g, bd_b, src_r, src_g, src_b)
  }
  // Porter-Duff compositing with source-over
  let out_a = src_a + bd_a * (1.0 - src_a)
  if out_a < 0.001 {
    return Color::transparent()
  }
  let out_r = (src_a * blended_r + bd_a * bd_r * (1.0 - src_a)) / out_a
  let out_g = (src_a * blended_g + bd_a * bd_g * (1.0 - src_a)) / out_a
  let out_b = (src_a * blended_b + bd_a * bd_b * (1.0 - src_a)) / out_a
  Color::rgba(
    clamp_int((out_r * 255.0).to_int(), 0, 255),
    clamp_int((out_g * 255.0).to_int(), 0, 255),
    clamp_int((out_b * 255.0).to_int(), 0, 255),
    clamp_int((out_a * 255.0).to_int(), 0, 255),
  )
}

///|
/// Overlay blend for a single channel
fn blend_overlay_channel(backdrop : Double, source : Double) -> Double {
  if backdrop < 0.5 {
    2.0 * backdrop * source
  } else {
    1.0 - 2.0 * (1.0 - backdrop) * (1.0 - source)
  }
}

///|
/// Color dodge blend for a single channel
fn blend_color_dodge_channel(backdrop : Double, source : Double) -> Double {
  if backdrop < 0.001 {
    0.0
  } else if source >= 0.999 {
    1.0
  } else {
    let result = backdrop / (1.0 - source)
    if result > 1.0 {
      1.0
    } else {
      result
    }
  }
}

///|
/// Color burn blend for a single channel
fn blend_color_burn_channel(backdrop : Double, source : Double) -> Double {
  if backdrop >= 0.999 {
    1.0
  } else if source < 0.001 {
    0.0
  } else {
    let result = 1.0 - (1.0 - backdrop) / source
    if result < 0.0 {
      0.0
    } else {
      result
    }
  }
}

///|
/// Soft light blend for a single channel
fn blend_soft_light_channel(backdrop : Double, source : Double) -> Double {
  if source <= 0.5 {
    backdrop - (1.0 - 2.0 * source) * backdrop * (1.0 - backdrop)
  } else {
    let d = if backdrop <= 0.25 {
      ((16.0 * backdrop - 12.0) * backdrop + 4.0) * backdrop
    } else {
      sqrt_approx(backdrop)
    }
    backdrop + (2.0 * source - 1.0) * (d - backdrop)
  }
}

///|
/// Approximate square root
fn sqrt_approx(x : Double) -> Double {
  if x < 0.0 {
    return 0.0
  }
  // Newton-Raphson iteration
  let mut guess = x / 2.0
  if guess < 0.001 {
    guess = 0.001
  }
  for _ in 0..<10 {
    guess = (guess + x / guess) / 2.0
  }
  guess
}

///|
/// Convert RGB to HSL
fn rgb_to_hsl(r : Double, g : Double, b : Double) -> (Double, Double, Double) {
  let max_val = if r > g {
    if r > b {
      r
    } else {
      b
    }
  } else if g > b {
    g
  } else {
    b
  }
  let min_val = if r < g {
    if r < b {
      r
    } else {
      b
    }
  } else if g < b {
    g
  } else {
    b
  }
  let l = (max_val + min_val) / 2.0
  if max_val - min_val < 0.001 {
    return (0.0, 0.0, l) // Achromatic
  }
  let d = max_val - min_val
  let s = if l > 0.5 {
    d / (2.0 - max_val - min_val)
  } else {
    d / (max_val + min_val)
  }
  let h = if max_val - r < 0.001 {
    let h_raw = (g - b) / d
    if g < b {
      h_raw + 6.0
    } else {
      h_raw
    }
  } else if max_val - g < 0.001 {
    (b - r) / d + 2.0
  } else {
    (r - g) / d + 4.0
  }
  (h / 6.0, s, l)
}

///|
/// Convert HSL to RGB
fn hsl_to_rgb(h : Double, s : Double, l : Double) -> (Double, Double, Double) {
  if s < 0.001 {
    return (l, l, l)
  }
  let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }
  let p = 2.0 * l - q
  (
    hue_to_rgb(p, q, h + 1.0 / 3.0),
    hue_to_rgb(p, q, h),
    hue_to_rgb(p, q, h - 1.0 / 3.0),
  )
}

///|
fn hue_to_rgb(p : Double, q : Double, t_raw : Double) -> Double {
  let mut t = t_raw
  if t < 0.0 {
    t = t + 1.0
  }
  if t > 1.0 {
    t = t - 1.0
  }
  if t < 1.0 / 6.0 {
    return p + (q - p) * 6.0 * t
  }
  if t < 0.5 {
    return q
  }
  if t < 2.0 / 3.0 {
    return p + (q - p) * (2.0 / 3.0 - t) * 6.0
  }
  p
}

///|
/// Get luminosity of RGB
fn get_luminosity(r : Double, g : Double, b : Double) -> Double {
  0.3 * r + 0.59 * g + 0.11 * b
}

///|
/// Set luminosity of RGB
fn set_luminosity(
  r : Double,
  g : Double,
  b : Double,
  target_lum : Double,
) -> (Double, Double, Double) {
  let current_lum = get_luminosity(r, g, b)
  let d = target_lum - current_lum
  clip_color(r + d, g + d, b + d)
}

///|
/// Clip color to valid range
fn clip_color(r : Double, g : Double, b : Double) -> (Double, Double, Double) {
  let lum = get_luminosity(r, g, b)
  let min_val = if r < g {
    if r < b {
      r
    } else {
      b
    }
  } else if g < b {
    g
  } else {
    b
  }
  let max_val = if r > g {
    if r > b {
      r
    } else {
      b
    }
  } else if g > b {
    g
  } else {
    b
  }
  let mut out_r = r
  let mut out_g = g
  let mut out_b = b
  if min_val < 0.0 && lum - min_val > 0.001 {
    let factor = lum / (lum - min_val)
    out_r = lum + (out_r - lum) * factor
    out_g = lum + (out_g - lum) * factor
    out_b = lum + (out_b - lum) * factor
  }
  if max_val > 1.0 && max_val - lum > 0.001 {
    let factor = (1.0 - lum) / (max_val - lum)
    out_r = lum + (out_r - lum) * factor
    out_g = lum + (out_g - lum) * factor
    out_b = lum + (out_b - lum) * factor
  }
  (out_r, out_g, out_b)
}

///|
/// Hue blend mode
fn blend_hue(
  bd_r : Double,
  bd_g : Double,
  bd_b : Double,
  src_r : Double,
  src_g : Double,
  src_b : Double,
) -> (Double, Double, Double) {
  let (src_h, _, _) = rgb_to_hsl(src_r, src_g, src_b)
  let (_, bd_s, bd_l) = rgb_to_hsl(bd_r, bd_g, bd_b)
  let (r, g, b) = hsl_to_rgb(src_h, bd_s, bd_l)
  set_luminosity(r, g, b, get_luminosity(bd_r, bd_g, bd_b))
}

///|
/// Saturation blend mode
fn blend_saturation(
  bd_r : Double,
  bd_g : Double,
  bd_b : Double,
  src_r : Double,
  src_g : Double,
  src_b : Double,
) -> (Double, Double, Double) {
  let (bd_h, _, bd_l) = rgb_to_hsl(bd_r, bd_g, bd_b)
  let (_, src_s, _) = rgb_to_hsl(src_r, src_g, src_b)
  let (r, g, b) = hsl_to_rgb(bd_h, src_s, bd_l)
  set_luminosity(r, g, b, get_luminosity(bd_r, bd_g, bd_b))
}

///|
/// Color blend mode
fn blend_color_mode(
  bd_r : Double,
  bd_g : Double,
  bd_b : Double,
  src_r : Double,
  src_g : Double,
  src_b : Double,
) -> (Double, Double, Double) {
  let (src_h, src_s, _) = rgb_to_hsl(src_r, src_g, src_b)
  let (_, _, bd_l) = rgb_to_hsl(bd_r, bd_g, bd_b)
  let (r, g, b) = hsl_to_rgb(src_h, src_s, bd_l)
  set_luminosity(r, g, b, get_luminosity(bd_r, bd_g, bd_b))
}

///|
/// Luminosity blend mode
fn blend_luminosity(
  bd_r : Double,
  bd_g : Double,
  bd_b : Double,
  src_r : Double,
  src_g : Double,
  src_b : Double,
) -> (Double, Double, Double) {
  set_luminosity(bd_r, bd_g, bd_b, get_luminosity(src_r, src_g, src_b))
}

///|
/// Blend two images using the specified blend mode
pub fn blend_images(
  backdrop : Image,
  source : Image,
  mode : BlendMode,
) -> Image {
  let width = if backdrop.width < source.width {
    backdrop.width
  } else {
    source.width
  }
  let height = if backdrop.height < source.height {
    backdrop.height
  } else {
    source.height
  }
  let result = Image::new(width, height)
  for y in 0.. Array[Array[Color]] {
  if radius <= 0 || pixels.is_empty() {
    return pixels
  }
  let height = pixels.length()
  let width = if height > 0 { pixels[0].length() } else { 0 }
  // Create output buffer
  let output : Array[Array[Color]] = []
  for _ in 0.. Int {
  if v < min {
    min
  } else if v > max {
    max
  } else {
    v
  }
}

///|
/// Apply brightness filter to a color
pub fn apply_brightness(color : Color, factor : Double) -> Color {
  {
    r: clamp_int((color.r.to_double() * factor).to_int(), 0, 255),
    g: clamp_int((color.g.to_double() * factor).to_int(), 0, 255),
    b: clamp_int((color.b.to_double() * factor).to_int(), 0, 255),
    a: color.a,
  }
}

///|
/// Apply grayscale filter to a color
pub fn apply_grayscale(color : Color, amount : Double) -> Color {
  let gray = (color.r.to_double() * 0.299 +
  color.g.to_double() * 0.587 +
  color.b.to_double() * 0.114).to_int()
  {
    r: lerp(color.r.to_double(), gray.to_double(), amount).to_int(),
    g: lerp(color.g.to_double(), gray.to_double(), amount).to_int(),
    b: lerp(color.b.to_double(), gray.to_double(), amount).to_int(),
    a: color.a,
  }
}

///|
/// Apply contrast filter to a color
pub fn apply_contrast(color : Color, factor : Double) -> Color {
  // Contrast formula: ((value - 128) * factor) + 128
  let r = ((color.r.to_double() - 128.0) * factor + 128.0).to_int()
  let g = ((color.g.to_double() - 128.0) * factor + 128.0).to_int()
  let b = ((color.b.to_double() - 128.0) * factor + 128.0).to_int()
  {
    r: clamp_int(r, 0, 255),
    g: clamp_int(g, 0, 255),
    b: clamp_int(b, 0, 255),
    a: color.a,
  }
}

///|
/// Apply sepia filter to a color
pub fn apply_sepia(color : Color, amount : Double) -> Color {
  // Sepia matrix coefficients
  let r = color.r.to_double()
  let g = color.g.to_double()
  let b = color.b.to_double()
  // Sepia tone calculation
  let sepia_r = r * 0.393 + g * 0.769 + b * 0.189
  let sepia_g = r * 0.349 + g * 0.686 + b * 0.168
  let sepia_b = r * 0.272 + g * 0.534 + b * 0.131
  // Interpolate between original and sepia
  {
    r: clamp_int(lerp(r, sepia_r, amount).to_int(), 0, 255),
    g: clamp_int(lerp(g, sepia_g, amount).to_int(), 0, 255),
    b: clamp_int(lerp(b, sepia_b, amount).to_int(), 0, 255),
    a: color.a,
  }
}

///|
/// Apply hue rotation to a color
pub fn apply_hue_rotate(color : Color, angle_degrees : Double) -> Color {
  // Convert to radians
  let angle = angle_degrees * 3.14159265358979 / 180.0
  let cos_a = cos_approx(angle)
  let sin_a = sin_approx(angle)
  let r = color.r.to_double() / 255.0
  let g = color.g.to_double() / 255.0
  let b = color.b.to_double() / 255.0
  // Hue rotation matrix (based on SVG spec)
  let matrix_00 = 0.213 + cos_a * 0.787 - sin_a * 0.213
  let matrix_01 = 0.715 - cos_a * 0.715 - sin_a * 0.715
  let matrix_02 = 0.072 - cos_a * 0.072 + sin_a * 0.928
  let matrix_10 = 0.213 - cos_a * 0.213 + sin_a * 0.143
  let matrix_11 = 0.715 + cos_a * 0.285 + sin_a * 0.140
  let matrix_12 = 0.072 - cos_a * 0.072 - sin_a * 0.283
  let matrix_20 = 0.213 - cos_a * 0.213 - sin_a * 0.787
  let matrix_21 = 0.715 - cos_a * 0.715 + sin_a * 0.715
  let matrix_22 = 0.072 + cos_a * 0.928 + sin_a * 0.072
  let new_r = r * matrix_00 + g * matrix_01 + b * matrix_02
  let new_g = r * matrix_10 + g * matrix_11 + b * matrix_12
  let new_b = r * matrix_20 + g * matrix_21 + b * matrix_22
  {
    r: clamp_int((new_r * 255.0).to_int(), 0, 255),
    g: clamp_int((new_g * 255.0).to_int(), 0, 255),
    b: clamp_int((new_b * 255.0).to_int(), 0, 255),
    a: color.a,
  }
}

///|
/// Approximate cosine function
fn cos_approx(x : Double) -> Double {
  let pi = 3.14159265358979
  let pi2 = 6.28318530717959
  // Normalize to [0, 2π]
  let mut normalized = x
  while normalized < 0.0 {
    normalized = normalized + pi2
  }
  while normalized >= pi2 {
    normalized = normalized - pi2
  }
  // Reduce to [-π, π] for better Taylor accuracy
  if normalized > pi {
    normalized = normalized - pi2
  }
  // Taylor series approximation for cos (accurate near 0)
  let x2 = normalized * normalized
  let x4 = x2 * x2
  let x6 = x4 * x2
  let x8 = x4 * x4
  let x10 = x4 * x6
  1.0 - x2 / 2.0 + x4 / 24.0 - x6 / 720.0 + x8 / 40320.0 - x10 / 3628800.0
}

///|
/// Approximate sine function
fn sin_approx(x : Double) -> Double {
  let pi = 3.14159265358979
  let pi2 = 6.28318530717959
  // Normalize to [0, 2π]
  let mut normalized = x
  while normalized < 0.0 {
    normalized = normalized + pi2
  }
  while normalized >= pi2 {
    normalized = normalized - pi2
  }
  // Reduce to [-π, π]
  if normalized > pi {
    normalized = normalized - pi2
  }
  // Taylor series for sin: x - x³/6 + x⁵/120 - x⁷/5040 + x⁹/362880
  let x2 = normalized * normalized
  let x3 = normalized * x2
  let x5 = x3 * x2
  let x7 = x5 * x2
  let x9 = x7 * x2
  normalized - x3 / 6.0 + x5 / 120.0 - x7 / 5040.0 + x9 / 362880.0
}

///|
/// Apply invert filter to a color
pub fn apply_invert(color : Color, amount : Double) -> Color {
  let inv_r = 255 - color.r
  let inv_g = 255 - color.g
  let inv_b = 255 - color.b
  {
    r: lerp(color.r.to_double(), inv_r.to_double(), amount).to_int(),
    g: lerp(color.g.to_double(), inv_g.to_double(), amount).to_int(),
    b: lerp(color.b.to_double(), inv_b.to_double(), amount).to_int(),
    a: color.a,
  }
}

///|
/// Apply saturate filter to a color
pub fn apply_saturate(color : Color, factor : Double) -> Color {
  // Saturation matrix based on luminance
  let r = color.r.to_double() / 255.0
  let g = color.g.to_double() / 255.0
  let b = color.b.to_double() / 255.0
  // Luminance coefficients
  let lum_r = 0.2126
  let lum_g = 0.7152
  let lum_b = 0.0722
  // Saturation matrix
  let sr = (1.0 - factor) * lum_r + factor
  let sg = (1.0 - factor) * lum_g
  let sb = (1.0 - factor) * lum_b
  let new_r = r * sr + g * sg + b * sb
  let new_g = r * ((1.0 - factor) * lum_r) +
    g * ((1.0 - factor) * lum_g + factor) +
    b * ((1.0 - factor) * lum_b)
  let new_b = r * ((1.0 - factor) * lum_r) +
    g * ((1.0 - factor) * lum_g) +
    b * ((1.0 - factor) * lum_b + factor)
  {
    r: clamp_int((new_r * 255.0).to_int(), 0, 255),
    g: clamp_int((new_g * 255.0).to_int(), 0, 255),
    b: clamp_int((new_b * 255.0).to_int(), 0, 255),
    a: color.a,
  }
}

///|
/// Apply color matrix filter (feColorMatrix)
/// Matrix is 5x4 (20 values) in row-major order:
/// [R'] = [a00 a01 a02 a03 a04] [R]
/// [G'] = [a10 a11 a12 a13 a14] [G]
/// [B'] = [a20 a21 a22 a23 a24] [B]
/// [A'] = [a30 a31 a32 a33 a34] [A]
///                              [1]
pub fn apply_color_matrix(color : Color, matrix : FixedArray[Double]) -> Color {
  if matrix.length() != 20 {
    return color // Invalid matrix
  }
  let r = color.r.to_double() / 255.0
  let g = color.g.to_double() / 255.0
  let b = color.b.to_double() / 255.0
  let a = color.a.to_double() / 255.0
  let new_r = r * matrix[0] +
    g * matrix[1] +
    b * matrix[2] +
    a * matrix[3] +
    matrix[4]
  let new_g = r * matrix[5] +
    g * matrix[6] +
    b * matrix[7] +
    a * matrix[8] +
    matrix[9]
  let new_b = r * matrix[10] +
    g * matrix[11] +
    b * matrix[12] +
    a * matrix[13] +
    matrix[14]
  let new_a = r * matrix[15] +
    g * matrix[16] +
    b * matrix[17] +
    a * matrix[18] +
    matrix[19]
  {
    r: clamp_int((new_r * 255.0).to_int(), 0, 255),
    g: clamp_int((new_g * 255.0).to_int(), 0, 255),
    b: clamp_int((new_b * 255.0).to_int(), 0, 255),
    a: clamp_int((new_a * 255.0).to_int(), 0, 255),
  }
}

///|
/// Create identity color matrix
pub fn identity_matrix() -> FixedArray[Double] {
  [
    1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
  ]
}

///|
/// Create saturate color matrix
pub fn saturate_matrix(factor : Double) -> FixedArray[Double] {
  let s = factor
  let lum_r = 0.2126
  let lum_g = 0.7152
  let lum_b = 0.0722
  [
    (1.0 - s) * lum_r + s,
    (1.0 - s) * lum_g,
    (1.0 - s) * lum_b,
    0.0,
    0.0,
    (1.0 - s) * lum_r,
    (1.0 - s) * lum_g + s,
    (1.0 - s) * lum_b,
    0.0,
    0.0,
    (1.0 - s) * lum_r,
    (1.0 - s) * lum_g,
    (1.0 - s) * lum_b + s,
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
    1.0,
    0.0,
  ]
}

///|
/// Create hue-rotate color matrix
pub fn hue_rotate_matrix(angle_degrees : Double) -> FixedArray[Double] {
  let angle = angle_degrees * 3.14159265358979 / 180.0
  let cos_a = cos_approx(angle)
  let sin_a = sin_approx(angle)
  [
    0.213 + cos_a * 0.787 - sin_a * 0.213,
    0.715 - cos_a * 0.715 - sin_a * 0.715,
    0.072 - cos_a * 0.072 + sin_a * 0.928,
    0.0,
    0.0,
    0.213 - cos_a * 0.213 + sin_a * 0.143,
    0.715 + cos_a * 0.285 + sin_a * 0.140,
    0.072 - cos_a * 0.072 - sin_a * 0.283,
    0.0,
    0.0,
    0.213 - cos_a * 0.213 - sin_a * 0.787,
    0.715 - cos_a * 0.715 + sin_a * 0.715,
    0.072 + cos_a * 0.928 + sin_a * 0.072,
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
    1.0,
    0.0,
  ]
}

///|
/// Create luminance-to-alpha color matrix
pub fn luminance_to_alpha_matrix() -> FixedArray[Double] {
  [
    0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2126,
    0.7152, 0.0722, 0.0, 0.0,
  ]
}

///|
/// Apply drop shadow to an image and return new image with shadow
pub fn apply_drop_shadow(
  image : Image,
  offset_x : Int,
  offset_y : Int,
  blur_radius : Int,
  shadow_color : Color,
) -> Image {
  // Create output with extra space for shadow
  let max_offset = if offset_x.abs() > offset_y.abs() {
    offset_x.abs()
  } else {
    offset_y.abs()
  }
  let margin = blur_radius + max_offset
  let new_width = image.width + margin * 2
  let new_height = image.height + margin * 2
  let output = Image::new(new_width, new_height)
  // First, render the shadow (offset copy with color)
  for y in 0.. 0 {
        let shadow_x = margin + x + offset_x
        let shadow_y = margin + y + offset_y
        if shadow_x >= 0 &&
          shadow_x < new_width &&
          shadow_y >= 0 &&
          shadow_y < new_height {
          // Use original alpha to modulate shadow
          let alpha = (shadow_color.a.to_double() *
          src_color.a.to_double() /
          255.0).to_int()
          output.set_pixel(shadow_x, shadow_y, { ..shadow_color, a: alpha })
        }
      }
    }
  }
  // Apply blur to shadow
  if blur_radius > 0 {
    output.apply_blur_in_place(blur_radius)
  }
  // Then overlay the original image on top
  for y in 0.. 0 {
        let dst_x = margin + x
        let dst_y = margin + y
        // Alpha blend
        let bg = output.get_pixel(dst_x, dst_y)
        let blended = alpha_blend(src_color, bg)
        output.set_pixel(dst_x, dst_y, blended)
      }
    }
  }
  output
}

///|
/// Alpha blend foreground over background
fn alpha_blend(fg : Color, bg : Color) -> Color {
  let fg_a = fg.a.to_double() / 255.0
  let bg_a = bg.a.to_double() / 255.0
  let out_a = fg_a + bg_a * (1.0 - fg_a)
  if out_a < 0.001 {
    return Color::transparent()
  }
  let r = (fg.r.to_double() * fg_a + bg.r.to_double() * bg_a * (1.0 - fg_a)) /
    out_a
  let g = (fg.g.to_double() * fg_a + bg.g.to_double() * bg_a * (1.0 - fg_a)) /
    out_a
  let b = (fg.b.to_double() * fg_a + bg.b.to_double() * bg_a * (1.0 - fg_a)) /
    out_a
  Color::rgba(r.to_int(), g.to_int(), b.to_int(), (out_a * 255.0).to_int())
}

///|
/// Apply a filter to an image (returns new image)
pub fn apply_filter(image : Image, filter : Filter) -> Image {
  match filter {
    Blur(radius) => {
      let result = image.clone()
      result.apply_blur_in_place(radius.to_int())
      result
    }
    DropShadow(offset_x, offset_y, blur, color) =>
      apply_drop_shadow(
        image,
        offset_x.to_int(),
        offset_y.to_int(),
        blur.to_int(),
        color,
      )
    Brightness(factor) => {
      let result = image.clone()
      result.apply_brightness_in_place(factor)
      result
    }
    Contrast(factor) => {
      let result = image.clone()
      result.apply_contrast_in_place(factor)
      result
    }
    Grayscale(amount) => {
      let result = image.clone()
      result.apply_grayscale_in_place(amount)
      result
    }
    Sepia(amount) => {
      let result = image.clone()
      result.apply_sepia_in_place(amount)
      result
    }
    HueRotate(angle) => {
      let result = image.clone()
      result.apply_hue_rotate_in_place(angle)
      result
    }
    Invert(amount) => {
      let result = image.clone()
      result.apply_invert_in_place(amount)
      result
    }
    Saturate(factor) => {
      let result = image.clone()
      result.apply_saturate_in_place(factor)
      result
    }
    ColorMatrix(matrix) => {
      let result = image.clone()
      result.apply_color_matrix_in_place(matrix)
      result
    }
  }
}

// ============================================================================
// Image Support
// ============================================================================

///|
/// Image data (RGBA pixels)
pub(all) struct Image {
  width : Int
  height : Int
  pixels : Array[Color] // Row-major order
}

///|
/// Create a new image with specified dimensions
pub fn Image::new(width : Int, height : Int) -> Image {
  let total = width * height
  let pixels = Array::make(total, Color::transparent())
  { width, height, pixels }
}

///|
/// Create an image filled with a color
pub fn Image::filled(width : Int, height : Int, color : Color) -> Image {
  let total = width * height
  let pixels = Array::make(total, color)
  { width, height, pixels }
}

///|
/// Get pixel at coordinates
pub fn Image::get_pixel(self : Image, x : Int, y : Int) -> Color {
  if x < 0 || x >= self.width || y < 0 || y >= self.height {
    return Color::transparent()
  }
  self.pixels[y * self.width + x]
}

///|
/// Set pixel at coordinates
pub fn Image::set_pixel(self : Image, x : Int, y : Int, color : Color) -> Unit {
  if x >= 0 && x < self.width && y >= 0 && y < self.height {
    self.pixels[y * self.width + x] = color
  }
}

///|
/// Set pixel without bounds checking (caller must ensure valid coordinates)
pub fn Image::set_pixel_unchecked(
  self : Image,
  x : Int,
  y : Int,
  color : Color,
) -> Unit {
  self.pixels[y * self.width + x] = color
}

///|
/// Fill a horizontal line (optimized for scanline rendering)
/// x1 and x2 are inclusive, caller should ensure y is valid
pub fn Image::fill_horizontal_line(
  self : Image,
  x1 : Int,
  x2 : Int,
  y : Int,
  color : Color,
) -> Unit {
  if y < 0 || y >= self.height {
    return
  }
  let start_x = if x1 < 0 { 0 } else { x1 }
  let end_x = if x2 >= self.width { self.width - 1 } else { x2 }
  if start_x > end_x {
    return
  }
  let base = y * self.width
  for x in start_x..<(end_x + 1) {
    self.pixels[base + x] = color
  }
}

///|
/// Fill a rectangle region
pub fn Image::fill_rect(
  self : Image,
  x : Int,
  y : Int,
  w : Int,
  h : Int,
  color : Color,
) -> Unit {
  if w <= 0 || h <= 0 {
    return
  }
  let x2 = x + w - 1
  for py in y..<(y + h) {
    self.fill_horizontal_line(x, x2, py, color)
  }
}

///|
/// Clear image to transparent
pub fn Image::clear(self : Image) -> Unit {
  let transparent = Color::transparent()
  for i in 0.. Image {
  let pixels : Array[Color] = []
  for i in 0.. Unit {
  if radius <= 0 {
    return
  }
  // Convert to 2D array for processing
  let rows : Array[Array[Color]] = []
  for y in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Unit {
  for i in 0.. Image {
  apply_filter(self, filter)
}

///|
/// Sprite definition (sub-region of an image)
pub(all) struct Sprite {
  image : Image
  x : Int // Source x
  y : Int // Source y
  width : Int
  height : Int
}

///|
/// Create a sprite from an image region
pub fn Sprite::new(
  image : Image,
  x : Int,
  y : Int,
  width : Int,
  height : Int,
) -> Sprite {
  { image, x, y, width, height }
}

///|
/// Create a sprite from the entire image
pub fn Sprite::from_image(image : Image) -> Sprite {
  { image, x: 0, y: 0, width: image.width, height: image.height }
}

///|
/// Get pixel from sprite (local coordinates)
pub fn Sprite::get_pixel(self : Sprite, x : Int, y : Int) -> Color {
  if x < 0 || x >= self.width || y < 0 || y >= self.height {
    return Color::transparent()
  }
  self.image.get_pixel(self.x + x, self.y + y)
}

///|
/// Sprite sheet for animations
pub(all) struct SpriteSheet {
  image : Image
  tile_width : Int
  tile_height : Int
  columns : Int
  rows : Int
}

///|
/// Create a sprite sheet from an image
pub fn SpriteSheet::new(
  image : Image,
  tile_width : Int,
  tile_height : Int,
) -> SpriteSheet {
  let columns = image.width / tile_width
  let rows = image.height / tile_height
  { image, tile_width, tile_height, columns, rows }
}

///|
/// Get sprite at grid position
pub fn SpriteSheet::get_sprite(
  self : SpriteSheet,
  col : Int,
  row : Int,
) -> Sprite {
  Sprite::new(
    self.image,
    col * self.tile_width,
    row * self.tile_height,
    self.tile_width,
    self.tile_height,
  )
}

///|
/// Get sprite by index (left-to-right, top-to-bottom)
pub fn SpriteSheet::get_sprite_by_index(
  self : SpriteSheet,
  index : Int,
) -> Sprite {
  let col = index % self.columns
  let row = index / self.columns
  self.get_sprite(col, row)
}

///|
/// Total number of sprites in the sheet
pub fn SpriteSheet::sprite_count(self : SpriteSheet) -> Int {
  self.columns * self.rows
}

///|
/// Blit (copy) source image onto destination at position
pub fn blit(dest : Image, src : Image, dest_x : Int, dest_y : Int) -> Unit {
  for sy in 0.. 0 {
        let dx = dest_x + sx
        let dy = dest_y + sy
        if color.a == 255 {
          dest.set_pixel(dx, dy, color)
        } else {
          let dst_color = dest.get_pixel(dx, dy)
          let blended = blend_colors(dst_color, color)
          dest.set_pixel(dx, dy, blended)
        }
      }
    }
  }
}

///|
/// Blit sprite onto image
pub fn blit_sprite(
  dest : Image,
  sprite : Sprite,
  dest_x : Int,
  dest_y : Int,
) -> Unit {
  for sy in 0.. 0 {
        let dx = dest_x + sx
        let dy = dest_y + sy
        if color.a == 255 {
          dest.set_pixel(dx, dy, color)
        } else {
          let dst_color = dest.get_pixel(dx, dy)
          let blended = blend_colors(dst_color, color)
          dest.set_pixel(dx, dy, blended)
        }
      }
    }
  }
}

///|
/// Blit with scaling
pub fn blit_scaled(
  dest : Image,
  src : Image,
  dest_x : Int,
  dest_y : Int,
  dest_w : Int,
  dest_h : Int,
) -> Unit {
  for dy in 0.. 0 {
        dest.set_pixel(dest_x + dx, dest_y + dy, color)
      }
    }
  }
}

///|
/// Flip image horizontally
pub fn Image::flip_horizontal(self : Image) -> Image {
  let result = Image::new(self.width, self.height)
  for y in 0.. Image {
  let result = Image::new(self.width, self.height)
  for y in 0.. Image {
  let result = Image::new(self.height, self.width)
  for y in 0.. Image {
  let result = Image::new(self.height, self.width)
  for y in 0.. Image {
  let result = Image::new(w, h)
  for py in 0.. Color {
  let sa = src.a.to_double() / 255.0
  let da = dst.a.to_double() / 255.0
  let out_a = sa + da * (1.0 - sa)
  if out_a == 0.0 {
    return Color::transparent()
  }
  let r = ((src.r.to_double() * sa + dst.r.to_double() * da * (1.0 - sa)) /
  out_a).to_int()
  let g = ((src.g.to_double() * sa + dst.g.to_double() * da * (1.0 - sa)) /
  out_a).to_int()
  let b = ((src.b.to_double() * sa + dst.b.to_double() * da * (1.0 - sa)) /
  out_a).to_int()
  { r, g, b, a: (out_a * 255.0).to_int() }
}

///|
/// Animated sprite helper
pub(all) struct AnimatedSprite {
  sheet : SpriteSheet
  frames : Array[Int] // Frame indices
  mut current_frame : Int
  frame_duration : Double // Seconds per frame
  mut elapsed : Double
  mut looping : Bool
  mut playing : Bool
}

///|
/// Create an animated sprite
pub fn AnimatedSprite::new(
  sheet : SpriteSheet,
  frames : Array[Int],
  frame_duration : Double,
) -> AnimatedSprite {
  {
    sheet,
    frames,
    current_frame: 0,
    frame_duration,
    elapsed: 0.0,
    looping: true,
    playing: true,
  }
}

///|
/// Create animation for a range of frames
pub fn AnimatedSprite::from_range(
  sheet : SpriteSheet,
  start : Int,
  end : Int,
  frame_duration : Double,
) -> AnimatedSprite {
  let frames : Array[Int] = []
  for i in start..<=end {
    frames.push(i)
  }
  AnimatedSprite::new(sheet, frames, frame_duration)
}

///|
/// Update animation with delta time
pub fn AnimatedSprite::update(self : AnimatedSprite, dt : Double) -> Unit {
  if !self.playing || self.frames.is_empty() {
    return
  }
  self.elapsed = self.elapsed + dt
  while self.elapsed >= self.frame_duration {
    self.elapsed = self.elapsed - self.frame_duration
    self.current_frame = self.current_frame + 1
    if self.current_frame >= self.frames.length() {
      if self.looping {
        self.current_frame = 0
      } else {
        self.current_frame = self.frames.length() - 1
        self.playing = false
      }
    }
  }
}

///|
/// Get current sprite
pub fn AnimatedSprite::get_current_sprite(self : AnimatedSprite) -> Sprite {
  if self.frames.is_empty() {
    return Sprite::new(self.sheet.image, 0, 0, 0, 0)
  }
  let frame_index = self.frames[self.current_frame]
  self.sheet.get_sprite_by_index(frame_index)
}

///|
/// Play animation from beginning
pub fn AnimatedSprite::play(self : AnimatedSprite) -> Unit {
  self.playing = true
  self.current_frame = 0
  self.elapsed = 0.0
}

///|
/// Stop animation
pub fn AnimatedSprite::stop(self : AnimatedSprite) -> Unit {
  self.playing = false
}

///|
/// Pause animation
pub fn AnimatedSprite::pause(self : AnimatedSprite) -> Unit {
  self.playing = false
}

///|
/// Continue animation
pub fn AnimatedSprite::continue_playing(self : AnimatedSprite) -> Unit {
  self.playing = true
}

///|
/// Set looping
pub fn AnimatedSprite::set_looping(
  self : AnimatedSprite,
  looping : Bool,
) -> Unit {
  self.looping = looping
}

// ============================================================================
// ClipPath
// ============================================================================

///|
/// Clip path definition
pub(all) struct ClipPath {
  id : String
  shape : Shape // The clipping shape
  transform : Transform
  clip_rule : FillRule // nonzero or evenodd
  units : ClipPathUnits
}

///|
/// Clip path units for coordinate system
pub(all) enum ClipPathUnits {
  UserSpaceOnUse
  ObjectBoundingBox
} derive(Debug, Eq)

///|
pub fn ClipPath::new(id : String, shape : Shape) -> ClipPath {
  {
    id,
    shape,
    transform: Transform::identity(),
    clip_rule: NonZero,
    units: UserSpaceOnUse,
  }
}

///|
pub fn ClipPath::with_transform(
  id : String,
  shape : Shape,
  transform : Transform,
) -> ClipPath {
  { id, shape, transform, clip_rule: NonZero, units: UserSpaceOnUse }
}

///|
/// Check if a point is inside the clip path
pub fn ClipPath::contains(self : ClipPath, x : Double, y : Double) -> Bool {
  // Apply inverse transform to get point in clip space
  let inv = self.transform.inverse()
  let (cx, cy) = inv.apply(x, y)
  // Use hit test
  hit_test_shape(cx, cy, self.shape)
}

///|
/// Clip path registry for referencing by ID
pub(all) struct ClipPathRegistry {
  clips : Map[String, ClipPath]
}

///|
pub fn ClipPathRegistry::new() -> ClipPathRegistry {
  { clips: {} }
}

///|
pub fn ClipPathRegistry::add(self : ClipPathRegistry, clip : ClipPath) -> Unit {
  self.clips.set(clip.id, clip)
}

///|
pub fn ClipPathRegistry::get(self : ClipPathRegistry, id : String) -> ClipPath? {
  self.clips.get(id)
}

// ============================================================================
// Mask
// ============================================================================

///|
/// Mask content units
pub(all) enum MaskUnits {
  UserSpaceOnUse // Coordinates relative to current user space
  ObjectBoundingBox // Coordinates relative to bounding box (0-1)
} derive(Debug, Eq)

///|
/// Mask type - how mask values are interpreted
pub(all) enum MaskType {
  Luminance // Use luminance (brightness) as mask value
  Alpha // Use alpha channel as mask value
} derive(Debug, Eq)

///|
/// Mask definition for transparency masking
pub(all) struct Mask {
  id : String
  content : Array[SVGNode] // Mask content (rendered to get mask values)
  x : Double // Mask region x
  y : Double // Mask region y
  width : Double // Mask region width
  height : Double // Mask region height
  x_is_percent : Bool
  y_is_percent : Bool
  width_is_percent : Bool
  height_is_percent : Bool
  mask_units : MaskUnits
  mask_content_units : MaskUnits
  mask_type : MaskType
}

///|
pub fn Mask::new(id : String, content : Array[SVGNode]) -> Mask {
  {
    id,
    content,
    x: -0.1,
    y: -0.1,
    width: 1.2,
    height: 1.2,
    x_is_percent: true,
    y_is_percent: true,
    width_is_percent: true,
    height_is_percent: true,
    mask_units: MaskUnits::ObjectBoundingBox,
    mask_content_units: MaskUnits::UserSpaceOnUse,
    mask_type: Luminance,
  }
}

///|
pub fn Mask::with_bounds(
  id : String,
  content : Array[SVGNode],
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> Mask {
  {
    id,
    content,
    x,
    y,
    width,
    height,
    x_is_percent: false,
    y_is_percent: false,
    width_is_percent: false,
    height_is_percent: false,
    mask_units: MaskUnits::ObjectBoundingBox,
    mask_content_units: MaskUnits::UserSpaceOnUse,
    mask_type: Luminance,
  }
}

///|
/// Compute mask value (0.0-1.0) at a point using luminance
pub fn compute_luminance(color : Color) -> Double {
  // Standard luminance formula (ITU-R BT.709)
  let r = color.r.to_double() / 255.0
  let g = color.g.to_double() / 255.0
  let b = color.b.to_double() / 255.0
  let a = color.a.to_double() / 255.0
  // Luminance weighted by alpha
  (0.2126 * r + 0.7152 * g + 0.0722 * b) * a
}

///|
/// Compute mask value using alpha channel
pub fn compute_alpha_mask(color : Color) -> Double {
  color.a.to_double() / 255.0
}

///|
/// Mask registry for referencing by ID
pub(all) struct MaskRegistry {
  masks : Map[String, Mask]
}

///|
pub fn MaskRegistry::new() -> MaskRegistry {
  { masks: {} }
}

///|
pub fn MaskRegistry::add(self : MaskRegistry, mask : Mask) -> Unit {
  self.masks.set(mask.id, mask)
}

///|
pub fn MaskRegistry::get(self : MaskRegistry, id : String) -> Mask? {
  self.masks.get(id)
}

///|
fn resolve_mask_coord(
  value : Double,
  is_percent : Bool,
  min : Double,
  size : Double,
) -> Double {
  if is_percent {
    min + value * size
  } else {
    value
  }
}

///|
fn resolve_mask_size(
  value : Double,
  is_percent : Bool,
  size : Double,
) -> Double {
  if is_percent {
    value * size
  } else {
    value
  }
}

///|
/// Parsed SVG document with reusable resources
pub(all) struct SVGDocument {
  root : SVGNode
  symbols : SymbolRegistry
  clips : ClipPathRegistry
  masks : MaskRegistry
  patterns : PatternRegistry
  gradients : GradientRegistry
  markers : MarkerRegistry
}

///|
pub fn SVGDocument::new(root : SVGNode) -> SVGDocument {
  {
    root,
    symbols: SymbolRegistry::new(),
    clips: ClipPathRegistry::new(),
    masks: MaskRegistry::new(),
    patterns: PatternRegistry::new(),
    gradients: GradientRegistry::new(),
    markers: MarkerRegistry::new(),
  }
}

///|
/// Get the mask region bounds for a given target bounds
pub fn Mask::get_mask_bounds(self : Mask, target : BoundingBox) -> BoundingBox {
  match self.mask_units {
    MaskUnits::ObjectBoundingBox => {
      let tw = target.max_x - target.min_x
      let th = target.max_y - target.min_y
      {
        min_x: target.min_x + self.x * tw,
        min_y: target.min_y + self.y * th,
        max_x: target.min_x + (self.x + self.width) * tw,
        max_y: target.min_y + (self.y + self.height) * th,
      }
    }
    MaskUnits::UserSpaceOnUse => {
      let tw = target.max_x - target.min_x
      let th = target.max_y - target.min_y
      let min_x = resolve_mask_coord(
        self.x,
        self.x_is_percent,
        target.min_x,
        tw,
      )
      let min_y = resolve_mask_coord(
        self.y,
        self.y_is_percent,
        target.min_y,
        th,
      )
      let w = resolve_mask_size(self.width, self.width_is_percent, tw)
      let h = resolve_mask_size(self.height, self.height_is_percent, th)
      { min_x, min_y, max_x: min_x + w, max_y: min_y + h }
    }
  }
}

///|
/// Apply mask to an image using luminance or alpha
pub fn apply_mask_to_image(
  image : Image,
  mask_buffer : Image,
  mask_type : MaskType,
) -> Image {
  let result = Image::new(image.width, image.height)
  for y in 0..= 0 &&
        mask_x < mask_buffer.width &&
        mask_y >= 0 &&
        mask_y < mask_buffer.height {
        mask_buffer.get_pixel(mask_x, mask_y)
      } else {
        Color::transparent()
      }
      // Compute mask value
      let mask_value = match mask_type {
        Luminance => compute_luminance(mask_color)
        Alpha => compute_alpha_mask(mask_color)
      }
      // Apply mask to alpha
      let new_alpha = (src_color.a.to_double() * mask_value).to_int()
      result.set_pixel(
        x,
        y,
        Color::rgba(src_color.r, src_color.g, src_color.b, new_alpha),
      )
    }
  }
  result
}

// ============================================================================
// Pattern Fill
// ============================================================================

///|
/// Pattern definition for fills
pub(all) struct Pattern {
  id : String
  width : Double // Pattern tile width
  height : Double // Pattern tile height
  content : Array[SVGNode] // Pattern content
  pattern_units : PatternUnits
  pattern_content_units : PatternUnits
  transform : Transform
  view_box : ViewBox?
  preserve_aspect_ratio : PreserveAspectRatio
}

///|
pub(all) enum PatternUnits {
  UserSpaceOnUse // Coordinates relative to current user space
  ObjectBoundingBox // Coordinates relative to bounding box (0-1)
}

///|
pub fn Pattern::new(
  id : String,
  width : Double,
  height : Double,
  content : Array[SVGNode],
) -> Pattern {
  {
    id,
    width,
    height,
    content,
    pattern_units: ObjectBoundingBox,
    pattern_content_units: UserSpaceOnUse,
    transform: Transform::identity(),
    view_box: None,
    preserve_aspect_ratio: PreserveAspectRatio::default(),
  }
}

///|
/// Get pattern color at a point (simplified - returns first solid color found)
pub fn Pattern::get_color_at(
  self : Pattern,
  x : Double,
  y : Double,
  bbox : BoundingBox,
) -> Color? {
  // Calculate pattern space coordinates
  let (pw, ph) = match self.pattern_units {
    UserSpaceOnUse => (self.width, self.height)
    ObjectBoundingBox =>
      (self.width * bbox.width(), self.height * bbox.height())
  }
  if pw <= 0.0 || ph <= 0.0 {
    return None
  }
  // Get position within pattern tile (modulo)
  let px = x - (x / pw).floor() * pw
  let py = y - (y / ph).floor() * ph
  let (cx, cy) = match self.pattern_content_units {
    UserSpaceOnUse => (px, py)
    ObjectBoundingBox => {
      let bw = bbox.width()
      let bh = bbox.height()
      if bw <= 0.0 || bh <= 0.0 {
        return None
      }
      (px / bw, py / bh)
    }
  }
  let (cx, cy) = match self.view_box {
    Some(view_box) => {
      let t = view_box.get_transform(pw, ph, self.preserve_aspect_ratio)
      if !t.is_invertible() {
        return None
      }
      t.inverse().apply(cx, cy)
    }
    None => (cx, cy)
  }
  // Find which content element contains this point
  for node in self.content {
    if hit_test_shape(cx, cy, node.shape) {
      match node.fill {
        SolidColor(color) => return Some(color)
        _ => continue
      }
    }
  }
  None
}

///|
/// Pattern registry
pub(all) struct PatternRegistry {
  patterns : Map[String, Pattern]
}

///|
pub fn PatternRegistry::new() -> PatternRegistry {
  { patterns: {} }
}

///|
pub fn PatternRegistry::add(self : PatternRegistry, pattern : Pattern) -> Unit {
  self.patterns.set(pattern.id, pattern)
}

///|
pub fn PatternRegistry::get(self : PatternRegistry, id : String) -> Pattern? {
  self.patterns.get(id)
}

///|
/// Gradient definitions
pub(all) enum Gradient {
  Linear(LinearGradient)
  Radial(RadialGradient)
}

///|
/// Gradient registry
pub(all) struct GradientRegistry {
  gradients : Map[String, Gradient]
}

///|
pub fn GradientRegistry::new() -> GradientRegistry {
  { gradients: {} }
}

///|
pub fn GradientRegistry::add(
  self : GradientRegistry,
  id : String,
  gradient : Gradient,
) -> Unit {
  self.gradients.set(id, gradient)
}

///|
pub fn GradientRegistry::get(self : GradientRegistry, id : String) -> Gradient? {
  self.gradients.get(id)
}

// ============================================================================
// Text Layout (SVG 2.0)
// ============================================================================

///|
/// Text anchor (horizontal alignment)
pub(all) enum TextAnchor {
  Start // Left aligned (default for LTR)
  Middle // Center aligned
  End // Right aligned
}

///|
/// Writing mode (SVG 2.0)
pub(all) enum WritingMode {
  HorizontalTB // Left to right, top to bottom (default)
  VerticalRL // Top to bottom, right to left (Japanese, Chinese)
  VerticalLR // Top to bottom, left to right
} derive(Debug, Eq)

///|
/// Text orientation for vertical writing (SVG 2.0)
pub(all) enum TextOrientation {
  Mixed // Upright for CJK, rotated for others (default)
  Upright // All characters upright
  Sideways // All characters rotated 90°
} derive(Debug, Eq)

///|
/// White space handling (SVG 2.0)
pub(all) enum WhiteSpace {
  Normal // Collapse whitespace, wrap at boundaries
  Pre // Preserve whitespace, no wrapping
  NoWrap // Collapse whitespace, no wrapping
  PreWrap // Preserve whitespace, wrap at boundaries
  PreLine // Collapse spaces, preserve newlines, wrap
  BreakSpaces // Like pre-wrap but break at any space
} derive(Debug, Eq)

///|
/// Text overflow handling (SVG 2.0)
pub(all) enum TextOverflow {
  Clip // Clip at boundary
  Ellipsis // Show "..." at overflow
  Custom(String) // Custom overflow indicator
} derive(Debug, Eq)

///|
/// Text decoration style (SVG 2.0)
pub(all) enum TextDecorationStyle {
  Solid
  Double
  Dotted
  Dashed
  Wavy
} derive(Debug, Eq)

///|
/// Extended text decoration (SVG 2.0)
pub(all) struct TextDecorationFull {
  line : TextDecoration // underline, overline, line-through
  style : TextDecorationStyle
  color : Color? // None means use current color
  thickness : Double? // None means auto
}

///|
pub fn TextDecorationFull::default() -> TextDecorationFull {
  { line: NoDecoration, style: Solid, color: None, thickness: None }
}

///|
/// Paint order items (SVG 2.0)
pub(all) enum PaintOrderItem {
  Fill
  Stroke
  Markers
} derive(Debug, Eq)

///|
/// Paint order specification (SVG 2.0)
pub(all) struct PaintOrder {
  order : Array[PaintOrderItem]
} derive(Debug, Eq)

///|
pub fn PaintOrder::default() -> PaintOrder {
  { order: [Fill, Stroke, Markers] }
}

///|
/// Dominant baseline (vertical alignment)
pub(all) enum DominantBaseline {
  Auto // Default baseline
  TextTop // Top of em box
  Hanging // Hanging baseline
  Middle // Middle of em box
  Central // Central baseline
  TextBottom // Bottom of em box
  Alphabetic // Alphabetic baseline (default)
  Ideographic // Ideographic baseline
}

///|
/// Text decoration
pub(all) enum TextDecoration {
  NoDecoration
  Underline
  Overline
  LineThrough
} derive(Debug, Eq)

///|
/// Font weight
pub(all) enum FontWeight {
  Normal // 400
  Bold // 700
  Lighter
  Bolder
  Weight(Int) // 100-900
}

///|
/// Font style
pub(all) enum FontStyle {
  NormalStyle
  Italic
  Oblique
}

///|
/// Complete text style (SVG 2.0 extended)
pub(all) struct TextStyle {
  font_family : String
  font_size : Double
  font_weight : FontWeight
  font_style : FontStyle
  text_anchor : TextAnchor
  dominant_baseline : DominantBaseline
  text_decoration : TextDecoration
  letter_spacing : Double
  word_spacing : Double
  line_height : Double // Multiplier of font_size
  // SVG 2.0 additions
  writing_mode : WritingMode
  text_orientation : TextOrientation
  white_space : WhiteSpace
  paint_order : PaintOrder
}

///|
pub fn TextStyle::default() -> TextStyle {
  {
    font_family: "sans-serif",
    font_size: 16.0,
    font_weight: Normal,
    font_style: NormalStyle,
    text_anchor: Start,
    dominant_baseline: Auto,
    text_decoration: NoDecoration,
    letter_spacing: 0.0,
    word_spacing: 0.0,
    line_height: 1.2,
    writing_mode: HorizontalTB,
    text_orientation: Mixed,
    white_space: Normal,
    paint_order: PaintOrder::default(),
  }
}

///|
/// Text span (styled portion of text)
pub(all) struct TextSpan {
  text : String
  x : Double? // Absolute position (overrides flow)
  y : Double?
  dx : Double // Relative offset
  dy : Double
  style : TextStyle?
}

///|
pub fn TextSpan::new(text : String) -> TextSpan {
  { text, x: None, y: None, dx: 0.0, dy: 0.0, style: None }
}

///|
pub fn TextSpan::with_offset(
  text : String,
  dx : Double,
  dy : Double,
) -> TextSpan {
  { text, x: None, y: None, dx, dy, style: None }
}

///|
/// Multi-line text block (SVG 2.0 extended)
pub(all) struct TextBlock {
  spans : Array[TextSpan]
  x : Double
  y : Double
  style : TextStyle
  // SVG 2.0 additions
  inline_size : Double? // Max width before wrapping (None = no limit)
  text_overflow : TextOverflow
}

///|
pub fn TextBlock::new(x : Double, y : Double, text : String) -> TextBlock {
  {
    spans: [TextSpan::new(text)],
    x,
    y,
    style: TextStyle::default(),
    inline_size: None,
    text_overflow: Clip,
  }
}

///|
pub fn TextBlock::with_style(
  x : Double,
  y : Double,
  text : String,
  style : TextStyle,
) -> TextBlock {
  {
    spans: [TextSpan::new(text)],
    x,
    y,
    style,
    inline_size: None,
    text_overflow: Clip,
  }
}

///|
/// Create a text block with wrapping
pub fn TextBlock::with_wrap(
  x : Double,
  y : Double,
  text : String,
  inline_size : Double,
) -> TextBlock {
  {
    spans: [TextSpan::new(text)],
    x,
    y,
    style: TextStyle::default(),
    inline_size: Some(inline_size),
    text_overflow: Clip,
  }
}

///|
pub fn TextBlock::add_span(self : TextBlock, span : TextSpan) -> Unit {
  self.spans.push(span)
}

///|
/// Calculate text width (simplified - assumes monospace)
pub fn TextBlock::get_width(self : TextBlock) -> Double {
  let mut total = 0.0
  for span in self.spans {
    let style = match span.style {
      Some(s) => s
      None => self.style
    }
    let cw = style.font_size * 0.6
    total = total + span.text.length().to_double() * cw
    total = total + style.letter_spacing * (span.text.length() - 1).to_double()
  }
  total
}

///|
/// Get text height
pub fn TextBlock::get_height(self : TextBlock) -> Double {
  self.style.font_size * self.style.line_height
}

///|
/// Get adjusted x position based on text-anchor
pub fn TextBlock::get_anchor_x(self : TextBlock) -> Double {
  match self.style.text_anchor {
    Start => self.x
    Middle => self.x - self.get_width() / 2.0
    End => self.x - self.get_width()
  }
}

///|
/// Get adjusted y position based on dominant-baseline
pub fn TextBlock::get_baseline_y(self : TextBlock) -> Double {
  let fs = self.style.font_size
  match self.style.dominant_baseline {
    Auto | Alphabetic => self.y
    TextTop | Hanging => self.y + fs * 0.8
    Middle | Central => self.y + fs * 0.35
    TextBottom | Ideographic => self.y - fs * 0.2
  }
}

///|
/// Wrap text to fit within inline_size, returns lines
pub fn TextBlock::wrap_text(self : TextBlock) -> Array[String] {
  let result : Array[String] = []
  // Collect all text from spans
  let mut full_text = ""
  for span in self.spans {
    full_text = full_text + span.text
  }
  // If no inline_size, return as single line
  if self.inline_size is Some(max_width) {
    let char_width = self.style.font_size * 0.6
    // Handle white-space mode
    match self.style.white_space {
      Pre | NoWrap => {
        // No wrapping
        result.push(full_text)
        return result
      }
      _ => {
        // Wrap at word boundaries
        let words = split_words(full_text)
        let mut current_line = ""
        let mut current_width = 0.0
        for word in words {
          let word_width = word.length().to_double() * char_width +
            self.style.letter_spacing * (word.length() - 1).to_double()
          let space_width = char_width + self.style.word_spacing
          if current_line.length() == 0 {
            current_line = word
            current_width = word_width
          } else if current_width + space_width + word_width <= max_width {
            current_line = current_line + " " + word
            current_width = current_width + space_width + word_width
          } else {
            result.push(current_line)
            current_line = word
            current_width = word_width
          }
        }
        if current_line.length() > 0 {
          result.push(current_line)
        }
      }
    }
  } else {
    result.push(full_text)
    return result
  }
  result
}

///|
/// Get the number of lines when text is wrapped
pub fn TextBlock::get_line_count(self : TextBlock) -> Int {
  self.wrap_text().length()
}

///|
/// Get total height including all wrapped lines
pub fn TextBlock::get_total_height(self : TextBlock) -> Double {
  let line_count = self.get_line_count()
  self.style.font_size * self.style.line_height * line_count.to_double()
}

///|
/// Check if text is vertical (for writing-mode)
pub fn TextBlock::is_vertical(self : TextBlock) -> Bool {
  match self.style.writing_mode {
    HorizontalTB => false
    VerticalRL | VerticalLR => true
  }
}

///|
/// Split text into words (simplified)
fn split_words(text : String) -> Array[String] {
  let words : Array[String] = []
  let mut current = ""
  for c in text {
    if c == ' ' || c == '\t' || c == '\n' {
      if current.length() > 0 {
        words.push(current)
        current = ""
      }
    } else {
      current = current + c.to_string()
    }
  }
  if current.length() > 0 {
    words.push(current)
  }
  words
}

///|
/// Process white-space according to mode
pub fn process_white_space(text : String, mode : WhiteSpace) -> String {
  match mode {
    Pre | PreWrap | BreakSpaces =>
      // Preserve whitespace
      text
    PreLine => {
      // Collapse spaces but preserve newlines
      let mut result = ""
      let mut prev_space = false
      for c in text {
        if c == '\n' {
          result = result + "\n"
          prev_space = false
        } else if c == ' ' || c == '\t' {
          if !prev_space {
            result = result + " "
            prev_space = true
          }
        } else {
          result = result + c.to_string()
          prev_space = false
        }
      }
      result
    }
    Normal | NoWrap => {
      // Collapse all whitespace
      let mut result = ""
      let mut prev_space = false
      for c in text {
        if c == ' ' || c == '\t' || c == '\n' {
          if !prev_space {
            result = result + " "
            prev_space = true
          }
        } else {
          result = result + c.to_string()
          prev_space = false
        }
      }
      result
    }
  }
}

///|
/// Apply text-overflow to a line
pub fn apply_text_overflow(
  line : String,
  max_width : Double,
  char_width : Double,
  overflow : TextOverflow,
) -> String {
  let line_width = line.length().to_double() * char_width
  if line_width <= max_width {
    return line
  }
  let max_chars = (max_width / char_width).to_int()
  match overflow {
    Clip =>
      if max_chars > 0 && max_chars < line.length() {
        take_chars(line, max_chars)
      } else {
        line
      }
    Ellipsis =>
      if max_chars > 3 {
        take_chars(line, max_chars - 3) + "..."
      } else if max_chars > 0 {
        "..."
      } else {
        ""
      }
    Custom(indicator) => {
      let indicator_len = indicator.length()
      if max_chars > indicator_len {
        take_chars(line, max_chars - indicator_len) + indicator
      } else if max_chars > 0 {
        indicator
      } else {
        ""
      }
    }
  }
}

///|
/// Take first n characters from a string
fn take_chars(s : String, n : Int) -> String {
  let mut result = ""
  let mut count = 0
  for c in s {
    if count >= n {
      break
    }
    result = result + c.to_string()
    count = count + 1
  }
  result
}

// ============================================================================
// Use/Symbol (Reusable Elements)
// ============================================================================

///|
/// Symbol definition (reusable graphic)
pub(all) struct Symbol {
  id : String
  content : SVGNode
  view_box : ViewBox?
  width : Double?
  height : Double?
  preserve_aspect_ratio : PreserveAspectRatio
  display_none : Bool
}

///|
pub fn Symbol::new(id : String, content : SVGNode) -> Symbol {
  {
    id,
    content,
    view_box: None,
    width: None,
    height: None,
    preserve_aspect_ratio: PreserveAspectRatio::default(),
    display_none: false,
  }
}

///|
pub fn Symbol::with_viewbox(
  id : String,
  content : SVGNode,
  view_box : ViewBox,
) -> Symbol {
  {
    id,
    content,
    view_box: Some(view_box),
    width: None,
    height: None,
    preserve_aspect_ratio: PreserveAspectRatio::default(),
    display_none: false,
  }
}

///|
/// Use element (instance of a symbol)
pub(all) struct UseElement {
  href : String // Reference to symbol ID (e.g., "#mySymbol")
  x : Double
  y : Double
  width : Double?
  height : Double?
  transform : Transform
}

///|
pub fn UseElement::new(href : String, x : Double, y : Double) -> UseElement {
  { href, x, y, width: None, height: None, transform: Transform::identity() }
}

///|
pub fn UseElement::with_size(
  href : String,
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> UseElement {
  {
    href,
    x,
    y,
    width: Some(width),
    height: Some(height),
    transform: Transform::identity(),
  }
}

///|
fn hex_value(c : Char) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - '0'.to_int())
  } else if c >= 'a' && c <= 'f' {
    Some(10 + (c.to_int() - 'a'.to_int()))
  } else if c >= 'A' && c <= 'F' {
    Some(10 + (c.to_int() - 'A'.to_int()))
  } else {
    None
  }
}

///|
fn decode_percent(s : String) -> String {
  let mut i = 0
  let len = s.length()
  let buf = StringBuilder::new()
  while i < len {
    let c = Int::unsafe_to_char(s[i].to_int())
    if c == '%' && i + 2 < len {
      let c1 = Int::unsafe_to_char(s[i + 1].to_int())
      let c2 = Int::unsafe_to_char(s[i + 2].to_int())
      match (hex_value(c1), hex_value(c2)) {
        (Some(h1), Some(h2)) => {
          let v = h1 * 16 + h2
          buf.write_char(Int::unsafe_to_char(v))
          i = i + 3
          continue
        }
        _ => ()
      }
    }
    buf.write_char(c)
    i = i + 1
  }
  buf.to_string()
}

///|
/// Get the href ID (strips leading #)
pub fn UseElement::get_id(self : UseElement) -> String {
  if self.href.length() > 0 && self.href[0].unsafe_to_char() == '#' {
    let buf = StringBuilder::new()
    for i in 1.. SymbolRegistry {
  { symbols: {} }
}

///|
pub fn SymbolRegistry::add(self : SymbolRegistry, symbol : Symbol) -> Unit {
  self.symbols.set(symbol.id, symbol)
}

///|
pub fn SymbolRegistry::get(self : SymbolRegistry, id : String) -> Symbol? {
  self.symbols.get(id)
}

///|
/// Registry for reusable elements defined in 
pub(all) struct DefsRegistry {
  elements : Map[String, SVGNode]
}

///|
pub fn DefsRegistry::new() -> DefsRegistry {
  { elements: {} }
}

///|
pub fn DefsRegistry::add(
  self : DefsRegistry,
  id : String,
  node : SVGNode,
) -> Unit {
  if id.length() == 0 {
    return
  }
  self.elements.set(id, node)
}

///|
pub fn DefsRegistry::get(self : DefsRegistry, id : String) -> SVGNode? {
  self.elements.get(id)
}

///|
/// Instantiate a use element with the symbol registry
pub fn UseElement::instantiate(
  self : UseElement,
  registry : SymbolRegistry,
) -> SVGNode? {
  let id = self.get_id()
  match registry.get(id) {
    Some(symbol) => {
      if symbol.display_none {
        return None
      }
      // Create a copy of the symbol content
      let node = symbol.content.clone()
      let (vb, force_non_uniform) = match symbol.view_box {
        Some(vb) => (Some(vb), false)
        None =>
          match (symbol.width, symbol.height) {
            (Some(w), Some(h)) =>
              (
                Some(ViewBox::{ min_x: 0.0, min_y: 0.0, width: w, height: h }),
                true,
              )
            _ => (None, false)
          }
      }
      let vw = match self.width {
        Some(w) => Some(w)
        None => symbol.width
      }
      let vh = match self.height {
        Some(h) => Some(h)
        None => symbol.height
      }
      // Apply viewBox scaling (if viewport is known)
      node.view_box = vb
      if force_non_uniform {
        node.preserve_aspect_ratio = PreserveAspectRatio::{
          align: None,
          meet_or_slice: Meet,
        }
      }
      node.viewport_width = vw
      node.viewport_height = vh
      // Apply use element transform
      let translate = Transform::translate(self.x, self.y)
      let base = translate.multiply(self.transform)
      node.transform = base.multiply(node.transform)
      Some(node)
    }
    None => None
  }
}

///|
/// Clone an SVGNode (shallow clone of children)
pub fn SVGNode::clone(self : SVGNode) -> SVGNode {
  let children : Array[SVGNode] = []
  for child in self.children {
    children.push(child.clone())
  }
  let filters : Array[Filter] = []
  for f in self.filters {
    filters.push(f)
  }
  {
    id: self.id,
    shape: self.shape,
    transform: self.transform,
    view_box: self.view_box,
    viewport_width: self.viewport_width,
    viewport_height: self.viewport_height,
    preserve_aspect_ratio: self.preserve_aspect_ratio,
    preserve_aspect_ratio_is_set: self.preserve_aspect_ratio_is_set,
    fill: self.fill,
    fill_is_set: self.fill_is_set,
    color: self.color,
    color_is_set: self.color_is_set,
    paint_order: self.paint_order,
    fill_rule: self.fill_rule,
    fill_opacity: self.fill_opacity,
    stroke: self.stroke,
    stroke_paint_is_set: self.stroke_paint_is_set,
    stroke_width_is_set: self.stroke_width_is_set,
    stroke_opacity: self.stroke_opacity,
    opacity: self.opacity,
    marker_start: self.marker_start,
    marker_start_is_set: self.marker_start_is_set,
    marker_mid: self.marker_mid,
    marker_mid_is_set: self.marker_mid_is_set,
    marker_end: self.marker_end,
    marker_end_is_set: self.marker_end_is_set,
    z_index: self.z_index,
    node_dirty: self.node_dirty,
    prev_bounds: self.prev_bounds,
    filters,
    mask_id: self.mask_id,
    clip_path_id: self.clip_path_id,
    clip_overflow: self.clip_overflow,
    children,
  }
}

// ============================================================================
// Marker
// ============================================================================

///|
/// Marker definition (for line endpoints)
pub(all) struct Marker {
  id : String
  content : SVGNode
  ref_x : Double // Reference point x
  ref_y : Double // Reference point y
  marker_width : Double
  marker_height : Double
  orient : MarkerOrient
  marker_units : MarkerUnits
  view_box : ViewBox?
  preserve_aspect_ratio : PreserveAspectRatio
  clip_overflow : Bool
}

///|
pub(all) enum MarkerOrient {
  Auto // Orient along path direction
  AutoStartReverse // Reverse at start
  Angle(Double) // Fixed angle in degrees
}

///|
pub(all) enum MarkerUnits {
  StrokeWidth // Marker size relative to stroke width
  UserSpaceOnUse_ // Marker size in user space (renamed to avoid conflict)
}

///|
pub fn Marker::new(id : String, content : SVGNode) -> Marker {
  {
    id,
    content,
    ref_x: 0.0,
    ref_y: 0.0,
    marker_width: 3.0,
    marker_height: 3.0,
    orient: Angle(0.0),
    marker_units: StrokeWidth,
    view_box: None,
    preserve_aspect_ratio: PreserveAspectRatio::default(),
    clip_overflow: true,
  }
}

///|
pub fn Marker::arrow(id : String) -> Marker {
  // Create a simple arrow marker
  let arrow_path = Path(commands=[
    MoveTo(0.0, 0.0),
    LineTo(10.0, 5.0),
    LineTo(0.0, 10.0),
    ClosePath,
  ])
  let node = SVGNode::new(arrow_path)
  node.id = id + "_content"
  {
    id,
    content: node,
    ref_x: 10.0,
    ref_y: 5.0,
    marker_width: 10.0,
    marker_height: 10.0,
    orient: Auto,
    marker_units: StrokeWidth,
    view_box: Some({ min_x: 0.0, min_y: 0.0, width: 10.0, height: 10.0 }),
    preserve_aspect_ratio: PreserveAspectRatio::default(),
    clip_overflow: true,
  }
}

///|
pub fn Marker::dot(id : String, radius : Double) -> Marker {
  let circle = Circle(cx=radius, cy=radius, r=radius)
  let node = SVGNode::new(circle)
  node.id = id + "_content"
  {
    id,
    content: node,
    ref_x: radius,
    ref_y: radius,
    marker_width: radius * 2.0,
    marker_height: radius * 2.0,
    orient: Auto,
    marker_units: StrokeWidth,
    view_box: Some({
      min_x: 0.0,
      min_y: 0.0,
      width: radius * 2.0,
      height: radius * 2.0,
    }),
    preserve_aspect_ratio: PreserveAspectRatio::default(),
    clip_overflow: true,
  }
}

///|
/// Get transform for marker at a point with given angle
pub fn Marker::get_transform(
  self : Marker,
  x : Double,
  y : Double,
  angle : Double,
  stroke_width : Double,
) -> Transform {
  let scale = match self.marker_units {
    StrokeWidth => stroke_width
    UserSpaceOnUse_ => 1.0
  }
  let orient_angle = match self.orient {
    Auto => angle
    AutoStartReverse => angle + 3.14159265358979323846
    Angle(a) => degrees_to_radians(a)
  }
  let content_transform = match self.view_box {
    Some(vb) => {
      let view_t = vb.get_transform(
        self.marker_width,
        self.marker_height,
        self.preserve_aspect_ratio,
      )
      let (ref_px, ref_py) = view_t.apply(self.ref_x, self.ref_y)
      let ref_t = Transform::translate(-ref_px, -ref_py)
      ref_t.multiply(view_t)
    }
    None => Transform::translate(-self.ref_x, -self.ref_y)
  }
  let t1 = Transform::translate(x, y)
  let r = Transform::rotate(orient_angle)
  let s = Transform::scale(scale, scale)
  t1.multiply(r).multiply(s).multiply(content_transform)
}

///|
/// Marker registry
pub(all) struct MarkerRegistry {
  markers : Map[String, Marker]
}

///|
pub fn MarkerRegistry::new() -> MarkerRegistry {
  { markers: {} }
}

///|
pub fn MarkerRegistry::add(self : MarkerRegistry, marker : Marker) -> Unit {
  self.markers.set(marker.id, marker)
}

///|
pub fn MarkerRegistry::get(self : MarkerRegistry, id : String) -> Marker? {
  self.markers.get(id)
}

///|
/// Line with markers
pub(all) struct MarkedLine {
  points : Array[(Double, Double)]
  marker_start : String? // Marker ID
  marker_mid : String?
  marker_end : String?
}

///|
pub fn MarkedLine::new(points : Array[(Double, Double)]) -> MarkedLine {
  { points, marker_start: None, marker_mid: None, marker_end: None }
}

///|
pub fn MarkedLine::with_markers(
  points : Array[(Double, Double)],
  start : String?,
  mid : String?,
  end : String?,
) -> MarkedLine {
  { points, marker_start: start, marker_mid: mid, marker_end: end }
}

///|
/// Get angle at a point on the line
pub fn MarkedLine::get_angle_at(self : MarkedLine, index : Int) -> Double {
  let len = self.points.length()
  if len < 2 {
    return 0.0
  }
  let eps = 0.0001
  fn angle_from(dx : Double, dy : Double) -> Double {
    @math.atan2(dy, dx)
  }

  fn normalize(dx : Double, dy : Double) -> (Double, Double, Double) {
    let len = (dx * dx + dy * dy).sqrt()
    if len < eps {
      (0.0, 0.0, len)
    } else {
      (dx / len, dy / len, len)
    }
  }

  if index == 0 {
    // Start point - use first non-zero segment
    for i in 1..= eps {
        return angle_from(dx, dy)
      }
    }
    0.0
  } else if index >= len - 1 {
    // End point - use last non-zero segment
    for i = len - 1; i > 0; i = i - 1 {
      let dx = self.points[i].0 - self.points[i - 1].0
      let dy = self.points[i].1 - self.points[i - 1].1
      let (_, _, l) = normalize(dx, dy)
      if l >= eps {
        return angle_from(dx, dy)
      }
    }
    0.0
  } else {
    // Mid point - use bisector of incoming/outgoing vectors
    let dx1 = self.points[index].0 - self.points[index - 1].0
    let dy1 = self.points[index].1 - self.points[index - 1].1
    let dx2 = self.points[index + 1].0 - self.points[index].0
    let dy2 = self.points[index + 1].1 - self.points[index].1
    let (ux1, uy1, l1) = normalize(dx1, dy1)
    let (ux2, uy2, l2) = normalize(dx2, dy2)
    if l1 < eps && l2 < eps {
      return 0.0
    }
    if l1 < eps {
      return angle_from(dx2, dy2)
    }
    if l2 < eps {
      return angle_from(dx1, dy1)
    }
    let sx = ux1 + ux2
    let sy = uy1 + uy2
    if sx * sx + sy * sy < eps * eps {
      // 180-degree turn, fall back to outgoing direction
      angle_from(dx2, dy2)
    } else {
      angle_from(sx, sy)
    }
  }
}

///|
pub impl Show for Align with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for MeetOrSlice with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for LineCap with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for LineJoin with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for FillRule with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for MaskType with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for WritingMode with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for TextOrientation with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for WhiteSpace with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for TextOverflow with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for TextDecorationStyle with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for PaintOrderItem with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for TextDecoration with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}