// 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"
  }
}

///|
/// `Show` has two methods, `output` and `to_string`, and `#must_implement_one`
/// lets an `impl` supply either. The one it leaves to the default is still
/// promoted onto `CardBrand` as a regular method, which the compiler warns
/// about (E0079) because a later refactor could drop it without notice. Both
/// are named here so the promotion is stated rather than implied.
pub extend CardBrand with Show::{output, to_string}

///|
/// 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())
}