///|
pub const VERSION : String = "0.1.0"

///|
pub const MAX_INPUT_BYTES : Int = 64 * 1024 * 1024

///|
pub const MAX_TOKEN_COUNT : Int = 1_000_000

///|
pub const MAX_WORD_BYTES : Int = 64 * 1024

///|
pub const MAX_EXTENDED_BLOCK_BYTES : Int = 1024 * 1024

///|
pub(all) enum Severity {
  Error
  Warning
} derive(Eq, Debug)

///|
pub(all) enum AnalysisStatus {
  Pass
  PassWithWarnings
  Fail
} derive(Eq, Debug)

///|
pub(all) enum GerberUnit {
  Millimeter
  Inch
} derive(Eq, Debug)

///|
pub(all) struct CoordinateFormat {
  integer_digits : Int
  decimal_digits : Int
} derive(Eq, Debug)

///|
pub(all) enum RawTokenKind {
  Word
  ExtendedBlock
} derive(Eq, Debug)

///|
pub(all) struct RawToken {
  kind : RawTokenKind
  raw : String
  line : Int
} derive(Eq, Debug)

///|
pub(all) struct CoordinateFields {
  x : String?
  y : String?
  i : String?
  j : String?
} derive(Eq, Debug)

///|
pub(all) enum PlotMode {
  Linear
  Clockwise
  CounterClockwise
} derive(Eq, Debug)

///|
pub(all) enum Polarity {
  Dark
  Clear
} derive(Eq, Debug)

///|
pub(all) struct ApertureDefinition {
  code : Int
  template : String
  parameters_raw : String?
  line : Int
} derive(Eq, Debug)

///|
pub(all) enum AttributeScope {
  File
  Aperture
  Object
} derive(Eq, Debug)

///|
pub(all) struct GerberAttribute {
  scope : AttributeScope
  name : String
  raw_value : String?
  line : Int
} derive(Eq, Debug)

///|
pub(all) enum GerberCommand {
  Comment(String)
  Format(CoordinateFormat)
  Unit(GerberUnit)
  ApertureMacro(String, String)
  ApertureDefinition(ApertureDefinition)
  SelectAperture(Int)
  SetPlotMode(PlotMode)
  MultiQuadrant
  Move(CoordinateFields)
  Draw(CoordinateFields)
  Flash(CoordinateFields)
  BeginRegion
  EndRegion
  SetPolarity(Polarity)
  Attribute(GerberAttribute)
  DeleteAttribute(String?)
  EndFile
  UnknownWord(String)
  UnknownExtended(String)
  Malformed(String)
} derive(Eq, Debug)

///|
pub(all) struct LocatedCommand {
  command : GerberCommand
  line : Int
} derive(Eq, Debug)

///|
pub(all) struct Issue {
  code : String
  severity : Severity
  message : String
  line : Int?
} derive(Eq, Debug)

///|
pub(all) struct GerberStatistics {
  apertures : Int
  aperture_macros : Int
  moves : Int
  draws : Int
  flashes : Int
  regions : Int
  arc_draws : Int
  comments : Int
  attributes : Int
  unknown_commands : Int
} derive(Eq, Debug)

///|
pub(all) struct GerberReport {
  status : AnalysisStatus
  unit : GerberUnit?
  coordinate_format : CoordinateFormat?
  file_function : String?
  file_polarity : String?
  generation_software : String?
  statistics : GerberStatistics
  issues : Array[Issue]
} derive(Eq, Debug)

///|
pub(all) enum CliCommand {
  Inspect
  Check
  Help
  Version
} derive(Eq, Debug)

///|
pub(all) struct CliConfig {
  command : CliCommand
  file : String?
  json : Bool
} derive(Eq, Debug)

///|
pub(all) enum CliError {
  Usage(String)
} derive(Eq, Debug)

///|
fn empty_statistics() -> GerberStatistics {
  {
    apertures: 0,
    aperture_macros: 0,
    moves: 0,
    draws: 0,
    flashes: 0,
    regions: 0,
    arc_draws: 0,
    comments: 0,
    attributes: 0,
    unknown_commands: 0,
  }
}

///|
fn compute_status(issues : Array[Issue]) -> AnalysisStatus {
  let mut errors = 0
  let mut warnings = 0
  for issue in issues {
    match issue.severity {
      Error => errors = errors + 1
      Warning => warnings = warnings + 1
    }
  }
  if errors > 0 {
    Fail
  } else if warnings > 0 {
    PassWithWarnings
  } else {
    Pass
  }
}

///|
fn count_errors(issues : Array[Issue]) -> Int {
  let mut n = 0
  for issue in issues {
    if issue.severity == Error {
      n = n + 1
    }
  }
  n
}

///|
fn count_warnings(issues : Array[Issue]) -> Int {
  let mut n = 0
  for issue in issues {
    if issue.severity == Warning {
      n = n + 1
    }
  }
  n
}

///|
pub fn truncate_for_message(s : String) -> String {
  if s.length() <= 120 {
    s
  } else {
    s[0:120].to_owned() + "..."
  }
}

///|
fn is_ascii_digit(c : Int) -> Bool {
  c >= 48 && c <= 57
}

///|
fn code_unit_at(s : String, i : Int) -> Int {
  s[i].to_int()
}

///|
fn safe_parse_aperture_code(s : String) -> Int? {
  if s.length() == 0 {
    return None
  }
  let mut value = 0
  let mut i = 0
  while i < s.length() {
    let c = code_unit_at(s, i)
    if !is_ascii_digit(c) {
      return None
    }
    let digit = c - 48
    if value > (2147483647 - digit) / 10 {
      return None
    }
    value = value * 10 + digit
    i = i + 1
  }
  if value < 10 {
    None
  } else {
    Some(value)
  }
}

///|
fn is_coordinate_integer(s : String) -> Bool {
  if s.length() == 0 {
    return false
  }
  let mut i = 0
  let first = code_unit_at(s, 0)
  if first == 43 || first == 45 {
    i = 1
  }
  if i >= s.length() {
    return false
  }
  while i < s.length() {
    if !is_ascii_digit(code_unit_at(s, i)) {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn digit_count_of_coordinate(s : String) -> Int {
  let mut i = 0
  if s.length() > 0 {
    let first = code_unit_at(s, 0)
    if first == 43 || first == 45 {
      i = 1
    }
  }
  s.length() - i
}

///|
fn exceeds_utf8_byte_limit(s : String, limit : Int) -> Bool {
  let mut bytes = 0
  let mut i = 0
  while i < s.length() {
    let code = code_unit_at(s, i)
    if code <= 0x7F {
      bytes = bytes + 1
    } else if code <= 0x7FF {
      bytes = bytes + 2
    } else if code >= 0xD800 && code <= 0xDBFF && i + 1 < s.length() {
      let next = code_unit_at(s, i + 1)
      if next >= 0xDC00 && next <= 0xDFFF {
        bytes = bytes + 4
        i = i + 1
      } else {
        bytes = bytes + 3
      }
    } else {
      bytes = bytes + 3
    }
    if bytes > limit {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn status_text(status : AnalysisStatus) -> String {
  match status {
    Pass => "PASS"
    PassWithWarnings => "PASS WITH WARNINGS"
    Fail => "FAIL"
  }
}

///|
fn status_json(status : AnalysisStatus) -> String {
  match status {
    Pass => "pass"
    PassWithWarnings => "pass_with_warnings"
    Fail => "fail"
  }
}

///|
fn unit_text(unit : GerberUnit) -> String {
  match unit {
    Millimeter => "millimeter"
    Inch => "inch"
  }
}

///|
fn unit_json(unit : GerberUnit) -> String {
  match unit {
    Millimeter => "mm"
    Inch => "inch"
  }
}

///|
fn severity_json(severity : Severity) -> String {
  match severity {
    Error => "error"
    Warning => "warning"
  }
}

///|
fn cli_command_json(command : CliCommand) -> String {
  match command {
    Inspect => "inspect"
    Check => "check"
    Help => "help"
    Version => "version"
  }
}

///|
fn slice_str(s : String, start : Int, end : Int) -> String {
  s[start:end].to_owned()
}

///|
fn trim_spaces(s : String) -> String {
  let mut start = 0
  let mut end = s.length()
  while start < end {
    let c = code_unit_at(s, start)
    if c == 32 || c == 9 || c == 10 || c == 13 {
      start = start + 1
    } else {
      break
    }
  }
  while end > start {
    let c = code_unit_at(s, end - 1)
    if c == 32 || c == 9 || c == 10 || c == 13 {
      end = end - 1
    } else {
      break
    }
  }
  slice_str(s, start, end)
}

///|
fn has_prefix(s : String, prefix : String) -> Bool {
  s.has_prefix(prefix)
}