///|
/// A single command in a `Path2D`. Matches the subset of Canvas2D commands
/// needed for fill and stroke.
pub enum PathCmd {
  MoveTo(Double, Double)
  LineTo(Double, Double)
  QuadTo(Double, Double, Double, Double)
  CubicTo(Double, Double, Double, Double, Double, Double)
  Close
} derive(Eq)

///|
pub impl Show for PathCmd with fn output(self, logger) {
  match self {
    MoveTo(x, y) => {
      logger.write_string("MoveTo(")
      logger.write_object(x)
      logger.write_string(", ")
      logger.write_object(y)
      logger.write_string(")")
    }
    LineTo(x, y) => {
      logger.write_string("LineTo(")
      logger.write_object(x)
      logger.write_string(", ")
      logger.write_object(y)
      logger.write_string(")")
    }
    QuadTo(cx, cy, x, y) => {
      logger.write_string("QuadTo(")
      logger.write_object(cx)
      logger.write_string(", ")
      logger.write_object(cy)
      logger.write_string(", ")
      logger.write_object(x)
      logger.write_string(", ")
      logger.write_object(y)
      logger.write_string(")")
    }
    CubicTo(x1, y1, x2, y2, x, y) => {
      logger.write_string("CubicTo(")
      logger.write_object(x1)
      logger.write_string(", ")
      logger.write_object(y1)
      logger.write_string(", ")
      logger.write_object(x2)
      logger.write_string(", ")
      logger.write_object(y2)
      logger.write_string(", ")
      logger.write_object(x)
      logger.write_string(", ")
      logger.write_object(y)
      logger.write_string(")")
    }
    Close => logger.write_string("Close")
  }
}

///|
/// A path — an ordered sequence of drawing commands.
/// Mirrors `Path2D` from the HTML Canvas2D API.
pub struct Path2D {
  commands : Array[PathCmd]
}

///|
pub fn Path2D::new() -> Path2D {
  { commands: [] }
}

///|
pub fn Path2D::move_to(self : Path2D, x : Double, y : Double) -> Unit {
  self.commands.push(PathCmd::MoveTo(x, y))
}

///|
pub fn Path2D::line_to(self : Path2D, x : Double, y : Double) -> Unit {
  self.commands.push(PathCmd::LineTo(x, y))
}

///|
pub fn Path2D::quadratic_curve_to(
  self : Path2D,
  cpx : Double,
  cpy : Double,
  x : Double,
  y : Double,
) -> Unit {
  self.commands.push(PathCmd::QuadTo(cpx, cpy, x, y))
}

///|
pub fn Path2D::bezier_curve_to(
  self : Path2D,
  cp1x : Double,
  cp1y : Double,
  cp2x : Double,
  cp2y : Double,
  x : Double,
  y : Double,
) -> Unit {
  self.commands.push(PathCmd::CubicTo(cp1x, cp1y, cp2x, cp2y, x, y))
}

///|
pub fn Path2D::rect(
  self : Path2D,
  x : Double,
  y : Double,
  w : Double,
  h : Double,
) -> Unit {
  self.commands.push(PathCmd::MoveTo(x, y))
  self.commands.push(PathCmd::LineTo(x + w, y))
  self.commands.push(PathCmd::LineTo(x + w, y + h))
  self.commands.push(PathCmd::LineTo(x, y + h))
  self.commands.push(PathCmd::Close)
}

///|
pub fn Path2D::close(self : Path2D) -> Unit {
  self.commands.push(PathCmd::Close)
}

///|
/// Append cubic Bezier segments approximating an elliptical arc.
/// Caller is responsible for the preceding `MoveTo`. No `MoveTo` is emitted.
///
/// - `rx, ry` — unsigned radii (caller normalizes negative values)
/// - `rotation` — rotation of the x-axis in radians
/// - `ccw` — if true, sweep counter-clockwise
fn emit_arc_cubics(
  path : Path2D,
  cx : Double,
  cy : Double,
  rx : Double,
  ry : Double,
  rotation : Double,
  start_angle : Double,
  end_angle : Double,
  ccw : Bool,
) -> Unit {
  let tau = 2.0 * @math.PI
  let half_pi = @math.PI / 2.0
  let a0 = start_angle
  let mut a1 = end_angle
  if ccw {
    while a1 > a0 {
      a1 = a1 - tau
    }
  } else {
    while a1 < a0 {
      a1 = a1 + tau
    }
  }
  let step = if ccw { -half_pi } else { half_pi }
  let cos_rot = @math.cos(rotation)
  let sin_rot = @math.sin(rotation)
  let mut a = a0
  while (!ccw && a < a1) || (ccw && a > a1) {
    let next = if ccw {
      if a + step < a1 {
        a1
      } else {
        a + step
      }
    } else if a + step > a1 {
      a1
    } else {
      a + step
    }
    let delta = next - a
    let k = 4.0 / 3.0 * @math.tan(delta / 4.0)
    let cos_a = @math.cos(a)
    let sin_a = @math.sin(a)
    let cos_n = @math.cos(next)
    let sin_n = @math.sin(next)
    let q1x = rx * (cos_a - k * sin_a)
    let q1y = ry * (sin_a + k * cos_a)
    let q2x = rx * (cos_n + k * sin_n)
    let q2y = ry * (sin_n - k * cos_n)
    let q3x = rx * cos_n
    let q3y = ry * sin_n
    let p1x = cx + q1x * cos_rot - q1y * sin_rot
    let p1y = cy + q1x * sin_rot + q1y * cos_rot
    let p2x = cx + q2x * cos_rot - q2y * sin_rot
    let p2y = cy + q2x * sin_rot + q2y * cos_rot
    let p3x = cx + q3x * cos_rot - q3y * sin_rot
    let p3y = cy + q3x * sin_rot + q3y * cos_rot
    path.commands.push(PathCmd::CubicTo(p1x, p1y, p2x, p2y, p3x, p3y))
    a = next
  }
}

///|
/// Return the current pen position — the endpoint of the last command.
/// For `Close`, scans backward to the matching `MoveTo` (DOM semantics).
/// Returns `None` for an empty path.
fn current_point(path : Path2D) -> (Double, Double)? {
  let len = path.commands.length()
  if len == 0 {
    return None
  }
  match path.commands[len - 1] {
    MoveTo(x, y) => Some((x, y))
    LineTo(x, y) => Some((x, y))
    QuadTo(_, _, x, y) => Some((x, y))
    CubicTo(_, _, _, _, x, y) => Some((x, y))
    Close => {
      // Scan backward for most recent MoveTo
      for i = len - 2; i >= 0; i = i - 1 {
        match path.commands[i] {
          MoveTo(x, y) => return Some((x, y))
          _ => continue
        }
      }
      None
    }
  }
}

///|
/// Append an elliptical arc. Mirrors HTML Canvas `ellipse()`.
/// Emits `MoveTo` to the arc start, then ≤90° cubic Bezier segments.
/// Silent normalization: negative `rx`/`ry` → absolute value.
pub fn Path2D::ellipse(
  self : Path2D,
  cx : Double,
  cy : Double,
  rx : Double,
  ry : Double,
  rotation : Double,
  start_angle : Double,
  end_angle : Double,
  counterclockwise? : Bool = false,
) -> Unit {
  let rx_abs = if rx < 0.0 { -rx } else { rx }
  let ry_abs = if ry < 0.0 { -ry } else { ry }
  let cos_rot = @math.cos(rotation)
  let sin_rot = @math.sin(rotation)
  let qx = rx_abs * @math.cos(start_angle)
  let qy = ry_abs * @math.sin(start_angle)
  let sx = cx + qx * cos_rot - qy * sin_rot
  let sy = cy + qx * sin_rot + qy * cos_rot
  self.commands.push(PathCmd::MoveTo(sx, sy))
  emit_arc_cubics(
    self, cx, cy, rx_abs, ry_abs, rotation, start_angle, end_angle, counterclockwise,
  )
}

///|
/// Append a tangent arc between two lines. Mirrors HTML Canvas `arcTo()`.
/// Silent normalization: negative `radius` → absolute value.
/// Degenerate cases (no current point, collinear, r=0) → `LineTo(x1, y1)`.
pub fn Path2D::arc_to(
  self : Path2D,
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  radius : Double,
) -> Unit {
  let r = if radius < 0.0 { -radius } else { radius }
  // 1. Current point
  let p0 = match current_point(self) {
    Some(pt) => pt
    None => {
      self.commands.push(PathCmd::MoveTo(x1, y1))
      return
    }
  }
  // 2. Vectors from p1 to p0, p1 to p2
  let v1x = p0.0 - x1
  let v1y = p0.1 - y1
  let v2x = x2 - x1
  let v2y = y2 - y1
  let len1 = (v1x * v1x + v1y * v1y).sqrt()
  let len2 = (v2x * v2x + v2y * v2y).sqrt()
  // 3. Degenerate checks
  if len1 == 0.0 || len2 == 0.0 || r == 0.0 {
    self.commands.push(PathCmd::LineTo(x1, y1))
    return
  }
  let cross = v1x * v2y - v1y * v2x
  if cross.abs() < 1.0e-12 * len1 * len2 {
    self.commands.push(PathCmd::LineTo(x1, y1))
    return
  }
  // 4. Normalize
  let n1x = v1x / len1
  let n1y = v1y / len1
  let n2x = v2x / len2
  let n2y = v2y / len2
  // 5. Half-angle geometry
  let cos_theta = n1x * n2x + n1y * n2y
  let sin_theta = (1.0 - cos_theta * cos_theta).sqrt()
  let d = r * (1.0 + cos_theta) / sin_theta
  // 6. Tangent points
  let t1x = x1 + d * n1x
  let t1y = y1 + d * n1y
  let t2x = x1 + d * n2x
  let t2y = y1 + d * n2y
  // 7. Arc center via perpendicular from t1
  let sign = if cross > 0.0 { 1.0 } else { -1.0 }
  let perp_x = -n1y
  let perp_y = n1x
  let cx = t1x + r * sign * perp_x
  let cy = t1y + r * sign * perp_y
  // 8. Start/end angles
  let a0 = @math.atan2(t1y - cy, t1x - cx)
  let a1 = @math.atan2(t2y - cy, t2x - cx)
  let ccw = cross > 0.0
  // 9. Emit
  self.commands.push(PathCmd::LineTo(t1x, t1y))
  emit_arc_cubics(self, cx, cy, r, r, 0.0, a0, a1, ccw)
}

///|
/// Append a rounded rectangle sub-path. Mirrors HTML Canvas `roundRect()`.
///
/// `radii` length: 1 = uniform, 2 = (tl/br, tr/bl), 3 = (tl, tr/bl, br),
/// 4 = (tl, tr, br, bl). Invalid lengths → nothing emitted.
/// Negative radii → abs(). Negative w/h → rect flips.
/// Radii exceeding min(|w|,|h|)/2 are proportionally scaled.
pub fn Path2D::round_rect(
  self : Path2D,
  x : Double,
  y : Double,
  w : Double,
  h : Double,
  radii : Array[Double],
) -> Unit {
  // 1. Expand to 4 corners (tl, tr, br, bl)
  let n = radii.length()
  if n != 1 && n != 2 && n != 3 && n != 4 {
    return // invalid length — emit nothing
  }
  let mut r_tl = radii[0]
  let mut r_tr = if n >= 2 { radii[1] } else { radii[0] }
  let mut r_br = if n >= 3 { radii[2] } else { radii[0] }
  let mut r_bl = if n == 4 {
    radii[3]
  } else if n == 2 {
    radii[1]
  } else if n == 3 {
    radii[1]
  } else {
    radii[0]
  }
  // 2. Absolute value
  if r_tl < 0.0 {
    r_tl = -r_tl
  }
  if r_tr < 0.0 {
    r_tr = -r_tr
  }
  if r_br < 0.0 {
    r_br = -r_br
  }
  if r_bl < 0.0 {
    r_bl = -r_bl
  }
  // 3. Normalize negative dimensions
  let mut x = x
  let mut y = y
  let mut w = w
  let mut h = h
  if w < 0.0 {
    x = x + w
    w = -w
  }
  if h < 0.0 {
    y = y + h
    h = -h
  }
  // 4. Proportional scale-down (HTML Canvas spec)
  let mut scale = 1.0
  let top = r_tl + r_tr
  if top > 0.0 && w / top < scale {
    scale = w / top
  }
  let right = r_tr + r_br
  if right > 0.0 && h / right < scale {
    scale = h / right
  }
  let bottom = r_br + r_bl
  if bottom > 0.0 && w / bottom < scale {
    scale = w / bottom
  }
  let left = r_bl + r_tl
  if left > 0.0 && h / left < scale {
    scale = h / left
  }
  if scale < 1.0 {
    r_tl = r_tl * scale
    r_tr = r_tr * scale
    r_br = r_br * scale
    r_bl = r_bl * scale
  }
  // 5. Emit sub-path (clockwise, top edge first)
  let half_pi = @math.PI / 2.0
  let pi = @math.PI
  // Top edge
  self.commands.push(PathCmd::MoveTo(x + r_tl, y))
  self.commands.push(PathCmd::LineTo(x + w - r_tr, y))
  // Top-right corner
  emit_arc_cubics(
    self,
    x + w - r_tr,
    y + r_tr,
    r_tr,
    r_tr,
    0.0,
    -half_pi,
    0.0,
    false,
  )
  // Right edge
  self.commands.push(PathCmd::LineTo(x + w, y + h - r_br))
  // Bottom-right corner
  emit_arc_cubics(
    self,
    x + w - r_br,
    y + h - r_br,
    r_br,
    r_br,
    0.0,
    0.0,
    half_pi,
    false,
  )
  // Bottom edge
  self.commands.push(PathCmd::LineTo(x + r_bl, y + h))
  // Bottom-left corner
  emit_arc_cubics(
    self,
    x + r_bl,
    y + h - r_bl,
    r_bl,
    r_bl,
    0.0,
    half_pi,
    pi,
    false,
  )
  // Left edge
  self.commands.push(PathCmd::LineTo(x, y + r_tl))
  // Top-left corner
  emit_arc_cubics(
    self,
    x + r_tl,
    y + r_tl,
    r_tl,
    r_tl,
    0.0,
    pi,
    pi + half_pi,
    false,
  )
  self.commands.push(PathCmd::Close)
}

///|
/// Approximate a circular arc with cubic Beziers of at most 90° per segment.
/// Angles are in radians, measured from the +X axis. The arc is traversed
/// counter-clockwise when `counterclockwise=true`, clockwise otherwise
/// (matching HTML Canvas semantics).
pub fn Path2D::arc(
  self : Path2D,
  cx : Double,
  cy : Double,
  radius : Double,
  start_angle : Double,
  end_angle : Double,
  counterclockwise? : Bool = false,
) -> Unit {
  self.ellipse(
    cx,
    cy,
    radius,
    radius,
    0.0,
    start_angle,
    end_angle,
    counterclockwise~,
  )
}

///|
/// Flatten a Path2D command stream into polylines in destination (post-matrix)
/// space. Each `Close` or a new `MoveTo` starts a new sub-polyline. Control
/// points are subdivided adaptively until each segment is within `flatness`
/// destination pixels of the ideal curve. `flatness` must be positive; the
/// caller is responsible for guarding against NaN and ≤0 values.
///
/// Empty polylines (produced by e.g. consecutive `Close` commands) are
/// discarded from the output.
pub fn flatten(
  cmds : Array[PathCmd],
  matrix : Matrix2D,
  flatness : Double,
) -> Array[Array[(Double, Double)]] {
  let polylines : Array[Array[(Double, Double)]] = []
  let mut current : Array[(Double, Double)] = []
  let mut cx = 0.0
  let mut cy = 0.0
  let mut start_x = 0.0
  let mut start_y = 0.0
  for cmd in cmds {
    match cmd {
      PathCmd::MoveTo(x, y) => {
        if current.length() > 0 {
          polylines.push(current)
        }
        current = []
        let (px, py) = matrix.transform_point(x, y)
        current.push((px, py))
        cx = x
        cy = y
        start_x = x
        start_y = y
      }
      PathCmd::LineTo(x, y) => {
        let (px, py) = matrix.transform_point(x, y)
        current.push((px, py))
        cx = x
        cy = y
      }
      PathCmd::QuadTo(x1, y1, x, y) => {
        flatten_quad(cx, cy, x1, y1, x, y, matrix, flatness, current)
        cx = x
        cy = y
      }
      PathCmd::CubicTo(x1, y1, x2, y2, x, y) => {
        flatten_cubic(cx, cy, x1, y1, x2, y2, x, y, matrix, flatness, current)
        cx = x
        cy = y
      }
      PathCmd::Close => {
        if current.length() > 0 {
          let (px, py) = matrix.transform_point(start_x, start_y)
          current.push((px, py))
          polylines.push(current)
          current = []
        }
        cx = start_x
        cy = start_y
      }
    }
  }
  if current.length() > 0 {
    polylines.push(current)
  }
  polylines
}

///|
/// Adaptive de Casteljau subdivision for a quadratic Bezier.
/// Appends the transformed endpoint and any intermediate split points to `out`.
fn flatten_quad(
  x0 : Double,
  y0 : Double,
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  matrix : Matrix2D,
  flatness : Double,
  out : Array[(Double, Double)],
) -> Unit {
  let (tx0, ty0) = matrix.transform_point(x0, y0)
  let (tx1, ty1) = matrix.transform_point(x1, y1)
  let (tx2, ty2) = matrix.transform_point(x2, y2)
  // Error estimate: distance of the control point from the chord midpoint.
  let mx = (tx0 + tx2) * 0.5
  let my = (ty0 + ty2) * 0.5
  let dx = tx1 - mx
  let dy = ty1 - my
  let err_sq = dx * dx + dy * dy
  if err_sq <= flatness * flatness {
    out.push((tx2, ty2))
    return
  }
  let x01 = (x0 + x1) * 0.5
  let y01 = (y0 + y1) * 0.5
  let x12 = (x1 + x2) * 0.5
  let y12 = (y1 + y2) * 0.5
  let xm = (x01 + x12) * 0.5
  let ym = (y01 + y12) * 0.5
  flatten_quad(x0, y0, x01, y01, xm, ym, matrix, flatness, out)
  flatten_quad(xm, ym, x12, y12, x2, y2, matrix, flatness, out)
}

///|
/// Adaptive de Casteljau subdivision for a cubic Bezier.
fn flatten_cubic(
  x0 : Double,
  y0 : Double,
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  x3 : Double,
  y3 : Double,
  matrix : Matrix2D,
  flatness : Double,
  out : Array[(Double, Double)],
) -> Unit {
  let (tx0, ty0) = matrix.transform_point(x0, y0)
  let (tx1, ty1) = matrix.transform_point(x1, y1)
  let (tx2, ty2) = matrix.transform_point(x2, y2)
  let (tx3, ty3) = matrix.transform_point(x3, y3)
  // Use distance of control points from the chord as a flatness estimate.
  let dx = tx3 - tx0
  let dy = ty3 - ty0
  let chord_sq = dx * dx + dy * dy
  let max_err_sq = if chord_sq == 0.0 {
    let ex = tx1 - tx0
    let ey = ty1 - ty0
    let fx = tx2 - tx0
    let fy = ty2 - ty0
    let e = ex * ex + ey * ey
    let f = fx * fx + fy * fy
    if e > f {
      e
    } else {
      f
    }
  } else {
    let d1 = (tx1 - tx0) * dy - (ty1 - ty0) * dx
    let d2 = (tx2 - tx0) * dy - (ty2 - ty0) * dx
    let d1_sq = d1 * d1 / chord_sq
    let d2_sq = d2 * d2 / chord_sq
    if d1_sq > d2_sq {
      d1_sq
    } else {
      d2_sq
    }
  }
  if max_err_sq <= flatness * flatness {
    out.push((tx3, ty3))
    return
  }
  // de Casteljau subdivision at t = 0.5
  let x01 = (x0 + x1) * 0.5
  let y01 = (y0 + y1) * 0.5
  let x12 = (x1 + x2) * 0.5
  let y12 = (y1 + y2) * 0.5
  let x23 = (x2 + x3) * 0.5
  let y23 = (y2 + y3) * 0.5
  let x012 = (x01 + x12) * 0.5
  let y012 = (y01 + y12) * 0.5
  let x123 = (x12 + x23) * 0.5
  let y123 = (y12 + y23) * 0.5
  let xm = (x012 + x123) * 0.5
  let ym = (y012 + y123) * 0.5
  flatten_cubic(x0, y0, x01, y01, x012, y012, xm, ym, matrix, flatness, out)
  flatten_cubic(xm, ym, x123, y123, x23, y23, x3, y3, matrix, flatness, out)
}