///|
priv struct ScanResult {
  tokens : Array[RawToken]
  issues : Array[Issue]
  mut fatal : Bool
}

///|
fn empty_scan_result() -> ScanResult {
  { tokens: [], issues: [], fatal: false }
}

///|
fn advance_line(ch : Int, next : Int?, line : Ref[Int]) -> Bool {
  if ch == 13 {
    line.val = line.val + 1
    match next {
      Some(10) => true
      _ => false
    }
  } else if ch == 10 {
    line.val = line.val + 1
    false
  } else {
    false
  }
}

///|
fn is_newline(ch : Int) -> Bool {
  ch == 10 || ch == 13
}

///|
fn scan_source(source : String) -> ScanResult {
  let result = empty_scan_result()
  if exceeds_utf8_byte_limit(source, MAX_INPUT_BYTES) {
    result.issues.push({
      code: "G001",
      severity: Error,
      message: "Input file exceeds 64 MiB limit.",
      line: None,
    })
    result.fatal = true
    return result
  }
  let line = Ref(1)
  let mut pos = 0
  let len = source.length()
  while pos < len {
    let ch = code_unit_at(source, pos)
    let next = if pos + 1 < len {
      Some(code_unit_at(source, pos + 1))
    } else {
      None
    }
    if is_newline(ch) {
      let skip_next = advance_line(ch, next, line)
      pos = pos + 1
      if skip_next {
        pos = pos + 1
      }
      continue
    }
    if ch == 37 {
      let start_line = line.val
      let start = pos
      pos = pos + 1
      let mut closed = false
      while pos < len {
        let c = code_unit_at(source, pos)
        let n = if pos + 1 < len {
          Some(code_unit_at(source, pos + 1))
        } else {
          None
        }
        if is_newline(c) {
          let skip_next = advance_line(c, n, line)
          pos = pos + 1
          if skip_next {
            pos = pos + 1
          }
          continue
        }
        if c == 37 {
          pos = pos + 1
          closed = true
          break
        }
        pos = pos + 1
      }
      if !closed {
        result.issues.push({
          code: "G102",
          severity: Error,
          message: "Unterminated extended block.",
          line: Some(start_line),
        })
        result.fatal = true
        return result
      }
      let raw = slice_str(source, start, pos)
      if exceeds_utf8_byte_limit(raw, MAX_EXTENDED_BLOCK_BYTES) {
        result.issues.push({
          code: "G103",
          severity: Error,
          message: "Command exceeds configured size limit.",
          line: Some(start_line),
        })
        result.fatal = true
        return result
      }
      result.tokens.push({ kind: ExtendedBlock, raw, line: start_line })
      if result.tokens.length() > MAX_TOKEN_COUNT {
        result.issues.push({
          code: "G002",
          severity: Error,
          message: "Command count exceeds 1,000,000.",
          line: Some(start_line),
        })
        result.fatal = true
        return result
      }
    } else {
      let start_line = line.val
      let start = pos
      let mut closed = false
      while pos < len {
        let c = code_unit_at(source, pos)
        let n = if pos + 1 < len {
          Some(code_unit_at(source, pos + 1))
        } else {
          None
        }
        if is_newline(c) {
          let skip_next = advance_line(c, n, line)
          pos = pos + 1
          if skip_next {
            pos = pos + 1
          }
          continue
        }
        if c == 42 {
          pos = pos + 1
          closed = true
          break
        }
        pos = pos + 1
      }
      if !closed {
        result.issues.push({
          code: "G101",
          severity: Error,
          message: "Unterminated word command.",
          line: Some(start_line),
        })
        result.fatal = true
        return result
      }
      let raw = slice_str(source, start, pos)
      if exceeds_utf8_byte_limit(raw, MAX_WORD_BYTES) {
        result.issues.push({
          code: "G103",
          severity: Error,
          message: "Command exceeds configured size limit.",
          line: Some(start_line),
        })
        result.fatal = true
        return result
      }
      result.tokens.push({ kind: Word, raw, line: start_line })
      if result.tokens.length() > MAX_TOKEN_COUNT {
        result.issues.push({
          code: "G002",
          severity: Error,
          message: "Command count exceeds 1,000,000.",
          line: Some(start_line),
        })
        result.fatal = true
        return result
      }
    }
  }
  result
}

///|
fn strip_word_star(raw : String) -> String {
  if raw.length() > 0 && code_unit_at(raw, raw.length() - 1) == 42 {
    slice_str(raw, 0, raw.length() - 1)
  } else {
    raw
  }
}

///|
fn strip_extended(raw : String) -> String {
  let mut inner = if raw.length() >= 2 &&
    code_unit_at(raw, 0) == 37 &&
    code_unit_at(raw, raw.length() - 1) == 37 {
    slice_str(raw, 1, raw.length() - 1)
  } else {
    raw
  }
  // Gerber extended commands terminate with '*' before the closing '%'.
  if inner.length() > 0 && code_unit_at(inner, inner.length() - 1) == 42 {
    inner = slice_str(inner, 0, inner.length() - 1)
  }
  inner
}