// Card data helpers: Luhn, compressed numeric (BCD), track 2, masking.

pub(all) struct Track2 {
  pan : String
  expiry : String
  service_code : String
}

pub(all) struct CardData {
  pan : String?
  expiry : String?
  cardholder : String?
  label : String?
}

pub fn luhn_valid(digits : String) -> Bool {
  let mut sum = 0
  let mut double = false
  let n = digits.length()
  if n == 0 {
    return false
  }
  for i in 0.. 57 {
      return false
    }
    let mut d = c - 48
    if double {
      d = d * 2
      if d > 9 {
        d = d - 9
      }
    }
    sum = sum + d
    double = !double
  }
  sum % 10 == 0
}

pub fn bcd_to_string(bytes : Array[Int]) -> String? {
  let out = StringBuilder()
  for b in bytes {
    let high = (b >> 4) & 0xf
    let low = b & 0xf
    if high == 0xf {
      return Some(out.to_string())
    }
    if high > 9 {
      return None
    }
    out.write_char(Char::from_int(48 + high))
    if low == 0xf {
      return Some(out.to_string())
    }
    if low > 9 {
      return None
    }
    out.write_char(Char::from_int(48 + low))
  }
  Some(out.to_string())
}

pub fn pan_from_tlv(items : Array[Tlv]) -> String? {
  match find_tag(items, 0x5a) {
    Some(t) => bcd_to_string(t.value)
    None =>
      match find_tag(items, 0x57) {
        Some(t) =>
          match parse_track2(t.value) {
            Some(track) => Some(track.pan)
            None => None
          }
        None => None
      }
  }
}

pub fn expiry_from_tlv(items : Array[Tlv]) -> String? {
  match find_tag(items, 0x5f24) {
    Some(t) => bcd_to_string(t.value)
    None => None
  }
}

pub fn cardholder_from_tlv(items : Array[Tlv]) -> String? {
  match find_tag(items, 0x5f20) {
    Some(t) => Some(ascii_of(t.value))
    None => None
  }
}

pub fn label_from_tlv(items : Array[Tlv]) -> String? {
  match find_tag(items, 0x50) {
    Some(t) => Some(ascii_of(t.value))
    None => None
  }
}

pub fn card_data_from_tlv(items : Array[Tlv]) -> CardData {
  { pan: pan_from_tlv(items), expiry: expiry_from_tlv(items), cardholder: cardholder_from_tlv(items), label: label_from_tlv(items) }
}

pub fn parse_track2(value : Array[Int]) -> Track2? {
  let text = ascii_of(value)
  let mut sep = -1
  for i in 0.. String {
  let n = pan.length()
  if n <= 10 {
    return pan
  }
  let out = StringBuilder()
  out.write_string(pan.substring(start=0, end=6))
  for _ in 0..<(n - 10) {
    out.write_char('*')
  }
  out.write_string(pan.substring(start=n - 4))
  out.to_string()
}

pub fn format_expiry(yymmdd : String) -> String {
  if yymmdd.length() < 4 {
    yymmdd
  } else {
    "20" + yymmdd.substring(start=0, end=2) + "-" +
    yymmdd.substring(start=2, end=4)
  }
}

fn all_digits(text : String, start : Int, end : Int) -> Bool {
  if start < 0 || end > text.length() || start >= end {
    return false
  }
  for i in start.. 57 {
      return false
    }
  }
  true
}

fn digits_value(text : String, start : Int, end : Int) -> Int {
  let mut value = 0
  for i in start.. Array[String] {
  let issues : Array[String] = Array::new()
  if pan.length() < 12 || pan.length() > 19 {
    issues.push("PAN length must be 12-19 digits")
  } else if !all_digits(pan, 0, pan.length()) {
    issues.push("PAN must contain only digits")
  } else if !luhn_valid(pan) {
    issues.push("PAN fails the Luhn check")
  }
  issues
}

pub fn validate_expiry(yymmdd : String) -> Array[String] {
  let issues : Array[String] = Array::new()
  if yymmdd.length() != 6 || !all_digits(yymmdd, 0, 6) {
    issues.push("expiry must be six digits (YYMMDD)")
  } else {
    let month = digits_value(yymmdd, 2, 4)
    let day = digits_value(yymmdd, 4, 6)
    if month < 1 || month > 12 {
      issues.push("expiry month must be 01-12")
    }
    if day < 1 || day > 31 {
      issues.push("expiry day must be 01-31")
    }
  }
  issues
}

pub fn validate_service_code(code : String) -> Array[String] {
  let issues : Array[String] = Array::new()
  if code.length() != 3 || !all_digits(code, 0, 3) {
    issues.push("service code must be three digits")
  }
  issues
}

pub fn service_code_text(code : String) -> String {
  if code.length() != 3 || !all_digits(code, 0, 3) {
    return "invalid service code"
  }
  let first = code[0].to_int() - 48
  let second = code[1].to_int() - 48
  let third = code[2].to_int() - 48
  let interchange = if first == 1 {
    "international"
  } else if first == 2 {
    "interbank"
  } else if first == 5 {
    "private"
  } else if first == 6 {
    "merchant"
  } else if first == 7 {
    "fuel"
  } else {
    "unknown"
  }
  let authorization = if second == 0 {
    "no restriction"
  } else if second == 2 {
    "normal"
  } else if second == 4 {
    "positive online"
  } else {
    "unknown"
  }
  let security = if third == 0 {
    "none"
  } else if third == 2 {
    "PIN"
  } else if third == 5 {
    "PIN if supported"
  } else {
    "unknown"
  }
  "interchange=" + interchange + ", authorization=" + authorization +
  ", security=" + security
}