///|
pub struct MotionStatistics {
  rapid_count_value : Int
  linear_count_value : Int
  arc_count_value : Int
  total_distance_value : Double
  cutting_distance_value : Double
  min_feed_value : Double
  max_feed_value : Double
} derive(Eq, Debug)

///|
pub fn MotionStatistics::rapid_count(self : MotionStatistics) -> Int {
  self.rapid_count_value
}

///|
pub fn MotionStatistics::linear_count(self : MotionStatistics) -> Int {
  self.linear_count_value
}

///|
pub fn MotionStatistics::arc_count(self : MotionStatistics) -> Int {
  self.arc_count_value
}

///|
pub fn MotionStatistics::total_distance(self : MotionStatistics) -> Double {
  self.total_distance_value
}

///|
pub fn MotionStatistics::cutting_distance(self : MotionStatistics) -> Double {
  self.cutting_distance_value
}

///|
pub fn MotionStatistics::min_feed(self : MotionStatistics) -> Double {
  self.min_feed_value
}

///|
pub fn MotionStatistics::max_feed(self : MotionStatistics) -> Double {
  self.max_feed_value
}

///|
/// Summarize motion classes, distances, and observed feed-rate range.
pub fn analyze(program : GCodeProgram) -> MotionStatistics {
  let mut rapid = 0
  let mut linear = 0
  let mut arc = 0
  let mut total = 0.0
  let mut cutting = 0.0
  let mut min_feed = 0.0
  let mut max_feed = 0.0
  let mut has_feed = false
  for motion in program.motions() {
    let length = motion_path_length(motion)
    total += length
    match motion.kind() {
      Rapid => rapid += 1
      Linear => {
        linear += 1
        cutting += length
      }
      ClockwiseArc | CounterclockwiseArc => {
        arc += 1
        cutting += length
      }
    }
    match motion.feed() {
      None => ()
      Some(value) =>
        if !has_feed {
          min_feed = value
          max_feed = value
          has_feed = true
        } else {
          if value < min_feed {
            min_feed = value
          }
          if value > max_feed {
            max_feed = value
          }
        }
    }
  }
  {
    rapid_count_value: rapid,
    linear_count_value: linear,
    arc_count_value: arc,
    total_distance_value: total,
    cutting_distance_value: cutting,
    min_feed_value: min_feed,
    max_feed_value: max_feed,
  }
}

///|
/// Parse several independent programs while preserving each input label.
pub struct GCodeBatchItem {
  label_value : String
  program_value : GCodeProgram?
  error_value : GCodeError?
} derive(Eq, Debug)

///|
pub fn GCodeBatchItem::label(self : GCodeBatchItem) -> String {
  self.label_value
}

///|
pub fn GCodeBatchItem::accepted(self : GCodeBatchItem) -> Bool {
  self.program_value is Some(_)
}

///|
pub fn GCodeBatchItem::program(self : GCodeBatchItem) -> GCodeProgram? {
  self.program_value
}

///|
pub fn GCodeBatchItem::error(self : GCodeBatchItem) -> GCodeError? {
  self.error_value
}

///|
pub fn parse_gcode_batch(
  inputs : Array[(String, String)],
  max_items? : Int = 1000,
) -> Array[GCodeBatchItem] {
  let results : Array[GCodeBatchItem] = []
  for index, item in inputs {
    if index >= max_items {
      break
    }
    let (label, text) = item
    match parse_gcode(text) {
      Ok(program) =>
        results.push({
          label_value: label,
          program_value: Some(program),
          error_value: None,
        })
      Err(error) =>
        results.push({
          label_value: label,
          program_value: None,
          error_value: Some(error),
        })
    }
  }
  results
}