// A small command-line front end for the checkdigit library.
// Every subcommand prints a report and, where the input carries a check
// digit, the digit that would make it correct.

///|
fn main {
  let args = @env.args()
  if args.length() < 3 {
    print_usage()
    return
  }
  let sub = args[1]
  let input = args[2]
  match sub {
    "luhn" => run_luhn(input)
    "verhoeff" => run_verhoeff(input)
    "damm" => run_damm(input)
    "card" => run_card(input)
    "iban" => run_iban(input)
    "imei" => run_imei(input)
    "cnid" => run_cn_id(input)
    "aadhaar" => run_aadhaar(input)
    "vin" => run_vin(input)
    "isin" => run_isin(input)
    "lei" => run_lei(input)
    "uscc" => run_uscc(input)
    "cpf" => run_cpf(input)
    "cnpj" => run_cnpj(input)
    "bsn" => run_bsn(input)
    "cf" => run_cf(input)
    "gtin" => run_gtin(input)
    "upce" => run_upce(input)
    "isbn" => run_isbn(input)
    "issn" => run_issn(input)
    _ => print_usage()
  }
}

///|
fn print_usage() -> Unit {
  println("checkdigit — check digits and identifier validation for MoonBit")
  println("")
  println("usage:")
  println("  checkdigit luhn     ")
  println("  checkdigit verhoeff ")
  println("  checkdigit damm     ")
  println("  checkdigit card     ")
  println("  checkdigit iban     ")
  println("  checkdigit imei     ")
  println("  checkdigit cnid     ")
  println("  checkdigit aadhaar  <12 digits>")
  println("  checkdigit vin      ")
  println("  checkdigit isin     ")
  println("  checkdigit lei      ")
  println("  checkdigit uscc     ")
  println("  checkdigit cpf      ")
  println("  checkdigit cnpj     ")
  println("  checkdigit bsn      <8 or 9 digits>")
  println("  checkdigit cf       ")
  println("  checkdigit gtin     <8, 12, 13 or 14 digits>")
  println("  checkdigit upce     <8-digit upc-e>")
  println("  checkdigit isbn     ")
  println("  checkdigit issn     ")
}

///|
fn show_opt(label : String, v : String?) -> Unit {
  match v {
    Some(x) => println("\{label}: \{x}")
    None => println("\{label}: (none)")
  }
}

///|
fn show_digit(label : String, v : Int?) -> Unit {
  match v {
    Some(x) => println("\{label}: \{x}")
    None => println("\{label}: (not computable)")
  }
}

///|
fn verdict(ok : Bool) -> String {
  if ok {
    "valid"
  } else {
    "invalid"
  }
}

///|
fn run_luhn(s : String) -> Unit {
  println("luhn: \{s}")
  println("status: \{verdict(@checkdigit.luhn_valid(s))}")
  show_digit("check digit (append to input)", @checkdigit.luhn_check_digit(s))
}

///|
fn run_verhoeff(s : String) -> Unit {
  println("verhoeff: \{s}")
  println("status: \{verdict(@checkdigit.verhoeff_valid(s))}")
  show_digit(
    "check digit (append to input)",
    @checkdigit.verhoeff_check_digit(s),
  )
}

///|
fn run_damm(s : String) -> Unit {
  println("damm: \{s}")
  println("status: \{verdict(@checkdigit.damm_valid(s))}")
  show_digit("check digit (append to input)", @checkdigit.damm_check_digit(s))
}

///|
fn run_card(s : String) -> Unit {
  println("card: \{s}")
  println("brand: \{@checkdigit.detect_brand(s).to_string()}")
  println("status: \{verdict(@checkdigit.card_valid(s))}")
}

///|
fn run_iban(s : String) -> Unit {
  println("iban: \{s}")
  show_opt("country", @checkdigit.iban_country(s))
  show_opt("formatted", @checkdigit.iban_format(s))
  println("status: \{verdict(@checkdigit.iban_valid(s))}")
}

///|
fn run_imei(s : String) -> Unit {
  println("imei: \{s}")
  match (@checkdigit.imei_tac(s), @checkdigit.imei_serial(s)) {
    (Some(tac), Some(serial)) => {
      println("tac: \{tac}")
      println("serial: \{serial}")
      // The check digit covers the 14 digits of TAC plus serial.
      show_digit("check digit", @checkdigit.imei_check_digit(tac + serial))
    }
    _ => {
      println("tac: (none)")
      println("serial: (none)")
    }
  }
  println("status: \{verdict(@checkdigit.imei_valid(s))}")
}

///|
/// The first `n` characters of `s`, or None when it is shorter. Used to pull
/// the payload out of a complete identifier so the expected check character
/// can be shown alongside the one that is present.
fn take(s : String, n : Int) -> String? {
  let buf = StringBuilder()
  let mut seen = 0
  for c in s {
    if seen < n {
      buf.write_char(c)
    }
    seen = seen + 1
  }
  if seen < n {
    None
  } else {
    Some(buf.to_string())
  }
}

///|
fn show_check(label : String, body : String?, f : (String) -> String?) -> Unit {
  match body {
    Some(b) => show_opt(label, f(b))
    None => show_opt(label, None)
  }
}

///|
fn run_cn_id(s : String) -> Unit {
  println("cnid: \{s}")
  show_opt("region", @checkdigit.cn_id_region(s))
  show_opt("birth date", @checkdigit.cn_id_birthdate(s))
  show_check(
    "expected check character",
    take(s, 17),
    @checkdigit.cn_id_check_char,
  )
  println("status: \{verdict(@checkdigit.cn_id_valid(s))}")
}

///|
fn run_aadhaar(s : String) -> Unit {
  println("aadhaar: \{s}")
  match take(s, 11) {
    Some(body) =>
      show_digit("expected check digit", @checkdigit.aadhaar_check_digit(body))
    None => show_digit("expected check digit", None)
  }
  println("status: \{verdict(@checkdigit.aadhaar_valid(s))}")
}

///|
fn run_vin(s : String) -> Unit {
  println("vin: \{s}")
  show_opt("wmi", @checkdigit.vin_wmi(s))
  show_opt("vis", @checkdigit.vin_vis(s))
  // The check character sits at position nine, so the whole VIN is the payload.
  show_check(
    "expected check character",
    take(s, 17),
    @checkdigit.vin_check_char,
  )
  println("status: \{verdict(@checkdigit.vin_valid(s))}")
}

///|
fn run_isin(s : String) -> Unit {
  println("isin: \{s}")
  show_opt("country", @checkdigit.isin_country(s))
  match take(s, 11) {
    Some(body) =>
      show_digit("expected check digit", @checkdigit.isin_check_digit(body))
    None => show_digit("expected check digit", None)
  }
  println("status: \{verdict(@checkdigit.isin_valid(s))}")
}

///|
fn run_lei(s : String) -> Unit {
  println("lei: \{s}")
  show_opt("lou", @checkdigit.lei_lou(s))
  show_check("expected check digits", take(s, 18), @checkdigit.lei_check_digits)
  println("status: \{verdict(@checkdigit.lei_valid(s))}")
}

///|
fn run_uscc(s : String) -> Unit {
  println("uscc: \{s}")
  show_opt("division", @checkdigit.uscc_division(s))
  show_check(
    "expected check character",
    take(s, 17),
    @checkdigit.uscc_check_char,
  )
  println("status: \{verdict(@checkdigit.uscc_valid(s))}")
}

///|
fn run_cpf(s : String) -> Unit {
  println("cpf: \{s}")
  show_opt("formatted", @checkdigit.cpf_format(s))
  show_check("expected check digits", take(s, 9), @checkdigit.cpf_check_digits)
  println("status: \{verdict(@checkdigit.cpf_valid(s))}")
}

///|
fn run_cnpj(s : String) -> Unit {
  println("cnpj: \{s}")
  show_opt("formatted", @checkdigit.cnpj_format(s))
  show_check(
    "expected check digits",
    take(s, 12),
    @checkdigit.cnpj_check_digits,
  )
  println("status: \{verdict(@checkdigit.cnpj_valid(s))}")
}

///|
fn run_bsn(s : String) -> Unit {
  println("bsn: \{s}")
  match take(s, 8) {
    Some(body) =>
      show_digit("expected check digit", @checkdigit.bsn_check_digit(body))
    None => show_digit("expected check digit", None)
  }
  println("status: \{verdict(@checkdigit.bsn_valid(s))}")
}

///|
fn run_cf(s : String) -> Unit {
  println("cf: \{s}")
  show_check("expected check character", take(s, 15), @checkdigit.cf_check_char)
  println("status: \{verdict(@checkdigit.cf_valid(s))}")
}

///|
/// Number of characters in `s`.
fn count(s : String) -> Int {
  let mut n = 0
  for c in s {
    let _ = c
    n = n + 1
  }
  n
}

///|
/// A one-character string, for printing a `Char?` as text.
fn cstr(c : Char) -> String {
  let buf = StringBuilder()
  buf.write_char(c)
  buf.to_string()
}

///|
/// Render an optional character.
fn show_char_opt(label : String, v : Char?) -> Unit {
  match v {
    Some(c) => println("\{label}: \{cstr(c)}")
    None => println("\{label}: (none)")
  }
}

///|
fn run_gtin(s : String) -> Unit {
  println("gtin: \{s}")
  let n = count(s)
  if n < 2 {
    show_digit("expected check digit", None)
  } else {
    match take(s, n - 1) {
      Some(body) =>
        show_digit("expected check digit", @checkdigit.gtin_check_digit(body))
      None => show_digit("expected check digit", None)
    }
  }
  println("status: \{verdict(@checkdigit.gtin_valid(s))}")
}

///|
fn run_upce(s : String) -> Unit {
  println("upce: \{s}")
  show_opt("expands to", @checkdigit.upce_expand(s))
  match take(s, 7) {
    Some(body) =>
      show_digit("expected check digit", @checkdigit.upce_check_digit(body))
    None => show_digit("expected check digit", None)
  }
  println("status: \{verdict(@checkdigit.upce_valid(s))}")
}

///|
fn run_isbn(s : String) -> Unit {
  println("isbn: \{s}")
  if count(s) == 10 {
    println("form: isbn-10")
    match take(s, 9) {
      Some(body) =>
        show_char_opt(
          "expected check character",
          @checkdigit.isbn10_check_char(body),
        )
      None => show_char_opt("expected check character", None)
    }
    show_opt("as isbn-13", @checkdigit.isbn10_to_isbn13(s))
    println("status: \{verdict(@checkdigit.isbn10_valid(s))}")
  } else {
    println("form: isbn-13")
    match take(s, 12) {
      Some(body) =>
        show_digit("expected check digit", @checkdigit.isbn13_check_digit(body))
      None => show_digit("expected check digit", None)
    }
    println("status: \{verdict(@checkdigit.isbn13_valid(s))}")
  }
}

///|
fn run_issn(s : String) -> Unit {
  println("issn: \{s}")
  match take(s, 7) {
    Some(body) =>
      show_char_opt("expected check character", @checkdigit.issn_check_char(body))
    None => show_char_opt("expected check character", None)
  }
  println("status: \{verdict(@checkdigit.issn_valid(s))}")
}