///|
pub(all) enum AddressField {
  Recipient
  Organization
  AddressLine
  DependentLocality
  Locality
  AdministrativeArea
  PostalCode
  Country
}

///|
pub struct PostalPattern {
  kind : PostalPatternKind
  minimum_length : Int
  maximum_length : Int
}

///|
pub(all) enum PostalPatternKind {
  NoCheck
  Digits
  DigitsRange
  AlphanumericRange
  UsZip
  CanadaPostal
  UkPostcode
  JapanPostal
  ChinaPostal
}

///|
pub fn PostalPattern::no_check() -> PostalPattern {
  { kind: NoCheck, minimum_length: 0, maximum_length: 0, }
}

///|
pub fn PostalPattern::digits(length : Int) -> PostalPattern {
  { kind: Digits, minimum_length: length, maximum_length: length, }
}

///|
pub fn PostalPattern::digits_in_range(
  minimum : Int,
  maximum : Int,
) -> PostalPattern {
  { kind: DigitsRange, minimum_length: minimum, maximum_length: maximum, }
}

///|
pub fn PostalPattern::alphanumeric_in_range(
  minimum : Int,
  maximum : Int,
) -> PostalPattern {
  { kind: AlphanumericRange, minimum_length: minimum, maximum_length: maximum, }
}

///|
fn builtin_postal_pattern(kind : PostalPatternKind) -> PostalPattern {
  { kind, minimum_length: 0, maximum_length: 0, }
}

///|
pub struct Address {
  recipient : String
  organization : String
  address_lines : Array[String]
  dependent_locality : String
  locality : String
  administrative_area : String
  postal_code : String
  country_code : String
}

///|
pub fn Address::new(
  recipient? : String = "",
  organization? : String = "",
  address_lines? : Array[String] = [],
  dependent_locality? : String = "",
  locality? : String = "",
  administrative_area? : String = "",
  postal_code? : String = "",
  country_code? : String = "",
) -> Address {
  {
    recipient,
    organization,
    address_lines,
    dependent_locality,
    locality,
    administrative_area,
    postal_code,
    country_code,
  }
}

///|
pub struct AddressRule {
  country_code : String
  required_fields : Array[AddressField]
  postal_pattern : PostalPattern
  field_order : Array[AddressField]
  combine_region_postal : Bool
}

///|
pub fn AddressRule::new(
  country_code : String,
  required_fields : Array[AddressField],
  postal_pattern : PostalPattern,
  field_order : Array[AddressField],
) -> AddressRule {
  {
    country_code: ascii_uppercase(country_code.trim().to_owned()),
    required_fields,
    postal_pattern,
    field_order,
    combine_region_postal: false,
  }
}

///|
pub(all) enum IssueCode {
  MissingRequiredField
  InvalidPostalCode
  CountryMismatch
}

///|
pub struct AddressIssue {
  code : IssueCode
  field : AddressField
}

///|
pub fn AddressIssue::message(self : AddressIssue) -> String {
  let field = match self.field {
    Recipient => "recipient"
    Organization => "organization"
    AddressLine => "address line"
    DependentLocality => "dependent locality"
    Locality => "locality"
    AdministrativeArea => "administrative area"
    PostalCode => "postal code"
    Country => "country code"
  }
  match self.code {
    MissingRequiredField => field + " is required"
    InvalidPostalCode => "postal code has an invalid format"
    CountryMismatch => "country code does not match the selected rule"
  }
}

///|
pub fn builtin_rule(country_code : String) -> AddressRule? {
  match ascii_uppercase(country_code) {
    "US" =>
      Some({
        country_code: "US",
        required_fields: [
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
        postal_pattern: builtin_postal_pattern(UsZip),
        combine_region_postal: true,
        field_order: [
          Recipient,
          Organization,
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
      })
    "CA" =>
      Some({
        country_code: "CA",
        required_fields: [
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
        postal_pattern: builtin_postal_pattern(CanadaPostal),
        combine_region_postal: true,
        field_order: [
          Recipient,
          Organization,
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
      })
    "GB" =>
      Some({
        country_code: "GB",
        required_fields: [AddressLine, Locality, PostalCode, Country],
        postal_pattern: builtin_postal_pattern(UkPostcode),
        combine_region_postal: false,
        field_order: [
          Recipient,
          Organization,
          AddressLine,
          DependentLocality,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
      })
    "JP" =>
      Some({
        country_code: "JP",
        required_fields: [AddressLine, Locality, PostalCode, Country],
        postal_pattern: builtin_postal_pattern(JapanPostal),
        combine_region_postal: false,
        field_order: [
          PostalCode,
          AdministrativeArea,
          Locality,
          DependentLocality,
          AddressLine,
          Organization,
          Recipient,
          Country,
        ],
      })
    "CN" =>
      Some({
        country_code: "CN",
        required_fields: [
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
        postal_pattern: builtin_postal_pattern(ChinaPostal),
        combine_region_postal: false,
        field_order: [
          Country,
          AdministrativeArea,
          Locality,
          DependentLocality,
          AddressLine,
          Organization,
          Recipient,
          PostalCode,
        ],
      })
    "AU" =>
      Some({
        country_code: "AU",
        required_fields: [
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
        postal_pattern: PostalPattern::digits(4),
        combine_region_postal: true,
        field_order: [
          Recipient,
          Organization,
          AddressLine,
          Locality,
          AdministrativeArea,
          PostalCode,
          Country,
        ],
      })
    "AE" =>
      Some({
        country_code: "AE",
        required_fields: [AddressLine, Locality, AdministrativeArea, Country],
        postal_pattern: PostalPattern::no_check(),
        combine_region_postal: false,
        field_order: [
          Recipient,
          Organization,
          AddressLine,
          Locality,
          AdministrativeArea,
          Country,
        ],
      })
    _ => None
  }
}

///|
pub fn validate(rule : AddressRule, address : Address) -> Array[AddressIssue] {
  let issues : Array[AddressIssue] = []
  if !address.country_code.trim().is_empty() &&
    ascii_uppercase(address.country_code.trim().to_owned()) != rule.country_code {
    issues.push({ code: CountryMismatch, field: Country, })
  }
  for field in rule.required_fields {
    if field_is_empty(field, address) {
      issues.push({ code: MissingRequiredField, field, })
    }
  }
  if !address.postal_code.trim().is_empty() &&
    !postal_matches(rule.postal_pattern, address.postal_code) {
    issues.push({ code: InvalidPostalCode, field: PostalCode, })
  }
  issues
}

///|
pub fn format_address(rule : AddressRule, address : Address) -> String {
  format_address_lines(rule, address).join("\n")
}

///|
pub fn format_if_valid(rule : AddressRule, address : Address) -> String? {
  if validate(rule, address).is_empty() {
    Some(format_address(rule, address))
  } else {
    None
  }
}

///|
pub fn format_address_lines(
  rule : AddressRule,
  address : Address,
) -> Array[String] {
  let lines : Array[String] = []
  for field in rule.field_order {
    match field {
      AddressLine =>
        for line in address.address_lines {
          push_nonempty(lines, line)
        }
      Locality if rule.combine_region_postal =>
        push_nonempty(lines, locality_region_postal_line(address))
      AdministrativeArea | PostalCode if rule.combine_region_postal => ()
      _ => push_nonempty(lines, field_value(field, address))
    }
  }
  lines
}

///|
fn locality_region_postal_line(address : Address) -> String {
  let suffix : Array[String] = []
  push_nonempty(suffix, address.administrative_area)
  push_nonempty(suffix, address.postal_code)
  let locality = address.locality.trim().to_owned()
  let tail = suffix.join(" ")
  if locality.is_empty() {
    tail
  } else if tail.is_empty() {
    locality
  } else {
    locality + ", " + tail
  }
}

///|
fn field_is_empty(field : AddressField, address : Address) -> Bool {
  match field {
    AddressLine => address_lines_are_empty(address.address_lines)
    Country => address.country_code.trim().is_empty()
    _ => field_value(field, address).trim().is_empty()
  }
}

///|
fn address_lines_are_empty(lines : Array[String]) -> Bool {
  for line in lines {
    if !line.trim().is_empty() {
      return false
    }
  }
  true
}

///|
fn field_value(field : AddressField, address : Address) -> String {
  match field {
    Recipient => address.recipient
    Organization => address.organization
    DependentLocality => address.dependent_locality
    Locality => address.locality
    AdministrativeArea => address.administrative_area
    PostalCode => address.postal_code
    Country => address.country_code
    AddressLine => ""
  }
}

///|
fn push_nonempty(lines : Array[String], value : String) -> Unit {
  let trimmed = value.trim()
  if !trimmed.is_empty() {
    lines.push(trimmed.to_owned())
  }
}

///|
fn postal_matches(pattern : PostalPattern, input : String) -> Bool {
  let code = input.trim().to_owned()
  match pattern.kind {
    NoCheck => true
    Digits => code.length() == pattern.minimum_length && all_ascii_digits(code)
    DigitsRange =>
      code.length() >= pattern.minimum_length &&
      code.length() <= pattern.maximum_length &&
      all_ascii_digits(code)
    AlphanumericRange =>
      code.length() >= pattern.minimum_length &&
      code.length() <= pattern.maximum_length &&
      all_ascii_alphanumeric(code)
    UsZip => us_zip_matches(input)
    CanadaPostal => canada_postal_matches(input)
    UkPostcode => uk_postcode_matches(input)
    JapanPostal => japan_postal_matches(input)
    ChinaPostal =>
      input.trim().length() == 6 && all_ascii_digits(input.trim().to_owned())
  }
}

///|
fn us_zip_matches(input : String) -> Bool {
  let chars = input.trim().iter().to_array()
  if chars.length() == 5 {
    return all_ascii_digits(input.trim().to_owned())
  }
  if chars.length() != 10 || chars[5] != '-' {
    return false
  }
  for index in 0..<10 {
    if index != 5 && (chars[index] < '0' || chars[index] > '9') {
      return false
    }
  }
  true
}

///|
fn japan_postal_matches(input : String) -> Bool {
  let chars = input.trim().iter().to_array()
  if chars.length() == 7 {
    return all_ascii_digits(input.trim().to_owned())
  }
  if chars.length() != 8 || chars[3] != '-' {
    return false
  }
  for index in 0..<8 {
    if index != 3 && (chars[index] < '0' || chars[index] > '9') {
      return false
    }
  }
  true
}

///|
fn compact_postal(input : String) -> String {
  let result = StringBuilder()
  for character in input.trim().iter() {
    if character != ' ' && character != '-' {
      result.write_char(character.to_ascii_uppercase())
    }
  }
  result.to_string()
}

///|
fn ascii_uppercase(input : String) -> String {
  let result = StringBuilder()
  for character in input.iter() {
    result.write_char(character.to_ascii_uppercase())
  }
  result.to_string()
}

///|
fn all_ascii_digits(input : String) -> Bool {
  if input.is_empty() {
    return false
  }
  for character in input.iter() {
    if character < '0' || character > '9' {
      return false
    }
  }
  true
}

///|
fn all_ascii_alphanumeric(input : String) -> Bool {
  if input.is_empty() {
    return false
  }
  for character in input.iter() {
    if (character < '0' || character > '9') && !is_ascii_letter(character) {
      return false
    }
  }
  true
}

///|
fn is_ascii_letter(character : Char) -> Bool {
  (character >= 'A' && character <= 'Z') ||
  (character >= 'a' && character <= 'z')
}

///|
fn is_ascii_digit(character : Char) -> Bool {
  character >= '0' && character <= '9'
}

///|
fn canada_postal_matches(input : String) -> Bool {
  let trimmed = input.trim()
  let raw = trimmed.iter().to_array()
  if raw.length() == 7 && raw[3] != ' ' {
    return false
  }
  if raw.length() != 6 && raw.length() != 7 {
    return false
  }
  let chars = compact_postal(trimmed.to_owned()).iter().to_array()
  if chars.length() != 6 {
    return false
  }
  for index in 0..<6 {
    let character = chars[index]
    if index % 2 == 0 {
      if !is_ascii_letter(character) || "DFIOQU".contains(character.to_string()) {
        return false
      }
      if index == 0 && "WZ".contains(character.to_string()) {
        return false
      }
    } else if character < '0' || character > '9' {
      return false
    }
  }
  true
}

///|
fn uk_postcode_matches(input : String) -> Bool {
  let trimmed = input.trim()
  let raw = trimmed.iter().to_array()
  let compact = compact_postal(trimmed.to_owned())
  let compact_length = compact.length()
  let mut spaces = 0
  let mut space_position = -1
  for index in 0.. 1 || (spaces == 1 && space_position != compact_length - 3) {
    return false
  }
  let chars = compact.iter().to_array()
  if chars.length() < 5 || chars.length() > 7 {
    return false
  }
  if compact == "GIR0AA" {
    return true
  }
  let outward_length = chars.length() - 3
  let area_length = if is_ascii_letter(chars[1]) { 2 } else { 1 }
  let district_length = outward_length - area_length
  if !is_ascii_letter(chars[0]) ||
    district_length < 1 ||
    district_length > 2 ||
    !is_ascii_digit(chars[area_length]) {
    return false
  }
  if district_length == 2 &&
    !is_ascii_digit(chars[area_length + 1]) &&
    !is_ascii_letter(chars[area_length + 1]) {
    return false
  }
  is_ascii_digit(chars[outward_length]) &&
  is_ascii_letter(chars[outward_length + 1]) &&
  is_ascii_letter(chars[outward_length + 2])
}