///|
pub struct CommandOccurrence {
  letter_value : Int
  code_value : Double
  line_value : Int
  column_value : Int
} derive(Eq, Debug)

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

///|
pub fn CommandOccurrence::code(self : CommandOccurrence) -> Double {
  self.code_value
}

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

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

///|
pub struct LetterUsage {
  letter_value : Int
  count_value : Int
  first_line_value : Int
  last_line_value : Int
  minimum_value : Double
  maximum_value : Double
} derive(Eq, Debug)

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

///|
pub fn LetterUsage::count(self : LetterUsage) -> Int {
  self.count_value
}

///|
pub fn LetterUsage::first_line(self : LetterUsage) -> Int {
  self.first_line_value
}

///|
pub fn LetterUsage::last_line(self : LetterUsage) -> Int {
  self.last_line_value
}

///|
pub fn LetterUsage::minimum(self : LetterUsage) -> Double {
  self.minimum_value
}

///|
pub fn LetterUsage::maximum(self : LetterUsage) -> Double {
  self.maximum_value
}

///|
pub struct SequenceGap {
  previous_value : Int
  current_value : Int
  line_value : Int
  missing_value : Int
} derive(Eq, Debug)

///|
pub fn SequenceGap::previous(self : SequenceGap) -> Int {
  self.previous_value
}

///|
pub fn SequenceGap::current(self : SequenceGap) -> Int {
  self.current_value
}

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

///|
pub fn SequenceGap::missing(self : SequenceGap) -> Int {
  self.missing_value
}

///|
pub struct ProgramIndex {
  commands_value : Array[CommandOccurrence]
  usage_value : Array[LetterUsage]
  executable_lines_value : Array[Int]
  comment_lines_value : Array[Int]
  sequence_gaps_value : Array[SequenceGap]
} derive(Eq, Debug)

///|
pub fn ProgramIndex::commands(self : ProgramIndex) -> Array[CommandOccurrence] {
  self.commands_value.copy()
}

///|
pub fn ProgramIndex::usage(self : ProgramIndex) -> Array[LetterUsage] {
  self.usage_value.copy()
}

///|
pub fn ProgramIndex::executable_lines(self : ProgramIndex) -> Array[Int] {
  self.executable_lines_value.copy()
}

///|
pub fn ProgramIndex::comment_lines(self : ProgramIndex) -> Array[Int] {
  self.comment_lines_value.copy()
}

///|
pub fn ProgramIndex::sequence_gaps(self : ProgramIndex) -> Array[SequenceGap] {
  self.sequence_gaps_value.copy()
}

///|
pub fn ProgramIndex::command_count(self : ProgramIndex) -> Int {
  self.commands_value.length()
}

///|
pub fn ProgramIndex::find_command(
  self : ProgramIndex,
  letter : Int,
  code : Double,
) -> Array[CommandOccurrence] {
  let matches : Array[CommandOccurrence] = []
  let upper = ascii_upper(letter)
  for occurrence in self.commands_value {
    if occurrence.letter() == upper && occurrence.code() == code {
      matches.push(occurrence)
    }
  }
  matches
}

///|
pub fn ProgramIndex::usage_for(
  self : ProgramIndex,
  letter : Int,
) -> LetterUsage? {
  let upper = ascii_upper(letter)
  for usage in self.usage_value {
    if usage.letter() == upper {
      return Some(usage)
    }
  }
  None
}

///|
priv struct MutableLetterUsage {
  letter : Int
  mut count : Int
  first_line : Int
  mut last_line : Int
  mut minimum : Double
  mut maximum : Double
}

///|
fn update_usage(
  entries : Array[MutableLetterUsage],
  word : BlockWord,
  line : Int,
) -> Unit {
  for entry in entries {
    if entry.letter == word.letter() {
      entry.count += 1
      entry.last_line = line
      if word.value() < entry.minimum {
        entry.minimum = word.value()
      }
      if word.value() > entry.maximum {
        entry.maximum = word.value()
      }
      return
    }
  }
  entries.push({
    letter: word.letter(),
    count: 1,
    first_line: line,
    last_line: line,
    minimum: word.value(),
    maximum: word.value(),
  })
}

///|
fn sort_usage(entries : Array[MutableLetterUsage]) -> Array[LetterUsage] {
  let result : Array[LetterUsage] = []
  for letter in 65..<91 {
    for entry in entries {
      if entry.letter == letter {
        result.push({
          letter_value: entry.letter,
          count_value: entry.count,
          first_line_value: entry.first_line,
          last_line_value: entry.last_line,
          minimum_value: entry.minimum,
          maximum_value: entry.maximum,
        })
      }
    }
  }
  result
}

///|
pub fn index_document(
  document : GCodeDocument,
  expected_sequence_step? : Int = 0,
) -> ProgramIndex {
  let commands : Array[CommandOccurrence] = []
  let mutable_usage : Array[MutableLetterUsage] = []
  let executable_lines : Array[Int] = []
  let comment_lines : Array[Int] = []
  let gaps : Array[SequenceGap] = []
  let mut previous_sequence : Int? = None
  for block in document.blocks() {
    if block.word_count() > 0 {
      executable_lines.push(block.source_line())
    }
    if block.comments().length() > 0 {
      comment_lines.push(block.source_line())
    }
    for word in block.words() {
      update_usage(mutable_usage, word, block.source_line())
      if word.letter() == 71 || word.letter() == 77 {
        commands.push({
          letter_value: word.letter(),
          code_value: word.value(),
          line_value: block.source_line(),
          column_value: word.column(),
        })
      }
    }
    match (previous_sequence, block.sequence()) {
      (Some(previous), Some(current)) => {
        let actual_step = current - previous
        if expected_sequence_step > 0 && actual_step > expected_sequence_step {
          gaps.push({
            previous_value: previous,
            current_value: current,
            line_value: block.source_line(),
            missing_value: actual_step / expected_sequence_step - 1,
          })
        }
        previous_sequence = Some(current)
      }
      (_, Some(current)) => previous_sequence = Some(current)
      _ => ()
    }
  }
  {
    commands_value: commands,
    usage_value: sort_usage(mutable_usage),
    executable_lines_value: executable_lines,
    comment_lines_value: comment_lines,
    sequence_gaps_value: gaps,
  }
}

///|
pub fn render_program_index(index : ProgramIndex) -> String {
  let mut output = "commands=" + index.command_count().to_string()
  output = output +
    ", executable_lines=" +
    index.executable_lines().length().to_string()
  output = output +
    ", comment_lines=" +
    index.comment_lines().length().to_string()
  output = output +
    ", sequence_gaps=" +
    index.sequence_gaps().length().to_string() +
    "\n"
  for usage in index.usage() {
    output = output + usage.letter().unsafe_to_char().to_string()
    output = output + " count=" + usage.count().to_string()
    output = output +
      " lines=" +
      usage.first_line().to_string() +
      ".." +
      usage.last_line().to_string()
    output = output +
      " range=" +
      usage.minimum().to_string() +
      ".." +
      usage.maximum().to_string() +
      "\n"
  }
  output
}

///|
pub struct ToolSection {
  ordinal_value : Int
  tool_value : Int?
  start_line_value : Int
  end_line_value : Int
  blocks_value : Array[GCodeBlock]
} derive(Eq, Debug)

///|
pub fn ToolSection::ordinal(self : ToolSection) -> Int {
  self.ordinal_value
}

///|
pub fn ToolSection::tool(self : ToolSection) -> Int? {
  self.tool_value
}

///|
pub fn ToolSection::start_line(self : ToolSection) -> Int {
  self.start_line_value
}

///|
pub fn ToolSection::end_line(self : ToolSection) -> Int {
  self.end_line_value
}

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

///|
pub fn ToolSection::document(self : ToolSection) -> GCodeDocument {
  {
    blocks_value: self.blocks_value.copy(),
    physical_lines_value: self.blocks_value.length(),
  }
}

///|
pub fn split_by_tool(document : GCodeDocument) -> Array[ToolSection] {
  let sections : Array[ToolSection] = []
  let mut current_blocks : Array[GCodeBlock] = []
  let mut current_tool : Int? = None
  let mut start_line = 1
  let mut ordinal = 0
  for block in document.blocks() {
    let tool = match block.first_value(84) {
      Some(value) => integer_code(value)
      None => None
    }
    if tool is Some(_) && current_blocks.length() > 0 {
      sections.push({
        ordinal_value: ordinal,
        tool_value: current_tool,
        start_line_value: start_line,
        end_line_value: block.source_line() - 1,
        blocks_value: current_blocks,
      })
      ordinal += 1
      current_blocks = []
      start_line = block.source_line()
    }
    match tool {
      Some(value) => current_tool = Some(value)
      None => ()
    }
    current_blocks.push(block)
  }
  if current_blocks.length() > 0 {
    sections.push({
      ordinal_value: ordinal,
      tool_value: current_tool,
      start_line_value: start_line,
      end_line_value: document.physical_lines(),
      blocks_value: current_blocks,
    })
  }
  sections
}

///|
pub(all) enum BlockDifferenceKind {
  AddedBlock
  RemovedBlock
  ChangedBlock
} derive(Eq, Debug)

///|
pub struct BlockDifference {
  kind_value : BlockDifferenceKind
  line_value : Int
  left_value : String?
  right_value : String?
} derive(Eq, Debug)

///|
pub fn BlockDifference::kind(self : BlockDifference) -> BlockDifferenceKind {
  self.kind_value
}

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

///|
pub fn BlockDifference::left(self : BlockDifference) -> String? {
  self.left_value
}

///|
pub fn BlockDifference::right(self : BlockDifference) -> String? {
  self.right_value
}

///|
pub struct DocumentDifference {
  differences_value : Array[BlockDifference]
  equal_blocks_value : Int
  left_lines_value : Int
  right_lines_value : Int
} derive(Eq, Debug)

///|
pub fn DocumentDifference::differences(
  self : DocumentDifference,
) -> Array[BlockDifference] {
  self.differences_value.copy()
}

///|
pub fn DocumentDifference::equal_blocks(self : DocumentDifference) -> Int {
  self.equal_blocks_value
}

///|
pub fn DocumentDifference::is_equal(self : DocumentDifference) -> Bool {
  self.differences_value.length() == 0
}

///|
pub fn compare_documents(
  left : GCodeDocument,
  right : GCodeDocument,
  ignore_comments? : Bool = false,
) -> DocumentDifference {
  let differences : Array[BlockDifference] = []
  let left_blocks = left.blocks()
  let right_blocks = right.blocks()
  let maximum = if left_blocks.length() > right_blocks.length() {
    left_blocks.length()
  } else {
    right_blocks.length()
  }
  let mut equal = 0
  for index in 0..= left_blocks.length() {
      differences.push({
        kind_value: AddedBlock,
        line_value: index + 1,
        left_value: None,
        right_value: Some(right_blocks[index].source()),
      })
    } else if index >= right_blocks.length() {
      differences.push({
        kind_value: RemovedBlock,
        line_value: index + 1,
        left_value: Some(left_blocks[index].source()),
        right_value: None,
      })
    } else {
      let left_text = if ignore_comments {
        left_blocks[index].body()
      } else {
        left_blocks[index].source()
      }
      let right_text = if ignore_comments {
        right_blocks[index].body()
      } else {
        right_blocks[index].source()
      }
      if left_text == right_text {
        equal += 1
      } else {
        differences.push({
          kind_value: ChangedBlock,
          line_value: index + 1,
          left_value: Some(left_text),
          right_value: Some(right_text),
        })
      }
    }
  }
  {
    differences_value: differences,
    equal_blocks_value: equal,
    left_lines_value: left.physical_lines(),
    right_lines_value: right.physical_lines(),
  }
}

///|
pub fn render_document_difference(difference : DocumentDifference) -> String {
  let mut output = "equal=" + difference.equal_blocks().to_string()
  output = output +
    ", differences=" +
    difference.differences().length().to_string() +
    "\n"
  for item in difference.differences() {
    let kind = match item.kind() {
      AddedBlock => "added"
      RemovedBlock => "removed"
      ChangedBlock => "changed"
    }
    output = output + kind + " line=" + item.line().to_string()
    match item.left() {
      Some(value) => output = output + " left=" + value
      None => ()
    }
    match item.right() {
      Some(value) => output = output + " right=" + value
      None => ()
    }
    output = output + "\n"
  }
  output
}