// Payment card validation: brand detection by issuer identification number
// (IIN/BIN) ranges, then length and Luhn checks.

///|
/// Recognised card networks. `Unknown` means no issuer range matched.
pub enum CardBrand {
  Visa
  Mastercard
  Amex
  Discover
  Jcb
  DinersClub
  UnionPay
  Maestro
  Unknown
}

///|
pub impl Show for CardBrand with fn to_string(self) -> String {
  match self {
    Visa => "visa"
    Mastercard => "mastercard"
    Amex => "amex"
    Discover => "discover"
    Jcb => "jcb"
    DinersClub => "diners-club"
    UnionPay => "unionpay"
    Maestro => "maestro"
    Unknown => "unknown"
  }
}

///|
/// Read the first `n` characters as an integer. None if there are too few
/// characters or any of them is not a digit.
fn leading_int(s : String, n : Int) -> Int? {
  let chars = chars_of(s)
  if chars.length() < n {
    return None
  }
  let mut v = 0
  for i = 0; i < n; i = i + 1 {
    let c = chars[i]
    if c < '0' || c > '9' {
      return None
    }
    v = v * 10 + (c.to_int() - '0'.to_int())
  }
  Some(v)
}

///|
/// True when the first `n` digits fall in `[lo, hi]`.
fn prefix_in(s : String, n : Int, lo : Int, hi : Int) -> Bool {
  match leading_int(s, n) {
    Some(v) => v >= lo && v <= hi
    None => false
  }
}

///|
/// Number of digits, or None if the string holds anything else.
fn digit_count(s : String) -> Int? {
  match digits_of(s) {
    Some(d) => Some(d.length())
    None => None
  }
}

///|
/// Identify the card network from its issuer range. The most specific ranges
/// are tested first (Discover's 622126-622925 range overlaps UnionPay's 62).
pub fn detect_brand(s : String) -> CardBrand {
  if prefix_in(s, 2, 34, 34) || prefix_in(s, 2, 37, 37) {
    return Amex
  }
  let discover = prefix_in(s, 6, 622126, 622925) ||
    prefix_in(s, 4, 6011, 6011) ||
    prefix_in(s, 2, 65, 65) ||
    prefix_in(s, 3, 644, 649)
  if discover {
    return Discover
  }
  if prefix_in(s, 4, 3528, 3589) {
    return Jcb
  }
  let diners = prefix_in(s, 3, 300, 305) ||
    prefix_in(s, 2, 36, 36) ||
    prefix_in(s, 2, 38, 39)
  if diners {
    return DinersClub
  }
  let maestro = prefix_in(s, 4, 5018, 5018) ||
    prefix_in(s, 4, 5020, 5020) ||
    prefix_in(s, 4, 5038, 5038) ||
    prefix_in(s, 4, 5893, 5893) ||
    prefix_in(s, 4, 6304, 6304) ||
    prefix_in(s, 4, 6759, 6759) ||
    prefix_in(s, 4, 6761, 6763)
  if maestro {
    return Maestro
  }
  if prefix_in(s, 2, 51, 55) || prefix_in(s, 4, 2221, 2720) {
    return Mastercard
  }
  if prefix_in(s, 1, 4, 4) {
    return Visa
  }
  if prefix_in(s, 2, 62, 62) {
    return UnionPay
  }
  Unknown
}

///|
/// Whether `len` digits is a valid length for this brand.
fn brand_length_ok(brand : CardBrand, len : Int) -> Bool {
  match brand {
    Visa => len == 13 || len == 16 || len == 19
    Mastercard => len == 16
    Amex => len == 15
    Discover => len == 16 || len == 19
    Jcb => len >= 16 && len <= 19
    DinersClub => len == 14 || len == 16 || len == 19
    UnionPay => len >= 16 && len <= 19
    Maestro => len >= 12 && len <= 19
    Unknown => false
  }
}

///|
/// Full card check: a known brand, a valid length for that brand, and a
/// correct Luhn check digit.
pub fn card_valid(s : String) -> Bool {
  let len = match digit_count(s) {
    None => return false
    Some(n) => n
  }
  brand_length_ok(detect_brand(s), len) && luhn_valid(s)
}

///|
/// Group the digits into blocks of four for display, e.g.
/// "3782 8224 6310 005". Returns None unless every character is a digit.
pub fn card_format(s : String) -> String? {
  if digits_of(s) is None {
    return None
  }
  let buf = StringBuilder()
  let mut i = 0
  for ch in s {
    if i > 0 && i % 4 == 0 {
      buf.write_char(' ')
    }
    buf.write_char(ch)
    i = i + 1
  }
  Some(buf.to_string())
}