///|
/// `sms-pdu` decodes and builds SMS PDUs from the command line.
///
///   sms-pdu decode                   print a decoded PDU
///   sms-pdu encode          build an SMS-SUBMIT
///   sms-pdu parts           split a long message, one PDU per line
///   sms-pdu length                  report how many parts and septets
fn main {
  let args = @env.args()
  if args.length() < 2 {
    print_usage()
    return
  }
  match args[1] {
    "decode" =>
      if args.length() < 3 {
        println("decode needs a hex string")
      } else {
        decode_cmd(args[2])
      }
    "encode" =>
      if args.length() < 4 {
        println("encode needs a number and a message")
      } else {
        encode_cmd(args[2], args[3])
      }
    "parts" =>
      if args.length() < 4 {
        println("parts needs a number and a message")
      } else {
        parts_cmd(args[2], args[3])
      }
    "length" =>
      if args.length() < 3 {
        println("length needs a message")
      } else {
        length_cmd(args[2])
      }
    "summary" =>
      if args.length() < 3 {
        println("summary needs at least one hex pdu")
      } else {
        summary_cmd(args[2:].to_owned())
      }
    "stats" =>
      if args.length() < 3 {
        println("stats needs at least one hex pdu")
      } else {
        stats_cmd(args[2:].to_owned())
      }
    "verify" =>
      if args.length() < 3 {
        println("verify needs at least one hex pdu")
      } else {
        verify_cmd(args[2:].to_owned())
      }
    "ussd" =>
      if args.length() < 3 {
        println("ussd needs a code, for example *100#")
      } else {
        ussd_cmd(args[2])
      }
    "-h" | "--help" | "help" => print_usage()
    other => println("unknown command: \{other}")
  }
}

///|
fn print_usage() -> Unit {
  println("sms-pdu — SMS PDU codec")
  println("")
  println(
    "  decode               decode an SMS-DELIVER, SUBMIT or STATUS-REPORT",
  )
  println("  encode      build an SMS-SUBMIT PDU")
  println("  parts       split a long message into PDUs")
  println("  length              report part count and septet usage")
  println("  summary ...          one line per PDU")
  println("  stats ...            tally a batch of PDUs")
  println(
    "  verify ...           re-encode each PDU and report differences",
  )
  println("  ussd                encode a USSD string, for example *100#")
}

///|
fn describe_pdu(pdu : @lib.Pdu) -> Unit {
  println(pdu.to_display())
}

///|
fn decode_cmd(hex : String) -> Unit {
  match (Ok(@lib.parse_pdu(hex)) catch { e => Err(e) }) {
    Ok(pdu) => {
      describe_pdu(pdu)
      match (Ok(pdu.to_hex()) catch { e => Err(e) }) {
        Ok(reencoded) => println("re-encoded:        \{reencoded}")
        Err(e) => println("re-encode failed:  \{e.to_message()}")
      }
    }
    Err(e) => println("decode failed: \{e.to_message()}")
  }
}

///|
fn parse_number(text : String) -> @lib.Address {
  if text.has_prefix("+") {
    @lib.Number(@lib.International, @lib.Isdn, text[1:].to_owned())
  } else {
    let (type_, plan, digits) = @lib.classify_digits(text)
    @lib.Number(type_, plan, digits)
  }
}

///|
fn encode_cmd(number : String, text : String) -> Unit {
  let encoding = if @lib.gsm7_is_encodable(text) {
    @lib.Gsm7
  } else {
    @lib.Ucs2
  }
  let submit = @lib.SmsSubmit::new(parse_number(number), text, encoding~)
  match (Ok(@lib.Pdu::Submit(submit).to_hex()) catch { e => Err(e) }) {
    Ok(hex) => {
      println(hex)
      println("")
      println("encoding: \{encoding}")
      println("septets:  \{@lib.gsm7_septet_length(text)}")
    }
    Err(e) => println("encode failed: \{e.to_message()}")
  }
}

///|
fn parts_cmd(number : String, text : String) -> Unit {
  let encoding = if @lib.gsm7_is_encodable(text) {
    @lib.Gsm7
  } else {
    @lib.Ucs2
  }
  match
    (Ok(@lib.build_submits(parse_number(number), text, 42, encoding~)) catch {
      e => Err(e)
    }) {
    Ok(submits) => {
      println("// \{submits.length()} part(s), \{encoding}")
      for submit in submits {
        match (Ok(@lib.Pdu::Submit(submit).to_hex()) catch { e => Err(e) }) {
          Ok(hex) => println(hex)
          Err(e) => println("// encode failed: \{e.to_message()}")
        }
      }
    }
    Err(e) => println("failed: \{e.to_message()}")
  }
}

///|
fn length_cmd(text : String) -> Unit {
  if @lib.gsm7_is_encodable(text) {
    let septets = @lib.gsm7_septet_length(text)
    let parts = (septets + 152) / 153
    println("encoding: GSM 7-bit")
    println("septets:  \{septets}")
    println("parts:    \{if septets <= 160 { 1 } else { parts }}")
  } else {
    let units = text.char_length()
    let parts = (units * 2 + 133) / 134
    println("encoding: UCS-2")
    println("units:    \{units}")
    println("parts:    \{if units <= 70 { 1 } else { parts }}")
  }
}

///|
fn ussd_cmd(code : String) -> Unit {
  let message = @lib.UssdMessage::new(code)
  match (Ok(message.to_octets() |> @lib.octets_to_hex) catch { e => Err(e) }) {
    Ok(hex) => {
      println(hex)
      println("")
      println("encoding: \{message.encoding}")
    }
    Err(e) => println("encode failed: \{e.to_message()}")
  }
}

///|
/// Joins the remaining arguments into one capture-shaped text, so a caller can
/// paste several PDUs or one whole list.
fn collect(parts : Array[String]) -> String {
  let buf = StringBuilder()
  for part in parts {
    buf.write_string(part)
    buf.write_char('\n')
  }
  buf.to_string()
}

///|
fn summary_cmd(parts : Array[String]) -> Unit {
  let (pdus, errors) = @lib.parse_pdu_lines(collect(parts))
  for pdu in pdus {
    println(pdu.summary())
  }
  for error in errors {
    println("!: \{error}")
  }
}

///|
fn stats_cmd(parts : Array[String]) -> Unit {
  let (pdus, errors) = @lib.parse_pdu_lines(collect(parts))
  println(@lib.stats_of(pdus).to_display())
  if errors.length() > 0 {
    println("\{errors.length()} line(s) could not be parsed")
  }
}

///|
fn verify_cmd(parts : Array[String]) -> Unit {
  let problems = @lib.audit_lines(collect(parts))
  if problems.length() == 0 {
    println("all pdus round-trip identically")
  } else {
    for problem in problems {
      println(problem)
    }
  }
}