///|
pub(all) enum GCodeErrorCode {
  EmptyLine
  InvalidWord
  InvalidNumber
  UnknownCommand
  InvalidArc
} derive(Eq, Debug)

///|
pub struct SourceLocation {
  line_value : Int
  column_value : Int
} derive(Eq, Debug)

///|
pub fn SourceLocation::new(line : Int, column : Int) -> SourceLocation {
  { line_value: line, column_value: column }
}

///|
pub fn SourceLocation::line(self : SourceLocation) -> Int {
  self.line_value
}

///|
pub fn SourceLocation::column(self : SourceLocation) -> Int {
  self.column_value
}

///|
pub struct GCodeError {
  code_value : GCodeErrorCode
  message_value : String
  location_value : SourceLocation
} derive(Eq, Debug)

///|
pub fn GCodeError::code(self : GCodeError) -> GCodeErrorCode {
  self.code_value
}

///|
pub fn GCodeError::message(self : GCodeError) -> String {
  self.message_value
}

///|
pub fn GCodeError::location(self : GCodeError) -> SourceLocation {
  self.location_value
}

///|
pub fn GCodeError::format(self : GCodeError) -> String {
  "line " +
  self.location().line().to_string() +
  ":" +
  self.location().column().to_string() +
  ": " +
  self.message()
}

///|
pub(all) enum MotionKind {
  Rapid
  Linear
  ClockwiseArc
  CounterclockwiseArc
} derive(Eq, Debug)

///|
pub struct Position {
  x_value : Double
  y_value : Double
  z_value : Double
  e_value : Double
} derive(Eq, Debug)

///|
pub fn Position::new(
  x : Double,
  y : Double,
  z : Double,
  e : Double,
) -> Position {
  { x_value: x, y_value: y, z_value: z, e_value: e }
}

///|
pub fn Position::x(self : Position) -> Double {
  self.x_value
}

///|
pub fn Position::y(self : Position) -> Double {
  self.y_value
}

///|
pub fn Position::z(self : Position) -> Double {
  self.z_value
}

///|
pub fn Position::e(self : Position) -> Double {
  self.e_value
}

///|
pub struct Motion {
  kind_value : MotionKind
  start_value : Position
  end_value : Position
  i_value : Double
  j_value : Double
  feed_value : Double?
  source_line_value : Int
} derive(Eq, Debug)

///|
pub fn Motion::kind(self : Motion) -> MotionKind {
  self.kind_value
}

///|
pub fn Motion::start(self : Motion) -> Position {
  self.start_value
}

///|
pub fn Motion::end(self : Motion) -> Position {
  self.end_value
}

///|
pub fn Motion::feed(self : Motion) -> Double? {
  self.feed_value
}

///|
pub fn Motion::i(self : Motion) -> Double {
  self.i_value
}

///|
pub fn Motion::j(self : Motion) -> Double {
  self.j_value
}

///|
pub fn Motion::source_line(self : Motion) -> Int {
  self.source_line_value
}

///|
pub struct GCodeProgram {
  motions_value : Array[Motion]
  ignored_lines_value : Int
} derive(Eq, Debug)

///|
pub fn GCodeProgram::motions(self : GCodeProgram) -> Array[Motion] {
  self.motions_value.copy()
}

///|
pub fn GCodeProgram::motion_count(self : GCodeProgram) -> Int {
  self.motions_value.length()
}

///|
pub fn GCodeProgram::ignored_lines(self : GCodeProgram) -> Int {
  self.ignored_lines_value
}

///|
fn make_error(
  code : GCodeErrorCode,
  message : String,
  line : Int,
) -> GCodeError {
  {
    code_value: code,
    message_value: message,
    location_value: SourceLocation::new(line, 1),
  }
}

///|
fn parse_number(text : String) -> Double? {
  if text == "" {
    return None
  }
  let mut sign = 1.0
  let mut value = 0.0
  let mut fraction = 0.0
  let mut divisor = 1.0
  let mut decimal = false
  let mut digits = 0
  for index, character in text {
    let code = character.to_int()
    if index == 0 && code == 45 {
      sign = -1.0
    } else if index == 0 && code == 43 {
      ()
    } else if code == 46 && !decimal {
      decimal = true
    } else if code >= 48 && code <= 57 {
      let digit = (code - 48).to_double()
      digits += 1
      if decimal {
        divisor *= 10.0
        fraction += digit / divisor
      } else {
        value = value * 10.0 + digit
      }
    } else {
      return None
    }
  }
  if digits == 0 {
    None
  } else {
    Some(sign * (value + fraction))
  }
}

///|
fn word_number(word : String) -> Double? {
  parse_number(word[1:].to_owned())
}

///|
fn coordinate(current : Double, value : Double?) -> Double {
  match value {
    Some(v) => v
    None => current
  }
}

///|
fn modal_coordinate(
  current : Double,
  value : Double,
  absolute : Bool,
  unit_scale : Double,
) -> Double {
  if absolute {
    value * unit_scale
  } else {
    current + value * unit_scale
  }
}

///|
pub fn parse_gcode(
  text : String,
  max_lines? : Int = 100000,
) -> Result[GCodeProgram, GCodeError] {
  let motions : Array[Motion] = []
  let mut position = Position::new(0.0, 0.0, 0.0, 0.0)
  let mut feed : Double? = None
  let mut modal_motion : MotionKind? = None
  let mut absolute = true
  let mut unit_scale = 1.0
  let mut ignored = 0
  let mut line_number = 0
  for raw in text.split("\n") {
    line_number += 1
    if line_number > max_lines {
      return Err(make_error(InvalidWord, "line limit exceeded", line_number))
    }
    let line = clean_gcode_line(raw.to_owned())
    if line == "" {
      ignored += 1
      continue
    }
    if line[0].to_int() == 59 {
      ignored += 1
      continue
    }
    let words = tokenize_gcode_line(line)
    if words.length() > 256 {
      return Err(make_error(InvalidWord, "word limit exceeded", line_number))
    }
    let mut kind : MotionKind? = None
    let mut x : Double? = None
    let mut y : Double? = None
    let mut z : Double? = None
    let mut e : Double? = None
    let mut i = 0.0
    let mut j = 0.0
    let mut has_axis = false
    let mut has_arc_offset = false
    for word in words {
      if word.length() < 2 {
        return Err(make_error(InvalidWord, "word is too short", line_number))
      }
      let number = match word_number(word) {
        Some(v) => v
        None =>
          return Err(
            make_error(InvalidNumber, "invalid numeric word", line_number),
          )
      }
      let raw_letter = word[0].to_int()
      let letter = if raw_letter >= 97 && raw_letter <= 122 {
        raw_letter - 32
      } else {
        raw_letter
      }
      if letter == 71 {
        if number == 90.0 {
          absolute = true
        } else if number == 91.0 {
          absolute = false
        } else if number == 20.0 {
          unit_scale = 25.4
        } else if number == 21.0 {
          unit_scale = 1.0
        } else if number == 0.0 {
          kind = Some(Rapid)
          modal_motion = kind
        } else if number == 1.0 {
          kind = Some(Linear)
          modal_motion = kind
        } else if number == 2.0 {
          kind = Some(ClockwiseArc)
          modal_motion = kind
        } else if number == 3.0 {
          kind = Some(CounterclockwiseArc)
          modal_motion = kind
        } else {
          return Err(
            make_error(UnknownCommand, "unsupported G motion", line_number),
          )
        }
      } else if letter == 88 {
        x = Some(modal_coordinate(position.x(), number, absolute, unit_scale))
        has_axis = true
      } else if letter == 89 {
        y = Some(modal_coordinate(position.y(), number, absolute, unit_scale))
        has_axis = true
      } else if letter == 90 {
        z = Some(modal_coordinate(position.z(), number, absolute, unit_scale))
        has_axis = true
      } else if letter == 69 {
        e = Some(modal_coordinate(position.e(), number, absolute, unit_scale))
        has_axis = true
      } else if letter == 70 {
        feed = Some(number * unit_scale)
      } else if letter == 73 {
        i = number * unit_scale
        has_arc_offset = true
      } else if letter == 74 {
        j = number * unit_scale
        has_arc_offset = true
      } else if letter == 77 || letter == 78 {
        ignored += 1
      } else {
        return Err(
          make_error(InvalidWord, "unsupported word letter", line_number),
        )
      }
    }
    let effective_kind = match kind {
      Some(value) => Some(value)
      None => modal_motion
    }
    match effective_kind {
      None => ignored += 1
      Some(motion_kind) => {
        if !has_axis && !has_arc_offset {
          ignored += 1
          continue
        }
        if motions.length() >= 1000000 {
          return Err(
            make_error(InvalidWord, "motion limit exceeded", line_number),
          )
        }
        let end = Position::new(
          coordinate(position.x(), x),
          coordinate(position.y(), y),
          coordinate(position.z(), z),
          coordinate(position.e(), e),
        )
        if (motion_kind == ClockwiseArc || motion_kind == CounterclockwiseArc) &&
          i == 0.0 &&
          j == 0.0 {
          return Err(make_error(InvalidArc, "arc requires I or J", line_number))
        }
        motions.push({
          kind_value: motion_kind,
          start_value: position,
          end_value: end,
          i_value: i,
          j_value: j,
          feed_value: feed,
          source_line_value: line_number,
        })
        position = end
      }
    }
  }
  Ok({ motions_value: motions, ignored_lines_value: ignored })
}

///|
pub fn total_distance(program : GCodeProgram) -> Double {
  let mut total = 0.0
  for motion in program.motions() {
    total += motion_path_length(motion)
  }
  total
}

///|
pub struct Bounds {
  min_value : Position
  max_value : Position
} derive(Eq, Debug)

///|
pub fn Bounds::min(self : Bounds) -> Position {
  self.min_value
}

///|
pub fn Bounds::max(self : Bounds) -> Position {
  self.max_value
}

///|
pub fn bounds(program : GCodeProgram) -> Bounds {
  let mut min = Position::new(0.0, 0.0, 0.0, 0.0)
  let mut max = min
  for motion in program.motions() {
    for point in [motion.start(), motion.end()] {
      min = Position::new(
        if point.x() < min.x() {
          point.x()
        } else {
          min.x()
        },
        if point.y() < min.y() {
          point.y()
        } else {
          min.y()
        },
        if point.z() < min.z() {
          point.z()
        } else {
          min.z()
        },
        0.0,
      )
      max = Position::new(
        if point.x() > max.x() {
          point.x()
        } else {
          max.x()
        },
        if point.y() > max.y() {
          point.y()
        } else {
          max.y()
        },
        if point.z() > max.z() {
          point.z()
        } else {
          max.z()
        },
        0.0,
      )
    }
  }
  { min_value: min, max_value: max }
}

///|
pub fn render_summary(program : GCodeProgram) -> String {
  let box = bounds(program)
  "motions=" +
  program.motion_count().to_string() +
  ", distance=" +
  total_distance(program).to_string() +
  ", bounds=[" +
  box.min().x().to_string() +
  "," +
  box.min().y().to_string() +
  "]..[" +
  box.max().x().to_string() +
  "," +
  box.max().y().to_string() +
  "]"
}