///|
pub suberror ParseError {
  UnknownAI(String)
  Truncated(String, Int, Int)
  InvalidSyntax(String)
} derive(Debug)

///|
pub enum ValueKind {
  Fixed
  Variable
} derive(Debug, Eq)

///|
pub struct ApplicationIdentifier {
  code : String
  title : String
  fixed_length : Int?
  max_length : Int
  kind : ValueKind
} derive(Debug, Eq)

///|
pub struct GS1Element {
  ai : ApplicationIdentifier
  value : String
} derive(Debug, Eq)

///|
pub struct GS1Message {
  elements : Array[GS1Element]
} derive(Debug, Eq)

///|
pub fn GS1Element::code(self : GS1Element) -> String {
  self.ai.code
}

///|
pub fn GS1Element::title(self : GS1Element) -> String {
  self.ai.title
}

///|
pub fn GS1Element::is_valid(self : GS1Element) -> Bool {
  match self.ai.code {
    "00" => is_all_digits(self.value) && self.value.length() == 18
    "01" => is_gtin(self.value)
    "02" => is_gtin(self.value)
    "11" | "12" | "13" | "15" | "16" | "17" =>
      is_all_digits(self.value) && self.value.length() == 6
    "3100" | "3101" | "3102" | "3103" | "3104" | "3105" | "3106" =>
      is_all_digits(self.value) && self.value.length() == 6
    _ => self.value.length() <= self.ai.max_length
  }
}

///|
pub fn GS1Element::to_hri(self : GS1Element) -> String {
  "(\{self.ai.code})\{self.value}"
}

///|
pub fn GS1Message::to_hri(self : GS1Message) -> String {
  self.elements.map(fn(elem) { elem.to_hri() }).join("")
}

///|
fn ai(
  code : String,
  title : String,
  fixed_length : Int?,
  max_length : Int,
  kind : ValueKind,
) -> ApplicationIdentifier {
  { code, title, fixed_length, max_length, kind }
}

///|
fn base_ais() -> Array[ApplicationIdentifier] {
  [
    ai("00", "SSCC", Some(18), 18, Fixed),
    ai("01", "GTIN", Some(14), 14, Fixed),
    ai("02", "Contained GTIN", Some(14), 14, Fixed),
    ai("10", "Batch or lot number", None, 20, Variable),
    ai("11", "Production date", Some(6), 6, Fixed),
    ai("12", "Due date", Some(6), 6, Fixed),
    ai("13", "Packaging date", Some(6), 6, Fixed),
    ai("15", "Best before date", Some(6), 6, Fixed),
    ai("16", "Sell by date", Some(6), 6, Fixed),
    ai("17", "Expiration date", Some(6), 6, Fixed),
    ai("21", "Serial number", None, 20, Variable),
    ai("240", "Additional product identification", None, 30, Variable),
    ai("241", "Customer part number", None, 30, Variable),
    ai("242", "Made-to-order variation", None, 6, Variable),
    ai("250", "Secondary serial number", None, 30, Variable),
    ai("251", "Reference to source entity", None, 30, Variable),
    ai("30", "Variable count", None, 8, Variable),
    ai("3100", "Net weight kg, 0 decimals", Some(6), 6, Fixed),
    ai("3101", "Net weight kg, 1 decimal", Some(6), 6, Fixed),
    ai("3102", "Net weight kg, 2 decimals", Some(6), 6, Fixed),
    ai("3103", "Net weight kg, 3 decimals", Some(6), 6, Fixed),
    ai("3104", "Net weight kg, 4 decimals", Some(6), 6, Fixed),
    ai("3105", "Net weight kg, 5 decimals", Some(6), 6, Fixed),
    ai("3106", "Net weight kg, 6 decimals", Some(6), 6, Fixed),
    ai("37", "Count of trade items", None, 8, Variable),
    ai("400", "Customer purchase order number", None, 30, Variable),
    ai("410", "Ship to GLN", Some(13), 13, Fixed),
    ai("411", "Bill to GLN", Some(13), 13, Fixed),
    ai("412", "Purchased from GLN", Some(13), 13, Fixed),
    ai("420", "Ship to postal code", None, 20, Variable),
  ]
}

///|
fn known_ais() -> Array[ApplicationIdentifier] {
  base_ais() + extended_ais() + supplemental_measure_ais()
}

///|
pub fn application_identifiers() -> Array[ApplicationIdentifier] {
  known_ais()
}

///|
pub fn lookup_ai(code : String) -> ApplicationIdentifier? {
  for item in known_ais() {
    if item.code == code {
      return Some(item)
    }
  }
  None
}

///|
pub fn ai_codes_with_prefix(prefix : String) -> Array[String] {
  let codes : Array[String] = []
  for item in known_ais() {
    if item.code.has_prefix(prefix) {
      codes.push(item.code)
    }
  }
  codes
}

///|
fn find_ai(
  input : String,
  pos : Int,
) -> (ApplicationIdentifier, Int) raise ParseError {
  let len = input.length()
  let candidates = known_ais()
  for size in [4, 3, 2] {
    if pos + size <= len {
      let code = input.unsafe_substring(start=pos, end=pos + size)
      for item in candidates {
        if item.code == code {
          return (item, pos + size)
        }
      }
    }
  }
  let end = if pos + 2 <= len { pos + 2 } else { len }
  raise ParseError::UnknownAI(input.unsafe_substring(start=pos, end~))
}

///|
fn is_group_separator(ch : Char) -> Bool {
  ch.to_int() == 29
}

///|
fn is_all_digits(input : String) -> Bool {
  input.length() > 0 && input.all(fn(ch) { ch.is_ascii_digit() })
}

///|
fn digit_at(input : String, pos : Int) -> Int {
  input[pos].to_int() - ('0' : UInt16).to_int()
}

///|
fn is_gtin(value : String) -> Bool {
  if value.length() != 14 || !is_all_digits(value) {
    false
  } else {
    let sum = for i = 0, acc = 0; i < 13; i = i + 1 {
      let weight = if i % 2 == 0 { 3 } else { 1 }
      continue i + 1, acc + digit_at(value, i) * weight
    } nobreak {
      acc
    }
    let check = (10 - sum % 10) % 10
    check == digit_at(value, 13)
  }
}