///|
/// A lexical word preserved from a G-code block.
pub struct BlockWord {
  letter_value : Int
  value_value : Double
  raw_value : String
  column_value : Int
} derive(Eq, Debug)

///|
pub fn BlockWord::letter(self : BlockWord) -> Int {
  self.letter_value
}

///|
pub fn BlockWord::value(self : BlockWord) -> Double {
  self.value_value
}

///|
pub fn BlockWord::raw(self : BlockWord) -> String {
  self.raw_value
}

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

///|
pub fn BlockWord::letter_text(self : BlockWord) -> String {
  self.letter_value.unsafe_to_char().to_string()
}

///|
/// One source block with comments and lexical metadata preserved.
pub struct GCodeBlock {
  source_value : String
  body_value : String
  source_line_value : Int
  sequence_value : Int?
  words_value : Array[BlockWord]
  comments_value : Array[String]
  checksum_value : Int?
  computed_checksum_value : Int?
} derive(Eq, Debug)

///|
pub fn GCodeBlock::source(self : GCodeBlock) -> String {
  self.source_value
}

///|
pub fn GCodeBlock::body(self : GCodeBlock) -> String {
  self.body_value
}

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

///|
pub fn GCodeBlock::sequence(self : GCodeBlock) -> Int? {
  self.sequence_value
}

///|
pub fn GCodeBlock::words(self : GCodeBlock) -> Array[BlockWord] {
  self.words_value.copy()
}

///|
pub fn GCodeBlock::comments(self : GCodeBlock) -> Array[String] {
  self.comments_value.copy()
}

///|
pub fn GCodeBlock::checksum(self : GCodeBlock) -> Int? {
  self.checksum_value
}

///|
pub fn GCodeBlock::computed_checksum(self : GCodeBlock) -> Int? {
  self.computed_checksum_value
}

///|
pub fn GCodeBlock::checksum_valid(self : GCodeBlock) -> Bool {
  match (self.checksum_value, self.computed_checksum_value) {
    (Some(expected), Some(actual)) => expected == actual
    (None, None) => true
    _ => false
  }
}

///|
pub fn GCodeBlock::is_comment_only(self : GCodeBlock) -> Bool {
  self.words_value.length() == 0 && self.comments_value.length() > 0
}

///|
pub fn GCodeBlock::word_count(self : GCodeBlock) -> Int {
  self.words_value.length()
}

///|
pub fn GCodeBlock::has_letter(self : GCodeBlock, letter : Int) -> Bool {
  let upper = ascii_upper(letter)
  for word in self.words_value {
    if word.letter() == upper {
      return true
    }
  }
  false
}

///|
pub fn GCodeBlock::values_for(self : GCodeBlock, letter : Int) -> Array[Double] {
  let values : Array[Double] = []
  let upper = ascii_upper(letter)
  for word in self.words_value {
    if word.letter() == upper {
      values.push(word.value())
    }
  }
  values
}

///|
pub fn GCodeBlock::first_value(self : GCodeBlock, letter : Int) -> Double? {
  let upper = ascii_upper(letter)
  for word in self.words_value {
    if word.letter() == upper {
      return Some(word.value())
    }
  }
  None
}

///|
pub fn GCodeBlock::duplicate_letters(self : GCodeBlock) -> Array[Int] {
  let duplicates : Array[Int] = []
  let seen : Array[Int] = []
  for word in self.words_value {
    let letter = word.letter()
    let mut already_seen = false
    for value in seen {
      if value == letter {
        already_seen = true
      }
    }
    if already_seen {
      let mut already_reported = false
      for value in duplicates {
        if value == letter {
          already_reported = true
        }
      }
      if !already_reported {
        duplicates.push(letter)
      }
    } else {
      seen.push(letter)
    }
  }
  duplicates
}

///|
/// A complete lexical document. Empty physical lines are intentionally kept.
pub struct GCodeDocument {
  blocks_value : Array[GCodeBlock]
  physical_lines_value : Int
} derive(Eq, Debug)

///|
pub fn GCodeDocument::blocks(self : GCodeDocument) -> Array[GCodeBlock] {
  self.blocks_value.copy()
}

///|
pub fn GCodeDocument::physical_lines(self : GCodeDocument) -> Int {
  self.physical_lines_value
}

///|
pub fn GCodeDocument::block_count(self : GCodeDocument) -> Int {
  self.blocks_value.length()
}

///|
pub fn GCodeDocument::executable_block_count(self : GCodeDocument) -> Int {
  let mut count = 0
  for block in self.blocks_value {
    if block.word_count() > 0 {
      count += 1
    }
  }
  count
}

///|
pub fn GCodeDocument::comment_count(self : GCodeDocument) -> Int {
  let mut count = 0
  for block in self.blocks_value {
    count += block.comments().length()
  }
  count
}

///|
pub fn GCodeDocument::checksum_count(self : GCodeDocument) -> Int {
  let mut count = 0
  for block in self.blocks_value {
    if block.checksum() is Some(_) {
      count += 1
    }
  }
  count
}

///|
pub fn GCodeDocument::invalid_checksum_lines(
  self : GCodeDocument,
) -> Array[Int] {
  let lines : Array[Int] = []
  for block in self.blocks_value {
    if !block.checksum_valid() {
      lines.push(block.source_line())
    }
  }
  lines
}

///|
fn ascii_upper(code : Int) -> Int {
  if code >= 97 && code <= 122 {
    code - 32
  } else {
    code
  }
}

///|
fn is_ascii_letter(code : Int) -> Bool {
  (code >= 65 && code <= 90) || (code >= 97 && code <= 122)
}

///|
fn is_space(code : Int) -> Bool {
  code == 32 || code == 9 || code == 13
}

///|
fn trim_ascii(text : String) -> String {
  text.trim().to_owned()
}

///|
fn parse_integer_word(text : String) -> Int? {
  match parse_number(text) {
    Some(value) => {
      let integer = value.to_int()
      if integer.to_double() == value {
        Some(integer)
      } else {
        None
      }
    }
    None => None
  }
}

///|
fn checksum_for_text(text : String) -> Int {
  let mut value = 0
  for character in text {
    value = value ^ character.to_int()
  }
  value & 0xFF
}

///|
struct BlockScan {
  body : String
  checksum_body : String
  comments : Array[String]
  checksum_text : String?
} derive(Eq, Debug)

///|
fn scan_block_source(raw : String, line : Int) -> Result[BlockScan, GCodeError] {
  let mut body = ""
  let mut checksum_body = ""
  let comments : Array[String] = []
  let mut parenthesized = ""
  let mut in_parentheses = false
  let mut semicolon_comment = false
  let mut after_checksum = false
  let mut checksum_text = ""
  for character in raw {
    let code = character.to_int()
    if semicolon_comment {
      checksum_body = checksum_body + character.to_string()
      parenthesized = parenthesized + character.to_string()
    } else if in_parentheses {
      checksum_body = checksum_body + character.to_string()
      if code == 41 {
        comments.push(trim_ascii(parenthesized))
        parenthesized = ""
        in_parentheses = false
      } else if code == 40 {
        return Err(make_error(InvalidWord, "nested comment", line))
      } else {
        parenthesized = parenthesized + character.to_string()
      }
    } else if code == 40 {
      checksum_body = checksum_body + character.to_string()
      in_parentheses = true
    } else if code == 41 {
      return Err(make_error(InvalidWord, "unmatched closing comment", line))
    } else if code == 59 {
      checksum_body = checksum_body + character.to_string()
      semicolon_comment = true
    } else if code == 42 {
      if after_checksum {
        return Err(make_error(InvalidWord, "multiple checksum markers", line))
      }
      after_checksum = true
    } else if after_checksum {
      if !is_space(code) {
        checksum_text = checksum_text + character.to_string()
      }
    } else {
      body = body + character.to_string()
      checksum_body = checksum_body + character.to_string()
    }
  }
  if in_parentheses {
    return Err(make_error(InvalidWord, "unterminated comment", line))
  }
  if semicolon_comment {
    comments.push(trim_ascii(parenthesized))
  }
  Ok({
    body: trim_ascii(body),
    checksum_body,
    comments,
    checksum_text: if after_checksum {
      Some(checksum_text)
    } else {
      None
    },
  })
}

///|
fn lexical_words(
  body : String,
  line : Int,
) -> Result[Array[BlockWord], GCodeError] {
  let words : Array[BlockWord] = []
  let mut current = ""
  let mut current_column = 0
  for index, character in body {
    let code = character.to_int()
    if is_ascii_letter(code) {
      if current != "" {
        let value = match word_number(current) {
          Some(number) => number
          None =>
            return Err(make_error(InvalidNumber, "invalid block word", line))
        }
        words.push({
          letter_value: ascii_upper(current[0].to_int()),
          value_value: value,
          raw_value: current,
          column_value: current_column,
        })
      }
      current = character.to_string()
      current_column = index + 1
    } else if is_space(code) {
      if current != "" {
        let value = match word_number(current) {
          Some(number) => number
          None =>
            return Err(make_error(InvalidNumber, "invalid block word", line))
        }
        words.push({
          letter_value: ascii_upper(current[0].to_int()),
          value_value: value,
          raw_value: current,
          column_value: current_column,
        })
        current = ""
      }
    } else if current == "" {
      return Err(
        make_error(InvalidWord, "value appears before a word letter", line),
      )
    } else {
      current = current + character.to_string()
    }
  }
  if current != "" {
    let value = match word_number(current) {
      Some(number) => number
      None => return Err(make_error(InvalidNumber, "invalid block word", line))
    }
    words.push({
      letter_value: ascii_upper(current[0].to_int()),
      value_value: value,
      raw_value: current,
      column_value: current_column,
    })
  }
  Ok(words)
}

///|
fn block_sequence(words : Array[BlockWord]) -> Int? {
  for word in words {
    if word.letter() == 78 {
      let value = word.value().to_int()
      if value.to_double() == word.value() {
        return Some(value)
      }
    }
  }
  None
}

///|
/// Parse source blocks without interpreting machine motion.
pub fn parse_gcode_document(
  text : String,
  max_lines? : Int = 100000,
) -> Result[GCodeDocument, GCodeError] {
  let blocks : Array[GCodeBlock] = []
  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 scan = match scan_block_source(raw.to_owned(), line_number) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let words = match lexical_words(scan.body, line_number) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let checksum = match scan.checksum_text {
      Some(text) =>
        match parse_integer_word(text) {
          Some(value) if value >= 0 && value <= 255 => Some(value)
          _ =>
            return Err(
              make_error(InvalidNumber, "invalid checksum", line_number),
            )
        }
      None => None
    }
    blocks.push({
      source_value: raw.to_owned(),
      body_value: scan.body,
      source_line_value: line_number,
      sequence_value: block_sequence(words),
      words_value: words,
      comments_value: scan.comments,
      checksum_value: checksum,
      computed_checksum_value: match checksum {
        Some(_) => Some(checksum_for_text(scan.checksum_body))
        None => None
      },
    })
  }
  Ok({ blocks_value: blocks, physical_lines_value: line_number })
}

///|
pub fn render_block_structure(document : GCodeDocument) -> String {
  let mut output = "lines=" + document.physical_lines().to_string()
  output = output +
    ", executable=" +
    document.executable_block_count().to_string()
  output = output + ", comments=" + document.comment_count().to_string()
  output = output +
    ", checksums=" +
    document.checksum_count().to_string() +
    "\n"
  for block in document.blocks() {
    output = output + "line " + block.source_line().to_string()
    match block.sequence() {
      Some(value) => output = output + " N" + value.to_string()
      None => ()
    }
    output = output + " words=" + block.word_count().to_string()
    output = output + " comments=" + block.comments().length().to_string()
    if block.checksum() is Some(_) {
      output = output +
        " checksum=" +
        (if block.checksum_valid() { "valid" } else { "invalid" })
    }
    output = output + "\n"
  }
  output
}