///|
priv struct ParseResult {
  commands : Array[LocatedCommand]
  issues : Array[Issue]
  issue_command_indices : Array[Int]
}

///|
fn empty_parse_result() -> ParseResult {
  { commands: [], issues: [], issue_command_indices: [] }
}

///|
fn push_cmd(result : ParseResult, command : GerberCommand, line : Int) -> Unit {
  result.commands.push({ command, line })
}

///|
fn push_parse_issue(
  result : ParseResult,
  code : String,
  severity : Severity,
  message : String,
  line : Int,
) -> Unit {
  result.issues.push({ code, severity, message, line: Some(line) })
  result.issue_command_indices.push(result.commands.length() - 1)
}

///|
fn parse_tokens(tokens : Array[RawToken]) -> ParseResult {
  let result = empty_parse_result()
  for token in tokens {
    match token.kind {
      Word => parse_word(result, token)
      ExtendedBlock => parse_extended(result, token)
    }
  }
  result
}

///|
fn parse_word(result : ParseResult, token : RawToken) -> Unit {
  let body = trim_spaces(strip_word_star(token.raw))
  let line = token.line
  if body.length() == 0 {
    push_cmd(result, Malformed(""), line)
    push_parse_issue(
      result,
      "G302",
      Warning,
      "Unsupported Gerber command.",
      line,
    )
    return
  }
  if has_prefix(body, "G04") {
    let content = trim_spaces(slice_str(body, 3, body.length()))
    push_cmd(result, Comment(content), line)
    return
  }
  if body == "G01" {
    push_cmd(result, SetPlotMode(Linear), line)
    return
  }
  if body == "G02" {
    push_cmd(result, SetPlotMode(Clockwise), line)
    return
  }
  if body == "G03" {
    push_cmd(result, SetPlotMode(CounterClockwise), line)
    return
  }
  if body == "G75" {
    push_cmd(result, MultiQuadrant, line)
    return
  }
  if body == "G36" {
    push_cmd(result, BeginRegion, line)
    return
  }
  if body == "G37" {
    push_cmd(result, EndRegion, line)
    return
  }
  if body == "M02" {
    push_cmd(result, EndFile, line)
    return
  }
  // Deprecated combined G/D syntax: G01X...D01 etc.
  if is_deprecated_combined_gd(body) {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G304",
      Warning,
      "Deprecated combined G/D syntax is not supported.",
      line,
    )
    return
  }
  // Deprecated styles G1, g01, G001
  if is_deprecated_g_style(body) {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G304",
      Warning,
      "Deprecated syntax not supported.",
      line,
    )
    return
  }
  // Dnn aperture selection nn >= 10, or D01/D02/D03 operations
  if code_unit_at(body, 0) == 68 {
    // 'D'
    match try_parse_operation_or_select(body) {
      Some(cmd) => {
        push_cmd(result, cmd, line)
        return
      }
      None => ()
    }
    if looks_like_invalid_aperture_code(body) {
      push_cmd(result, Malformed(body), line)
      push_parse_issue(result, "G203", Error, "Invalid aperture code.", line)
      return
    }
  }
  // Coordinate + D0x operation
  if looks_like_operation(body) {
    match parse_operation(body) {
      Ok(cmd) => {
        push_cmd(result, cmd, line)
        return
      }
      Err(msg) => {
        push_cmd(result, Malformed(body), line)
        push_parse_issue(result, "G210", Error, msg, line)
        return
      }
    }
  }
  // Modal operation without D code: X100Y100
  if is_modal_coordinate_only(body) {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G304",
      Warning,
      "Deprecated modal operation syntax is not supported.",
      line,
    )
    return
  }
  push_cmd(result, UnknownWord(body), line)
}

///|
fn parse_extended(result : ParseResult, token : RawToken) -> Unit {
  let inner = strip_extended(token.raw)
  let body = trim_spaces(inner)
  let line = token.line
  if body.length() == 0 {
    push_cmd(result, Malformed("%"), line)
    push_parse_issue(
      result,
      "G302",
      Warning,
      "Unsupported Gerber command.",
      line,
    )
    return
  }
  if has_prefix(body, "MO") {
    parse_unit(result, body, line)
    return
  }
  if has_prefix(body, "FS") {
    parse_format(result, body, line)
    return
  }
  if has_prefix(body, "ADD") {
    parse_ad(result, body, line)
    return
  }
  if has_prefix(body, "AM") {
    parse_am(result, inner, body, line)
    return
  }
  if has_prefix(body, "LP") {
    parse_lp(result, body, line)
    return
  }
  if has_prefix(body, "TF") || has_prefix(body, "TA") || has_prefix(body, "TO") {
    parse_attribute(result, body, line)
    return
  }
  if has_prefix(body, "TD") {
    parse_td(result, body, line)
    return
  }
  push_cmd(result, UnknownExtended(body), line)
}

///|
fn parse_unit(result : ParseResult, body : String, line : Int) -> Unit {
  if body == "MOMM" {
    push_cmd(result, Unit(Millimeter), line)
  } else if body == "MOIN" {
    push_cmd(result, Unit(Inch), line)
  } else {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(result, "G113", Error, "Malformed unit statement.", line)
  }
}

///|
fn parse_format(result : ParseResult, body : String, line : Int) -> Unit {
  if has_prefix(body, "FSTA") || has_prefix(body, "FSLI") {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G124",
      Error,
      "Unsupported or deprecated format variant.",
      line,
    )
    return
  }
  // Expect FSLAXn6Yn6
  if !has_prefix(body, "FSLA") {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G123",
      Error,
      "Malformed format specification.",
      line,
    )
    return
  }
  let rest = slice_str(body, 4, body.length())
  // XnnYnn
  if rest.length() != 6 {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G123",
      Error,
      "Malformed format specification.",
      line,
    )
    return
  }
  if code_unit_at(rest, 0) != 88 || code_unit_at(rest, 3) != 89 {
    // X ... Y
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G123",
      Error,
      "Malformed format specification.",
      line,
    )
    return
  }
  let xi = code_unit_at(rest, 1)
  let xd = code_unit_at(rest, 2)
  let yi = code_unit_at(rest, 4)
  let yd = code_unit_at(rest, 5)
  if !(is_ascii_digit(xi) &&
    is_ascii_digit(xd) &&
    is_ascii_digit(yi) &&
    is_ascii_digit(yd)) {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G123",
      Error,
      "Malformed format specification.",
      line,
    )
    return
  }
  let x_int = xi - 48
  let x_dec = xd - 48
  let y_int = yi - 48
  let y_dec = yd - 48
  if x_int < 1 ||
    x_int > 6 ||
    y_int < 1 ||
    y_int > 6 ||
    x_dec != 6 ||
    y_dec != 6 ||
    x_int != y_int ||
    x_dec != y_dec {
    push_cmd(result, Malformed(body), line)
    push_parse_issue(
      result,
      "G123",
      Error,
      "Malformed format specification.",
      line,
    )
    return
  }
  push_cmd(
    result,
    Format({ integer_digits: x_int, decimal_digits: x_dec }),
    line,
  )
}

///|
fn parse_am(
  result : ParseResult,
  inner : String,
  header_and_body : String,
  line : Int,
) -> Unit {
  // AMNAME*body...  (inner may contain * and newlines)
  let after_am = slice_str(header_and_body, 2, header_and_body.length())
  let mut name_end = 0
  while name_end < after_am.length() {
    let c = code_unit_at(after_am, name_end)
    if c == 42 {
      break
    }
    name_end = name_end + 1
  }
  let name = slice_str(after_am, 0, name_end)
  if name.length() == 0 || !is_valid_macro_name(name) {
    push_cmd(result, Malformed(header_and_body), line)
    push_parse_issue(
      result,
      "G204",
      Error,
      "Malformed aperture definition.",
      line,
    )
    return
  }
  // Prefer raw body from original inner after AMNAME*
  let raw_body = extract_am_body(inner, name)
  push_cmd(result, ApertureMacro(name, raw_body), line)
}

///|
fn extract_am_body(inner : String, name : String) -> String {
  // inner like: AMNAME*\n1,1,$1...\n or without newlines
  let prefix = "AM" + name
  let mut header_start = 0
  while header_start < inner.length() {
    let c = code_unit_at(inner, header_start)
    if c == 32 || c == 9 || c == 10 || c == 13 {
      header_start = header_start + 1
    } else {
      break
    }
  }
  if header_start + prefix.length() <= inner.length() &&
    slice_str(inner, header_start, header_start + prefix.length()) == prefix {
    let start_search = header_start + prefix.length()
    let mut i = start_search
    while i < inner.length() {
      if code_unit_at(inner, i) == 42 {
        return slice_str(inner, i + 1, inner.length())
      }
      i = i + 1
    }
  }
  ""
}

///|
fn is_valid_macro_name(name : String) -> Bool {
  if name.length() == 0 {
    return false
  }
  let mut i = 0
  while i < name.length() {
    let c = code_unit_at(name, i)
    let ok = (c >= 65 && c <= 90) ||
      (c >= 97 && c <= 122) ||
      (c >= 48 && c <= 57) ||
      c == 95 ||
      c == 46 ||
      c == 36
    if !ok {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn parse_ad(result : ParseResult, body : String, line : Int) -> Unit {
  // ADD