///|
pub struct ArcGeometry {
  center_x_value : Double
  center_y_value : Double
  radius_value : Double
  chord_value : Double
  clockwise_value : Bool
} derive(Eq, Debug)

///|
pub fn ArcGeometry::center_x(self : ArcGeometry) -> Double {
  self.center_x_value
}

///|
pub fn ArcGeometry::center_y(self : ArcGeometry) -> Double {
  self.center_y_value
}

///|
pub fn ArcGeometry::radius(self : ArcGeometry) -> Double {
  self.radius_value
}

///|
pub fn ArcGeometry::chord(self : ArcGeometry) -> Double {
  self.chord_value
}

///|
pub fn ArcGeometry::clockwise(self : ArcGeometry) -> Bool {
  self.clockwise_value
}

///|
fn absolute(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
/// Derive stable geometric facts for an I/J offset arc.
pub fn arc_geometry(motion : Motion) -> Result[ArcGeometry, GCodeError] {
  if motion.kind() != ClockwiseArc && motion.kind() != CounterclockwiseArc {
    return Err({
      code_value: InvalidArc,
      message_value: "motion is not an arc",
      location_value: SourceLocation::new(motion.source_line(), 1),
    })
  }
  let center_x = motion.start().x() + motion.i()
  let center_y = motion.start().y() + motion.j()
  let start_dx = motion.start().x() - center_x
  let start_dy = motion.start().y() - center_y
  let end_dx = motion.end().x() - center_x
  let end_dy = motion.end().y() - center_y
  let start_radius = (start_dx * start_dx + start_dy * start_dy).sqrt()
  let end_radius = (end_dx * end_dx + end_dy * end_dy).sqrt()
  if start_radius == 0.0 || absolute(start_radius - end_radius) > 0.000001 {
    return Err({
      code_value: InvalidArc,
      message_value: "arc endpoints do not share a radius",
      location_value: SourceLocation::new(motion.source_line(), 1),
    })
  }
  let chord_dx = motion.end().x() - motion.start().x()
  let chord_dy = motion.end().y() - motion.start().y()
  Ok({
    center_x_value: center_x,
    center_y_value: center_y,
    radius_value: start_radius,
    chord_value: (chord_dx * chord_dx + chord_dy * chord_dy).sqrt(),
    clockwise_value: motion.kind() == ClockwiseArc,
  })
}

///|
pub fn arc_chord_sum(program : GCodeProgram) -> Double {
  let mut total = 0.0
  for motion in program.motions() {
    match arc_geometry(motion) {
      Ok(arc) => total += arc.chord()
      Err(_) => ()
    }
  }
  total
}

///|
/// Calculate the swept XY arc length, including simultaneous Z travel.
pub fn arc_path_length(motion : Motion) -> Result[Double, GCodeError] {
  let geometry = match arc_geometry(motion) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let start_angle = @math.atan2(
    motion.start().y() - geometry.center_y(),
    motion.start().x() - geometry.center_x(),
  )
  let end_angle = @math.atan2(
    motion.end().y() - geometry.center_y(),
    motion.end().x() - geometry.center_x(),
  )
  let mut sweep = if motion.kind() == ClockwiseArc {
    start_angle - end_angle
  } else {
    end_angle - start_angle
  }
  while sweep <= 0.0 {
    sweep += 2.0 * @math.PI
  }
  let planar = geometry.radius() * sweep
  let dz = motion.end().z() - motion.start().z()
  Ok((planar * planar + dz * dz).sqrt())
}

///|
fn straight_path_length(motion : Motion) -> Double {
  let dx = motion.end().x() - motion.start().x()
  let dy = motion.end().y() - motion.start().y()
  let dz = motion.end().z() - motion.start().z()
  (dx * dx + dy * dy + dz * dz).sqrt()
}

///|
fn motion_path_length(motion : Motion) -> Double {
  match motion.kind() {
    ClockwiseArc | CounterclockwiseArc =>
      match arc_path_length(motion) {
        Ok(value) => value
        Err(_) => straight_path_length(motion)
      }
    Rapid | Linear => straight_path_length(motion)
  }
}