///|
/// 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 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)
}

///|
/// Approximate a circular arc with cubic Beziers of at most 90° per segment.
/// Uses the standard control-point distance `k = 4 * tan(delta/4) / 3`.
///
/// Angles are in radians, measured from the +X axis. The arc is traversed
/// counter-clockwise when `counterclockwise=true`, clockwise otherwise
/// (matching HTML Canvas semantics). If `start_angle == end_angle`, only a
/// `MoveTo` to the start point is emitted; no cubic segments are appended.
pub fn Path2D::arc(
  self : Path2D,
  cx : Double,
  cy : Double,
  radius : Double,
  start_angle : Double,
  end_angle : Double,
  counterclockwise? : Bool = false,
) -> Unit {
  let tau = 2.0 * @math.PI
  let half_pi = @math.PI / 2.0
  let a0 = start_angle
  let mut a1 = end_angle
  if counterclockwise {
    // Ensure a1 < a0 for CCW drawing.
    while a1 > a0 {
      a1 = a1 - tau
    }
  } else {
    while a1 < a0 {
      a1 = a1 + tau
    }
  }
  let x0 = cx + radius * @math.cos(a0)
  let y0 = cy + radius * @math.sin(a0)
  self.commands.push(PathCmd::MoveTo(x0, y0))
  let step = if counterclockwise { -half_pi } else { half_pi }
  let mut a = a0
  // Emit ≤ 90° cubic segments until we reach or pass a1.
  while (!counterclockwise && a < a1) || (counterclockwise && a > a1) {
    let next = if counterclockwise {
      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 p1x = cx + radius * (cos_a - k * sin_a)
    let p1y = cy + radius * (sin_a + k * cos_a)
    let p2x = cx + radius * (cos_n + k * sin_n)
    let p2y = cy + radius * (sin_n - k * cos_n)
    let p3x = cx + radius * cos_n
    let p3y = cy + radius * sin_n
    self.commands.push(PathCmd::CubicTo(p1x, p1y, p2x, p2y, p3x, p3y))
    a = next
  }
}

///|
/// 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)
}