///|
pub fn parse_gs1(input : String) -> GS1Message raise ParseError {
  let trimmed = input.trim().to_owned()
  if trimmed.is_empty() {
    raise ParseError::InvalidSyntax("empty GS1 payload")
  }
  if trimmed.has_prefix("(") {
    parse_hri(trimmed)
  } else {
    parse_raw(trimmed)
  }
}

///|
pub fn parse_gs1_with_options(
  input : String,
  options : ParseOptions,
) -> GS1Message raise ParseError {
  if input.length() > options.max_length {
    raise ParseError::Truncated("payload", options.max_length, input.length())
  }
  let trimmed = input.trim().to_owned()
  if trimmed.is_empty() {
    raise ParseError::InvalidSyntax("empty GS1 payload")
  }
  let hri = trimmed.has_prefix("(")
  match options.mode {
    HRI if !hri =>
      raise ParseError::InvalidSyntax("raw payload requires raw mode")
    Raw if hri =>
      raise ParseError::InvalidSyntax("HRI payload requires HRI mode")
    _ => ()
  }
  if !options.allow_group_separator && contains_group_separator(trimmed) {
    raise ParseError::InvalidSyntax("group separator is disabled")
  }
  if hri && trimmed.get_char(trimmed.length() - 1) == Some('\u{1D}') {
    raise ParseError::InvalidSyntax(
      "HRI payload cannot end with a group separator",
    )
  }
  let message = if hri { parse_hri(trimmed) } else { parse_raw(trimmed) }
  if !options.allow_empty_variable {
    for element in message.elements {
      if element.value.is_empty() {
        raise ParseError::InvalidSyntax("AI {element.code()} requires a value")
      }
    }
  }
  message
}

///|
fn contains_group_separator(input : String) -> Bool {
  for ch in input {
    if is_group_separator(ch) {
      return true
    }
  } nobreak {
    false
  }
}

///|
fn parse_hri(input : String) -> GS1Message raise ParseError {
  let elements : Array[GS1Element] = []
  let len = input.length()
  for pos = 0; pos < len; {
    guard input.get_char(pos) is Some('(') else {
      raise ParseError::InvalidSyntax("expected '(' at offset \{pos}")
    }
    let close = find_close(input, pos + 1)
    let code = input.unsafe_substring(start=pos + 1, end=close)
    let spec = lookup_ai_exact(code)
    let value_start = close + 1
    let (value, next) = read_value(input, value_start, spec, hri=true)
    elements.push({ ai: spec, value })
    continue next
  } nobreak {
    { elements, }
  }
}

///|
fn parse_raw(input : String) -> GS1Message raise ParseError {
  let elements : Array[GS1Element] = []
  let len = input.length()
  for pos = 0; pos < len; {
    match input.get_char(pos) {
      Some(ch) if is_group_separator(ch) => continue pos + 1
      _ => ()
    }
    let (spec, value_start) = find_ai(input, pos)
    let (value, next) = read_value(input, value_start, spec, hri=false)
    elements.push({ ai: spec, value })
    continue next
  } nobreak {
    { elements, }
  }
}

///|
fn lookup_ai_exact(code : String) -> ApplicationIdentifier raise ParseError {
  for item in known_ais() {
    if item.code == code {
      return item
    }
  }
  raise ParseError::UnknownAI(code)
}

///|
fn find_close(input : String, pos : Int) -> Int raise ParseError {
  let len = input.length()
  for i = pos; i < len; i = i + 1 {
    if input.get_char(i) == Some(')') {
      if i == pos {
        raise ParseError::InvalidSyntax("empty application identifier")
      }
      return i
    }
  }
  raise ParseError::InvalidSyntax("missing ')' after offset \{pos - 1}")
}

///|
fn read_value(
  input : String,
  start : Int,
  spec : ApplicationIdentifier,
  hri~ : Bool,
) -> (String, Int) raise ParseError {
  match spec.fixed_length {
    Some(size) => {
      let end = start + size
      if end > input.length() {
        raise ParseError::Truncated(spec.code, size, input.length() - start)
      }
      (input.unsafe_substring(start~, end~), end)
    }
    None => read_variable_value(input, start, spec, hri~)
  }
}

///|
fn read_variable_value(
  input : String,
  start : Int,
  spec : ApplicationIdentifier,
  hri~ : Bool,
) -> (String, Int) raise ParseError {
  let limit = if start + spec.max_length < input.length() {
    start + spec.max_length
  } else {
    input.length()
  }
  for pos = start; pos < limit; pos = pos + 1 {
    match input.get_char(pos) {
      Some(ch) if is_group_separator(ch) => {
        let value = input.unsafe_substring(start~, end=pos)
        return (value, pos + 1)
      }
      Some('(') if hri => {
        let value = input.unsafe_substring(start~, end=pos)
        return (value, pos)
      }
      _ => ()
    }
  }
  let value = input.unsafe_substring(start~, end=limit)
  if value.is_empty() {
    raise ParseError::InvalidSyntax("AI \{spec.code} requires a value")
  }
  (value, limit)
}