///|
const IFC_GUID_ALPHABET : String = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$"

///|
pub fn ifc_guid_alphabet() -> String {
  IFC_GUID_ALPHABET
}

///|
fn guid_is_valid_character(character : Char) -> Bool {
  IFC_GUID_ALPHABET.contains_char(character)
}

///|
fn guid_checksum(value : String) -> Int {
  let mut checksum = 17
  for character in value {
    checksum = (checksum * 31 + character.to_int()) % 1000003
  }
  checksum
}

///|
/// Validate the lexical constraints of an IFC compressed GUID.
pub fn validate_ifc_guid(value : String) -> GuidReport {
  let trimmed = value.trim()
  let mut valid = trimmed.length() == 22
  let mut decoded = 0
  for character in trimmed {
    if guid_is_valid_character(character) {
      decoded += 1
    } else {
      valid = false
    }
  }
  let message = if trimmed.length() != 22 {
    "IFC compressed GUID must contain 22 characters"
  } else if decoded != 22 {
    "IFC compressed GUID contains an unsupported character"
  } else {
    "valid lexical IFC compressed GUID"
  }
  {
    value,
    valid,
    decoded_bytes: if valid {
      16
    } else {
      0
    },
    normalized: trimmed.to_owned(),
    message,
  }
}

///|
pub fn ifc_guid_is_valid(value : String) -> Bool {
  validate_ifc_guid(value).valid
}

///|
pub fn ifc_guid_fingerprint(value : String) -> String {
  let report = validate_ifc_guid(value)
  if !report.valid {
    "invalid"
  } else {
    let hash = guid_checksum(report.normalized)
    "guid-" + hash.to_string()
  }
}

///|
pub fn GuidReport::headline(self : GuidReport) -> String {
  self.normalized + " valid=" + self.valid.to_string() + " " + self.message
}

///|
pub fn GuidReport::is_canonical(self : GuidReport) -> Bool {
  self.valid && self.value == self.normalized
}

///|
pub fn GuidReport::to_json(self : GuidReport) -> String {
  "{\"value\":\"" +
  self.value +
  "\",\"normalized\":\"" +
  self.normalized +
  "\",\"valid\":" +
  self.valid.to_string() +
  ",\"decoded_bytes\":" +
  self.decoded_bytes.to_string() +
  ",\"fingerprint\":\"" +
  ifc_guid_fingerprint(self.value) +
  "\"}"
}

///|
pub fn model_guid_reports(model : Model) -> Array[GuidReport] {
  model
  .products()
  .fold(init=Array::new(), (result, product) => {
    match product.global_id {
      Some(value) => result.push(validate_ifc_guid(value))
      None => ()
    }
    result
  })
}

///|
pub fn invalid_model_guids(model : Model) -> Array[GuidReport] {
  model_guid_reports(model).filter(report => !report.valid)
}

///|
pub fn duplicate_model_guids(model : Model) -> Array[String] {
  let counts : Map[String, Int] = Map([])
  for report in model_guid_reports(model) {
    if report.valid {
      counts.update_or_default(report.normalized, 1, count => count + 1)
    }
  }
  let result : Array[String] = []
  for value, count in counts {
    if count > 1 {
      result.push(value)
    }
  }
  result
}

///|
pub fn guid_quality_score(model : Model) -> Int {
  let reports = model_guid_reports(model)
  if reports.length() == 0 {
    return 0
  }
  let valid = reports.filter(report => report.valid).length()
  valid * 100 / reports.length()
}

///|
pub fn guid_quality_summary(model : Model) -> Array[String] {
  let reports = model_guid_reports(model)
  [
    "checked=" + reports.length().to_string(),
    "valid=" + reports.filter(report => report.valid).length().to_string(),
    "invalid=" + invalid_model_guids(model).length().to_string(),
    "duplicates=" + duplicate_model_guids(model).length().to_string(),
    "score=" + guid_quality_score(model).to_string(),
  ]
}

///|
pub fn ifc_guid_from_product(model : Model, product_id : Int) -> GuidReport? {
  for product in model.products() {
    if product.id == product_id {
      return product.global_id.map(value => validate_ifc_guid(value))
    }
  }
  None
}

///|
pub fn normalize_ifc_guid(value : String) -> String {
  validate_ifc_guid(value).normalized
}

///|
pub fn guid_fingerprint_map(model : Model) -> Map[String, Int] {
  model_guid_reports(model).fold(init=Map([]), (result, report) => {
    if report.valid {
      result.update_or_default(ifc_guid_fingerprint(report.value), 1, count => {
        count + 1
      })
    }
    result
  })
}