///|
pub(all) enum ControllerFamily {
  GenericCnc
  GrblCompatible
  MarlinCompatible
} derive(Eq, Debug)

///|
pub struct ValidationProfile {
  name_value : String
  family_value : ControllerFamily
  allowed_g_codes_value : Array[Double]
  allowed_m_codes_value : Array[Int]
  allowed_letters_value : Array[Int]
  max_x_value : Double
  max_y_value : Double
  max_z_value : Double
  max_feed_value : Double
  max_spindle_value : Double
  max_tool_value : Int
  require_sequence_value : Bool
  require_checksum_value : Bool
  require_monotonic_sequence_value : Bool
  allow_extrusion_value : Bool
  allow_arcs_value : Bool
} derive(Eq, Debug)

///|
pub fn ValidationProfile::generic_cnc() -> ValidationProfile {
  {
    name_value: "generic-cnc",
    family_value: GenericCnc,
    allowed_g_codes_value: [
      0.0, 1.0, 2.0, 3.0, 17.0, 18.0, 19.0, 20.0, 21.0, 90.0, 91.0, 92.0, 92.1,
    ],
    allowed_m_codes_value: [3, 4, 5, 6, 7, 8, 9],
    allowed_letters_value: [
      65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84,
      85, 86, 87, 88, 89, 90,
    ],
    max_x_value: 1000.0,
    max_y_value: 1000.0,
    max_z_value: 500.0,
    max_feed_value: 50000.0,
    max_spindle_value: 100000.0,
    max_tool_value: 999,
    require_sequence_value: false,
    require_checksum_value: false,
    require_monotonic_sequence_value: false,
    allow_extrusion_value: false,
    allow_arcs_value: true,
  }
}

///|
pub fn ValidationProfile::grbl() -> ValidationProfile {
  {
    ..ValidationProfile::generic_cnc(),
    name_value: "grbl-common-subset",
    family_value: GrblCompatible,
    allowed_m_codes_value: [3, 4, 5, 7, 8, 9],
    allowed_letters_value: [
      65, 66, 67, 70, 71, 73, 74, 75, 76, 77, 78, 80, 82, 83, 84, 88, 89, 90,
    ],
    max_x_value: 500.0,
    max_y_value: 500.0,
    max_z_value: 200.0,
    max_feed_value: 30000.0,
    max_spindle_value: 30000.0,
    max_tool_value: 0,
  }
}

///|
pub fn ValidationProfile::marlin() -> ValidationProfile {
  {
    ..ValidationProfile::generic_cnc(),
    name_value: "marlin-common-subset",
    family_value: MarlinCompatible,
    allowed_m_codes_value: [3, 4, 5, 6, 7, 8, 9, 82, 83],
    max_x_value: 500.0,
    max_y_value: 500.0,
    max_z_value: 500.0,
    max_feed_value: 60000.0,
    max_spindle_value: 100000.0,
    max_tool_value: 31,
    allow_extrusion_value: true,
  }
}

///|
pub fn ValidationProfile::name(self : ValidationProfile) -> String {
  self.name_value
}

///|
pub fn ValidationProfile::family(self : ValidationProfile) -> ControllerFamily {
  self.family_value
}

///|
pub fn ValidationProfile::with_workspace(
  self : ValidationProfile,
  max_x : Double,
  max_y : Double,
  max_z : Double,
) -> ValidationProfile {
  { ..self, max_x_value: max_x, max_y_value: max_y, max_z_value: max_z }
}

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

///|
pub fn ValidationProfile::with_max_spindle(
  self : ValidationProfile,
  max_spindle : Double,
) -> ValidationProfile {
  { ..self, max_spindle_value: max_spindle }
}

///|
pub fn ValidationProfile::with_sequence_policy(
  self : ValidationProfile,
  required : Bool,
  monotonic : Bool,
) -> ValidationProfile {
  {
    ..self,
    require_sequence_value: required,
    require_monotonic_sequence_value: monotonic,
  }
}

///|
pub fn ValidationProfile::with_checksum_required(
  self : ValidationProfile,
  required : Bool,
) -> ValidationProfile {
  { ..self, require_checksum_value: required }
}

///|
pub fn ValidationProfile::with_arcs_allowed(
  self : ValidationProfile,
  allowed : Bool,
) -> ValidationProfile {
  { ..self, allow_arcs_value: allowed }
}

///|
pub struct ValidationSummary {
  physical_lines_value : Int
  executable_blocks_value : Int
  checked_words_value : Int
  error_count_value : Int
  warning_count_value : Int
  first_sequence_value : Int?
  last_sequence_value : Int?
} derive(Eq, Debug)

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

///|
pub fn ValidationSummary::executable_blocks(self : ValidationSummary) -> Int {
  self.executable_blocks_value
}

///|
pub fn ValidationSummary::checked_words(self : ValidationSummary) -> Int {
  self.checked_words_value
}

///|
pub fn ValidationSummary::error_count(self : ValidationSummary) -> Int {
  self.error_count_value
}

///|
pub fn ValidationSummary::warning_count(self : ValidationSummary) -> Int {
  self.warning_count_value
}

///|
pub fn ValidationSummary::first_sequence(self : ValidationSummary) -> Int? {
  self.first_sequence_value
}

///|
pub fn ValidationSummary::last_sequence(self : ValidationSummary) -> Int? {
  self.last_sequence_value
}

///|
pub struct DocumentValidation {
  profile_value : ValidationProfile
  diagnostics_value : Array[Diagnostic]
  summary_value : ValidationSummary
  state_trace_value : StateTraceReport
} derive(Eq, Debug)

///|
pub fn DocumentValidation::profile(
  self : DocumentValidation,
) -> ValidationProfile {
  self.profile_value
}

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

///|
pub fn DocumentValidation::summary(
  self : DocumentValidation,
) -> ValidationSummary {
  self.summary_value
}

///|
pub fn DocumentValidation::state_trace(
  self : DocumentValidation,
) -> StateTraceReport {
  self.state_trace_value
}

///|
pub fn DocumentValidation::accepted(self : DocumentValidation) -> Bool {
  self.summary_value.error_count() == 0
}

///|
fn contains_int(values : Array[Int], expected : Int) -> Bool {
  for value in values {
    if value == expected {
      return true
    }
  }
  false
}

///|
fn contains_double(values : Array[Double], expected : Double) -> Bool {
  for value in values {
    if value == expected {
      return true
    }
  }
  false
}

///|
fn profile_issue(
  diagnostics : Array[Diagnostic],
  severity : DiagnosticSeverity,
  code : String,
  message : String,
  line : Int,
) -> Unit {
  diagnostics.push({
    severity_value: severity,
    code_value: code,
    message_value: message,
    line_value: line,
  })
}

///|
fn validate_word_letter(
  word : BlockWord,
  block : GCodeBlock,
  profile : ValidationProfile,
  diagnostics : Array[Diagnostic],
) -> Unit {
  if !contains_int(profile.allowed_letters_value, word.letter()) {
    profile_issue(
      diagnostics,
      Error,
      "word.not_allowed",
      "letter " + word.letter_text() + " is not allowed by profile",
      block.source_line(),
    )
  }
}

///|
fn validate_g_code(
  value : Double,
  block : GCodeBlock,
  profile : ValidationProfile,
  diagnostics : Array[Diagnostic],
) -> Unit {
  if !contains_double(profile.allowed_g_codes_value, value) {
    profile_issue(
      diagnostics,
      Error,
      "command.g_not_allowed",
      "G" + value.to_string() + " is outside the declared profile subset",
      block.source_line(),
    )
  }
  if !profile.allow_arcs_value && (value == 2.0 || value == 3.0) {
    profile_issue(
      diagnostics,
      Error,
      "command.arc_disabled",
      "arc commands are disabled by profile",
      block.source_line(),
    )
  }
}

///|
fn validate_m_code(
  value : Double,
  block : GCodeBlock,
  profile : ValidationProfile,
  diagnostics : Array[Diagnostic],
) -> Unit {
  match integer_code(value) {
    Some(code) =>
      if !contains_int(profile.allowed_m_codes_value, code) {
        profile_issue(
          diagnostics,
          Error,
          "command.m_not_allowed",
          "M" + code.to_string() + " is outside the declared profile subset",
          block.source_line(),
        )
      }
    None =>
      profile_issue(
        diagnostics,
        Error,
        "command.m_fractional",
        "M code must be an integer",
        block.source_line(),
      )
  }
}

///|
fn validate_arc_words(
  block : GCodeBlock,
  diagnostics : Array[Diagnostic],
) -> Unit {
  let mut arc = false
  for value in block.values_for(71) {
    if value == 2.0 || value == 3.0 {
      arc = true
    }
  }
  if !arc {
    return
  }
  let has_offsets = block.has_letter(73) ||
    block.has_letter(74) ||
    block.has_letter(75)
  let has_radius = block.has_letter(82)
  if !has_offsets && !has_radius {
    profile_issue(
      diagnostics,
      Error,
      "arc.geometry_missing",
      "arc requires I/J/K offsets or R radius",
      block.source_line(),
    )
  }
  if has_offsets && has_radius {
    profile_issue(
      diagnostics,
      Error,
      "arc.geometry_ambiguous",
      "arc cannot mix center offsets and radius",
      block.source_line(),
    )
  }
}

///|
fn validate_scalar_ranges(
  block : GCodeBlock,
  profile : ValidationProfile,
  diagnostics : Array[Diagnostic],
) -> Unit {
  match block.first_value(70) {
    Some(value) =>
      if value < 0.0 {
        profile_issue(
          diagnostics,
          Error,
          "feed.negative",
          "feed must not be negative",
          block.source_line(),
        )
      } else if value > profile.max_feed_value {
        profile_issue(
          diagnostics,
          Error,
          "feed.above_profile",
          "feed exceeds profile maximum",
          block.source_line(),
        )
      }
    None => ()
  }
  match block.first_value(83) {
    Some(value) =>
      if value < 0.0 {
        profile_issue(
          diagnostics,
          Error,
          "spindle.negative",
          "spindle speed must not be negative",
          block.source_line(),
        )
      } else if value > profile.max_spindle_value {
        profile_issue(
          diagnostics,
          Error,
          "spindle.above_profile",
          "spindle speed exceeds profile maximum",
          block.source_line(),
        )
      }
    None => ()
  }
  match block.first_value(84) {
    Some(value) =>
      match integer_code(value) {
        Some(tool) if tool >= 0 && tool <= profile.max_tool_value => ()
        _ =>
          profile_issue(
            diagnostics,
            Error,
            "tool.out_of_range",
            "tool number exceeds profile range",
            block.source_line(),
          )
      }
    None => ()
  }
  if !profile.allow_extrusion_value && block.has_letter(69) {
    profile_issue(
      diagnostics,
      Error,
      "word.extrusion_disabled",
      "E word is not allowed by this profile",
      block.source_line(),
    )
  }
}

///|
fn validate_sequence(
  block : GCodeBlock,
  profile : ValidationProfile,
  previous : Int?,
  diagnostics : Array[Diagnostic],
) -> Int? {
  if block.word_count() == 0 {
    return previous
  }
  let sequence = block.sequence()
  if profile.require_sequence_value && sequence is None {
    profile_issue(
      diagnostics,
      Error,
      "sequence.required",
      "executable block requires N sequence",
      block.source_line(),
    )
  }
  if profile.require_checksum_value && block.checksum() is None {
    profile_issue(
      diagnostics,
      Error,
      "checksum.required",
      "executable block requires checksum",
      block.source_line(),
    )
  }
  if !block.checksum_valid() {
    profile_issue(
      diagnostics,
      Error,
      "checksum.invalid",
      "checksum does not match block source",
      block.source_line(),
    )
  }
  match (previous, sequence) {
    (Some(old), Some(current)) if profile.require_monotonic_sequence_value &&
      current <= old =>
      profile_issue(
        diagnostics,
        Error,
        "sequence.not_monotonic",
        "sequence number must increase",
        block.source_line(),
      )
    _ => ()
  }
  match sequence {
    Some(value) => Some(value)
    None => previous
  }
}

///|
fn validate_state_workspace(
  trace : StateTraceReport,
  profile : ValidationProfile,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for snapshot in trace.snapshots() {
    let position = snapshot.after().position()
    if position.x() < 0.0 || position.x() > profile.max_x_value {
      profile_issue(
        diagnostics,
        Error,
        "axis.x_out_of_bounds",
        "X position exceeds profile workspace",
        snapshot.line(),
      )
    }
    if position.y() < 0.0 || position.y() > profile.max_y_value {
      profile_issue(
        diagnostics,
        Error,
        "axis.y_out_of_bounds",
        "Y position exceeds profile workspace",
        snapshot.line(),
      )
    }
    if position.z() < 0.0 || position.z() > profile.max_z_value {
      profile_issue(
        diagnostics,
        Error,
        "axis.z_out_of_bounds",
        "Z position exceeds profile workspace",
        snapshot.line(),
      )
    }
  }
}

///|
pub fn validate_document(
  document : GCodeDocument,
  profile? : ValidationProfile = ValidationProfile::generic_cnc(),
) -> DocumentValidation {
  let diagnostics : Array[Diagnostic] = []
  let state_trace = interpret_modal_state(document)
  for issue in state_trace.diagnostics() {
    diagnostics.push(issue)
  }
  let mut checked_words = 0
  let mut previous_sequence : Int? = None
  let mut first_sequence : Int? = None
  let mut last_sequence : Int? = None
  for block in document.blocks() {
    previous_sequence = validate_sequence(
      block, profile, previous_sequence, diagnostics,
    )
    match block.sequence() {
      Some(value) => {
        if first_sequence is None {
          first_sequence = Some(value)
        }
        last_sequence = Some(value)
      }
      None => ()
    }
    for word in block.words() {
      checked_words += 1
      validate_word_letter(word, block, profile, diagnostics)
      if word.letter() == 71 {
        validate_g_code(word.value(), block, profile, diagnostics)
      } else if word.letter() == 77 {
        validate_m_code(word.value(), block, profile, diagnostics)
      }
    }
    validate_arc_words(block, diagnostics)
    validate_scalar_ranges(block, profile, diagnostics)
  }
  validate_state_workspace(state_trace, profile, diagnostics)
  let mut errors = 0
  let mut warnings = 0
  for issue in diagnostics {
    match issue.severity() {
      Error => errors += 1
      Warning => warnings += 1
    }
  }
  {
    profile_value: profile,
    diagnostics_value: diagnostics,
    summary_value: {
      physical_lines_value: document.physical_lines(),
      executable_blocks_value: document.executable_block_count(),
      checked_words_value: checked_words,
      error_count_value: errors,
      warning_count_value: warnings,
      first_sequence_value: first_sequence,
      last_sequence_value: last_sequence,
    },
    state_trace_value: state_trace,
  }
}

///|
pub fn render_validation(validation : DocumentValidation) -> String {
  let summary = validation.summary()
  let mut output = "profile=" + validation.profile().name()
  output = output + ", accepted=" + validation.accepted().to_string()
  output = output + ", lines=" + summary.physical_lines().to_string()
  output = output + ", blocks=" + summary.executable_blocks().to_string()
  output = output + ", words=" + summary.checked_words().to_string()
  output = output + ", errors=" + summary.error_count().to_string()
  output = output + ", warnings=" + summary.warning_count().to_string() + "\n"
  output + render_diagnostics(validation.diagnostics())
}