///|
pub(all) struct Spec {
  layout : Layout
  document_code : String
  issuer : String
  primary_name : String
  secondary_name : String
  document_number : String
  nationality : String
  birth : String
  sex : String
  expiry : String
  optional : String
  optional_secondary : String
} derive(Eq, Debug)

///|
fn encode_code(code : String) -> String {
  pad_fillers(upper_ascii(spaces_to_fillers(trim_ascii(code))), 2)
}

///|
fn encode_alpha3(code : String) -> String {
  pad_fillers(upper_ascii(spaces_to_fillers(trim_ascii(code))), 3)
}

///|
fn encode_name_field(
  primary : String,
  secondary : String,
  width : Int,
) -> String {
  let p = spaces_to_fillers(upper_ascii(trim_ascii(primary)))
  let s = spaces_to_fillers(upper_ascii(trim_ascii(secondary)))
  let body = if s.length() == 0 { p } else { p + "<<" + s }
  pad_fillers(body, width)
}

///|
fn encode_doc(num : String) -> String {
  pad_fillers(upper_ascii(spaces_to_fillers(trim_ascii(num))), 9)
}

///|
fn encode_sex(sex : String) -> String {
  let t = upper_ascii(trim_ascii(sex))
  if t.length() == 0 {
    "<"
  } else {
    unit_at(t, 0)
  }
}

///|
fn must_digit(field : String) -> String {
  match check_digit_char(field) {
    None => "0"
    Some(ch) => ch
  }
}

///|
fn build_td3(spec : Spec, diags : Array[Diagnostic]) -> Array[String] {
  let l1 = encode_code(spec.document_code) +
    encode_alpha3(spec.issuer) +
    encode_name_field(spec.primary_name, spec.secondary_name, 39)
  let doc = encode_doc(spec.document_number)
  let birth = pad_fillers(spec.birth, 6)
  let expiry = pad_fillers(spec.expiry, 6)
  let optional = pad_fillers(
    upper_ascii(spaces_to_fillers(trim_ascii(spec.optional))),
    14,
  )
  let sex = encode_sex(spec.sex)
  let doc_cd = must_digit(doc)
  let birth_cd = must_digit(birth)
  let expiry_cd = must_digit(expiry)
  let optional_cd = must_digit(optional)
  let composite = doc +
    doc_cd +
    birth +
    birth_cd +
    expiry +
    expiry_cd +
    optional +
    optional_cd
  let l2 = doc +
    doc_cd +
    encode_alpha3(spec.nationality) +
    birth +
    birth_cd +
    sex +
    expiry +
    expiry_cd +
    optional +
    optional_cd +
    must_digit(composite)
  if spec.primary_name.length() == 0 {
    diags.push(error_diag("MRZ013", "primary name is missing", 1, 6, 5, 44))
  }
  if !is_valid_yymmdd(birth) {
    diags.push(
      error_diag(
        "MRZ010", "date of birth is not a valid YYMMDD value", 2, 14, 13, 19,
      ),
    )
  }
  if !is_valid_yymmdd(expiry) {
    diags.push(
      error_diag(
        "MRZ010", "date of expiry is not a valid YYMMDD value", 2, 22, 21, 27,
      ),
    )
  }
  if !is_valid_sex(sex) {
    diags.push(error_diag("MRZ011", "sex must be M, F, or <", 2, 21, 20, 21))
  }
  [l1, l2]
}

///|
fn build_td2(spec : Spec, diags : Array[Diagnostic]) -> Array[String] {
  let l1 = encode_code(spec.document_code) +
    encode_alpha3(spec.issuer) +
    encode_name_field(spec.primary_name, spec.secondary_name, 31)
  let doc = encode_doc(spec.document_number)
  let birth = pad_fillers(spec.birth, 6)
  let expiry = pad_fillers(spec.expiry, 6)
  let optional = pad_fillers(
    upper_ascii(spaces_to_fillers(trim_ascii(spec.optional))),
    7,
  )
  let sex = encode_sex(spec.sex)
  let doc_cd = must_digit(doc)
  let birth_cd = must_digit(birth)
  let expiry_cd = must_digit(expiry)
  let composite = doc +
    doc_cd +
    birth +
    birth_cd +
    expiry +
    expiry_cd +
    optional
  if spec.primary_name.length() == 0 {
    diags.push(error_diag("MRZ013", "primary name is missing", 1, 6, 5, 36))
  }
  if !is_valid_yymmdd(birth) {
    diags.push(
      error_diag(
        "MRZ010", "date of birth is not a valid YYMMDD value", 2, 14, 13, 19,
      ),
    )
  }
  [
    l1,
    doc +
    doc_cd +
    encode_alpha3(spec.nationality) +
    birth +
    birth_cd +
    sex +
    expiry +
    expiry_cd +
    optional +
    must_digit(composite),
  ]
}

///|
fn build_td1(spec : Spec, diags : Array[Diagnostic]) -> Array[String] {
  let doc = encode_doc(spec.document_number)
  let opt1 = pad_fillers(
    upper_ascii(spaces_to_fillers(trim_ascii(spec.optional))),
    15,
  )
  let l1 = encode_code(spec.document_code) +
    encode_alpha3(spec.issuer) +
    doc +
    must_digit(doc) +
    opt1
  let birth = pad_fillers(spec.birth, 6)
  let expiry = pad_fillers(spec.expiry, 6)
  let opt2 = pad_fillers(
    upper_ascii(spaces_to_fillers(trim_ascii(spec.optional_secondary))),
    11,
  )
  let sex = encode_sex(spec.sex)
  let l2body = birth +
    must_digit(birth) +
    sex +
    expiry +
    must_digit(expiry) +
    encode_alpha3(spec.nationality) +
    opt2
  let composite = l1[5:30].to_owned() +
    l2body[0:7].to_owned() +
    l2body[8:15].to_owned() +
    l2body[18:29].to_owned()
  let l2 = l2body + must_digit(composite)
  let l3 = encode_name_field(spec.primary_name, spec.secondary_name, 30)
  if spec.primary_name.length() == 0 {
    diags.push(error_diag("MRZ013", "primary name is missing", 3, 1, 0, 30))
  }
  if !is_valid_yymmdd(birth) {
    diags.push(
      error_diag(
        "MRZ010", "date of birth is not a valid YYMMDD value", 2, 1, 0, 6,
      ),
    )
  }
  [l1, l2, l3]
}

///|
/// Build canonical MRZ lines from structured fields. Check digits are computed.
pub fn build(spec : Spec) -> (String, Array[Diagnostic]) {
  let diags : Array[Diagnostic] = []
  if spec.layout != TD1 && spec.optional_secondary.length() > 0 {
    diags.push(
      warning_diag(
        "MRZ016", "optional_secondary is only encoded for TD1", 1, 1, 0, 0,
      ),
    )
  }
  let lines = match spec.layout {
    TD3 => build_td3(spec, diags)
    TD2 => build_td2(spec, diags)
    TD1 => build_td1(spec, diags)
  }
  let mut out = lines[0]
  let mut i = 1
  while i < lines.length() {
    out = out + "\n" + lines[i]
    i += 1
  }
  (out, diags)
}

///|
pub fn build_text(spec : Spec) -> String? {
  let (text, diags) = build(spec)
  let mut i = 0
  while i < diags.length() {
    if diags[i].severity == Error {
      return None
    }
    i += 1
  }
  Some(text)
}