///|
pub(all) enum CommentRewriteMode {
  PreserveComments
  DropComments
} derive(Eq, Debug)

///|
pub(all) enum BlankLineRewriteMode {
  PreserveBlankLines
  DropBlankLines
} derive(Eq, Debug)

///|
pub(all) enum SequenceRewriteMode {
  KeepSequences
  RemoveSequences
  RenumberSequences(Int, Int)
} derive(Eq, Debug)

///|
pub(all) enum ChecksumRewriteMode {
  RemoveChecksums
  PreserveChecksumValues
  RecomputeExistingChecksums
  AddChecksums
} derive(Eq, Debug)

///|
pub struct ToolMapping {
  from_value : Int
  to_value : Int
} derive(Eq, Debug)

///|
pub fn ToolMapping::from(self : ToolMapping) -> Int {
  self.from_value
}

///|
pub fn ToolMapping::to(self : ToolMapping) -> Int {
  self.to_value
}

///|
pub struct DocumentRewritePlan {
  comment_mode_value : CommentRewriteMode
  blank_mode_value : BlankLineRewriteMode
  sequence_mode_value : SequenceRewriteMode
  checksum_mode_value : ChecksumRewriteMode
  feed_multiplier_value : Double
  spindle_multiplier_value : Double
  tool_mappings_value : Array[ToolMapping]
  include_final_newline_value : Bool
} derive(Eq, Debug)

///|
pub fn DocumentRewritePlan::canonical() -> DocumentRewritePlan {
  {
    comment_mode_value: PreserveComments,
    blank_mode_value: PreserveBlankLines,
    sequence_mode_value: KeepSequences,
    checksum_mode_value: RecomputeExistingChecksums,
    feed_multiplier_value: 1.0,
    spindle_multiplier_value: 1.0,
    tool_mappings_value: [],
    include_final_newline_value: false,
  }
}

///|
pub fn DocumentRewritePlan::with_comments(
  self : DocumentRewritePlan,
  mode : CommentRewriteMode,
) -> DocumentRewritePlan {
  { ..self, comment_mode_value: mode }
}

///|
pub fn DocumentRewritePlan::with_blank_lines(
  self : DocumentRewritePlan,
  mode : BlankLineRewriteMode,
) -> DocumentRewritePlan {
  { ..self, blank_mode_value: mode }
}

///|
pub fn DocumentRewritePlan::with_sequences(
  self : DocumentRewritePlan,
  mode : SequenceRewriteMode,
) -> DocumentRewritePlan {
  { ..self, sequence_mode_value: mode }
}

///|
pub fn DocumentRewritePlan::with_checksums(
  self : DocumentRewritePlan,
  mode : ChecksumRewriteMode,
) -> DocumentRewritePlan {
  { ..self, checksum_mode_value: mode }
}

///|
pub fn DocumentRewritePlan::with_feed_multiplier(
  self : DocumentRewritePlan,
  multiplier : Double,
) -> Result[DocumentRewritePlan, GCodeError] {
  if multiplier <= 0.0 {
    return Err(make_error(InvalidNumber, "feed multiplier must be positive", 0))
  }
  Ok({ ..self, feed_multiplier_value: multiplier })
}

///|
pub fn DocumentRewritePlan::with_spindle_multiplier(
  self : DocumentRewritePlan,
  multiplier : Double,
) -> Result[DocumentRewritePlan, GCodeError] {
  if multiplier <= 0.0 {
    return Err(
      make_error(InvalidNumber, "spindle multiplier must be positive", 0),
    )
  }
  Ok({ ..self, spindle_multiplier_value: multiplier })
}

///|
pub fn DocumentRewritePlan::with_tool_mapping(
  self : DocumentRewritePlan,
  from : Int,
  to : Int,
) -> Result[DocumentRewritePlan, GCodeError] {
  if from < 0 || to < 0 {
    return Err(
      make_error(InvalidNumber, "tool mapping must be non-negative", 0),
    )
  }
  let mappings = self.tool_mappings_value.copy()
  for mapping in mappings {
    if mapping.from() == from {
      return Err(
        make_error(InvalidWord, "tool mapping source is duplicated", 0),
      )
    }
  }
  mappings.push({ from_value: from, to_value: to })
  Ok({ ..self, tool_mappings_value: mappings })
}

///|
pub fn DocumentRewritePlan::with_final_newline(
  self : DocumentRewritePlan,
  enabled : Bool,
) -> DocumentRewritePlan {
  { ..self, include_final_newline_value: enabled }
}

///|
pub struct RewriteLineRecord {
  source_line_value : Int
  output_line_value : Int?
  original_value : String
  rewritten_value : String?
  changes_value : Array[String]
} derive(Eq, Debug)

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

///|
pub fn RewriteLineRecord::output_line(self : RewriteLineRecord) -> Int? {
  self.output_line_value
}

///|
pub fn RewriteLineRecord::original(self : RewriteLineRecord) -> String {
  self.original_value
}

///|
pub fn RewriteLineRecord::rewritten(self : RewriteLineRecord) -> String? {
  self.rewritten_value
}

///|
pub fn RewriteLineRecord::changes(self : RewriteLineRecord) -> Array[String] {
  self.changes_value.copy()
}

///|
pub fn RewriteLineRecord::changed(self : RewriteLineRecord) -> Bool {
  match self.rewritten_value {
    Some(value) => value != self.original_value
    None => true
  }
}

///|
pub struct DocumentRewriteResult {
  text_value : String
  records_value : Array[RewriteLineRecord]
  changed_lines_value : Int
  omitted_lines_value : Int
  diagnostics_value : Array[Diagnostic]
} derive(Eq, Debug)

///|
pub fn DocumentRewriteResult::text(self : DocumentRewriteResult) -> String {
  self.text_value
}

///|
pub fn DocumentRewriteResult::records(
  self : DocumentRewriteResult,
) -> Array[RewriteLineRecord] {
  self.records_value.copy()
}

///|
pub fn DocumentRewriteResult::changed_lines(
  self : DocumentRewriteResult,
) -> Int {
  self.changed_lines_value
}

///|
pub fn DocumentRewriteResult::omitted_lines(
  self : DocumentRewriteResult,
) -> Int {
  self.omitted_lines_value
}

///|
pub fn DocumentRewriteResult::diagnostics(
  self : DocumentRewriteResult,
) -> Array[Diagnostic] {
  self.diagnostics_value.copy()
}

///|
fn mapped_tool(mappings : Array[ToolMapping], original : Double) -> Double {
  match integer_code(original) {
    Some(tool) => {
      for mapping in mappings {
        if mapping.from() == tool {
          return mapping.to().to_double()
        }
      }
      original
    }
    None => original
  }
}

///|
fn rewritten_word_value(word : BlockWord, plan : DocumentRewritePlan) -> Double {
  if word.letter() == 70 {
    word.value() * plan.feed_multiplier_value
  } else if word.letter() == 83 {
    word.value() * plan.spindle_multiplier_value
  } else if word.letter() == 84 {
    mapped_tool(plan.tool_mappings_value, word.value())
  } else {
    word.value()
  }
}

///|
fn canonical_number(value : Double) -> String {
  if value == 0.0 {
    "0"
  } else {
    value.to_string()
  }
}

///|
fn canonical_word(word : BlockWord, value : Double) -> String {
  word.letter_text() + canonical_number(value)
}

///|
fn rewritten_sequence(
  block : GCodeBlock,
  plan : DocumentRewritePlan,
  executable_ordinal : Int,
) -> Int? {
  match plan.sequence_mode_value {
    KeepSequences => block.sequence()
    RemoveSequences => None
    RenumberSequences(start, step) =>
      if block.word_count() > 0 {
        Some(start + executable_ordinal * step)
      } else {
        None
      }
  }
}

///|
fn render_rewrite_body(
  block : GCodeBlock,
  plan : DocumentRewritePlan,
  sequence : Int?,
  changes : Array[String],
) -> String {
  let mut output = ""
  match sequence {
    Some(value) => output = "N" + value.to_string()
    None => ()
  }
  for word in block.words() {
    if word.letter() == 78 {
      continue
    }
    let value = rewritten_word_value(word, plan)
    if output != "" {
      output = output + " "
    }
    output = output + canonical_word(word, value)
    if value != word.value() {
      if word.letter() == 70 {
        changes.push("feed scaled")
      } else if word.letter() == 83 {
        changes.push("spindle scaled")
      } else if word.letter() == 84 {
        changes.push("tool mapped")
      }
    }
  }
  if plan.comment_mode_value == PreserveComments {
    for comment in block.comments() {
      if output != "" {
        output = output + " "
      }
      output = output + "(" + comment + ")"
    }
  } else if block.comments().length() > 0 {
    changes.push("comments removed")
  }
  output
}

///|
fn append_checksum(
  body : String,
  block : GCodeBlock,
  mode : ChecksumRewriteMode,
  changes : Array[String],
  diagnostics : Array[Diagnostic],
) -> String {
  match mode {
    RemoveChecksums => {
      if block.checksum() is Some(_) {
        changes.push("checksum removed")
      }
      body
    }
    PreserveChecksumValues =>
      match block.checksum() {
        Some(value) => {
          if checksum_for_text(body) != value {
            diagnostics.push({
              severity_value: Warning,
              code_value: "rewrite.checksum_stale",
              message_value: "preserved checksum no longer matches normalized block",
              line_value: block.source_line(),
            })
          }
          body + "*" + value.to_string()
        }
        None => body
      }
    RecomputeExistingChecksums =>
      match block.checksum() {
        Some(value) => {
          let computed = checksum_for_text(body)
          if computed != value {
            changes.push("checksum recomputed")
          }
          body + "*" + computed.to_string()
        }
        None => body
      }
    AddChecksums =>
      if body == "" {
        body
      } else {
        if block.checksum() is None {
          changes.push("checksum added")
        } else if block.checksum() != Some(checksum_for_text(body)) {
          changes.push("checksum recomputed")
        }
        body + "*" + checksum_for_text(body).to_string()
      }
  }
}

///|
fn sequence_change_label(
  block : GCodeBlock,
  sequence : Int?,
  changes : Array[String],
) -> Unit {
  if sequence != block.sequence() {
    match (block.sequence(), sequence) {
      (None, Some(_)) => changes.push("sequence added")
      (Some(_), None) => changes.push("sequence removed")
      (Some(_), Some(_)) => changes.push("sequence changed")
      _ => ()
    }
  }
}

///|
fn should_omit_block(block : GCodeBlock, plan : DocumentRewritePlan) -> Bool {
  let blank = block.word_count() == 0 && block.comments().length() == 0
  let comment_only = block.word_count() == 0 && block.comments().length() > 0
  (blank && plan.blank_mode_value == DropBlankLines) ||
  (comment_only && plan.comment_mode_value == DropComments)
}

///|
pub fn rewrite_document(
  document : GCodeDocument,
  plan? : DocumentRewritePlan = DocumentRewritePlan::canonical(),
) -> Result[DocumentRewriteResult, GCodeError] {
  match plan.sequence_mode_value {
    RenumberSequences(start, step) =>
      if start < 0 || step <= 0 {
        return Err(
          make_error(
            InvalidNumber,
            "renumber start must be non-negative and step positive",
            0,
          ),
        )
      }
    _ => ()
  }
  let records : Array[RewriteLineRecord] = []
  let output_lines : Array[String] = []
  let diagnostics : Array[Diagnostic] = []
  let mut executable_ordinal = 0
  let mut changed = 0
  let mut omitted = 0
  for block in document.blocks() {
    if should_omit_block(block, plan) {
      records.push({
        source_line_value: block.source_line(),
        output_line_value: None,
        original_value: block.source(),
        rewritten_value: None,
        changes_value: ["line omitted"],
      })
      changed += 1
      omitted += 1
      continue
    }
    let changes : Array[String] = []
    let sequence = rewritten_sequence(block, plan, executable_ordinal)
    if block.word_count() > 0 {
      executable_ordinal += 1
    }
    sequence_change_label(block, sequence, changes)
    let body = render_rewrite_body(block, plan, sequence, changes)
    let line = append_checksum(
      body,
      block,
      plan.checksum_mode_value,
      changes,
      diagnostics,
    )
    if line != block.source() {
      if changes.length() == 0 {
        changes.push("lexical normalization")
      }
      changed += 1
    }
    output_lines.push(line)
    records.push({
      source_line_value: block.source_line(),
      output_line_value: Some(output_lines.length()),
      original_value: block.source(),
      rewritten_value: Some(line),
      changes_value: changes,
    })
  }
  let mut text = output_lines.join("\n")
  if plan.include_final_newline_value {
    text = text + "\n"
  }
  Ok({
    text_value: text,
    records_value: records,
    changed_lines_value: changed,
    omitted_lines_value: omitted,
    diagnostics_value: diagnostics,
  })
}

///|
pub fn render_rewrite_report(result : DocumentRewriteResult) -> String {
  let mut output = "records=" + result.records().length().to_string()
  output = output + ", changed=" + result.changed_lines().to_string()
  output = output + ", omitted=" + result.omitted_lines().to_string()
  output = output +
    ", diagnostics=" +
    result.diagnostics().length().to_string() +
    "\n"
  for record in result.records() {
    if record.changed() {
      output = output + "source=" + record.source_line().to_string()
      match record.output_line() {
        Some(line) => output = output + " output=" + line.to_string()
        None => output = output + " output=omitted"
      }
      output = output + " changes=" + record.changes().join(",") + "\n"
    }
  }
  output
}