///|
fn hex_digit(char : Char) -> Int raise {
  let code = char.to_int()
  if code >= '0'.to_int() && code <= '9'.to_int() {
    code - '0'.to_int()
  } else if code >= 'a'.to_int() && code <= 'f'.to_int() {
    code - 'a'.to_int() + 10
  } else if code >= 'A'.to_int() && code <= 'F'.to_int() {
    code - 'A'.to_int() + 10
  } else {
    fail("invalid hex digit: \{char}")
  }
}

///|
fn decode_hex(input : String) -> Bytes raise {
  let compact = StringBuilder::new()
  for i = 0; i < input.length(); i = i + 1 {
    let ch = input[i]
    if ch != ' ' && ch != '\n' && ch != '\r' && ch != '\t' {
      compact.write_char(Int::unsafe_to_char(ch.to_int()))
    }
  }
  let normalized = compact.to_string()
  if normalized.length() % 2 != 0 {
    fail("hex input must have an even number of digits")
  }
  let buf = Buffer()
  for i = 0; i < normalized.length(); i = i + 2 {
    let hi = hex_digit(Int::unsafe_to_char(normalized[i].to_int()))
    let lo = hex_digit(Int::unsafe_to_char(normalized[i + 1].to_int()))
    buf.write_byte(((hi << 4) | lo).to_byte())
  }
  buf.to_bytes()
}

///|
fn encode_hex(bytes : Bytes) -> String {
  let alphabet = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
  ]
  let builder = StringBuilder::new()
  for i = 0; i < bytes.length(); i = i + 1 {
    let value = bytes[i].to_int()
    builder.write_char(alphabet[value >> 4])
    builder.write_char(alphabet[value & 0x0F])
  }
  builder.to_string()
}

///|
fn sample_value() -> @cbor.CborValue {
  @cbor.Map([
    (@cbor.Text("message"), @cbor.Text("hello cbor")),
    (
      @cbor.Text("numbers"),
      @cbor.Array([@cbor.Integer(1L), @cbor.Integer(2L), @cbor.Integer(3L)]),
    ),
    (@cbor.Text("flag"), @cbor.Simple(21)),
  ])
}

///|
fn print_usage() -> Unit {
  println("moonbit-cbor CLI")
  println("  moon run ./cmd/main")
  println("  moon run ./cmd/main decode-hex ")
  println("  moon run ./cmd/main roundtrip-hex ")
  println("  moon run ./cmd/main benchmark [iterations]")
  println("  moon run ./cmd/main application-demo")
}

///|
fn run_demo() -> Unit {
  let value = sample_value()
  println("Diagnostic:")
  println(@cbor.diagnostic(value))
  let bytes = @cbor.encode(value)
  println("Hex:")
  println(encode_hex(bytes))
}

///|
fn run_decode_hex(hex : String) -> Unit raise {
  let bytes = decode_hex(hex)
  let value = @cbor.decode(bytes) catch { err => fail("decode failed: \{err}") }
  println(@cbor.diagnostic(value))
}

///|
fn run_roundtrip_hex(hex : String) -> Unit raise {
  let bytes = decode_hex(hex)
  let value = @cbor.decode(bytes) catch { err => fail("decode failed: \{err}") }
  let encoded = @cbor.encode(value)
  println("Diagnostic:")
  println(@cbor.diagnostic(value))
  println("Re-encoded hex:")
  println(encode_hex(encoded))
}

///|
fn run_benchmark_case(
  name : String,
  value : @cbor.CborValue,
  iterations : Int,
) -> Unit {
  let encoded = @cbor.encode(value)
  let started = @bench.monotonic_clock_start()
  for _i = 0; _i < iterations; _i = _i + 1 {
    ignore(@cbor.encode(value))
    ignore(
      @cbor.decode(encoded) catch {
        err => abort("benchmark decode failed: \{err}")
      },
    )
  }
  let elapsed_us = @bench.monotonic_clock_end(started)
  println(
    "case=\{name} iterations=\{iterations} payload_bytes=\{encoded.length()} elapsed_us=\{elapsed_us.to_int()} elapsed_ms=\{(elapsed_us / 1000.0).to_string()}",
  )
}

///|
fn benchmark_binary_value() -> @cbor.CborValue {
  let buf = Buffer()
  for i = 0; i < 256; i = i + 1 {
    buf.write_byte((i & 0xFF).to_byte())
  }
  @cbor.Bytes(buf.to_bytes())
}

///|
fn benchmark_utf8_value() -> @cbor.CborValue {
  let builder = StringBuilder::new()
  builder.write_string("MoonBit CBOR ")
  builder.write_char(Int::unsafe_to_char(0x1F642))
  builder.write_string(" RFC 8949")
  @cbor.Text(builder.to_string())
}

///|
fn benchmark_nested_value() -> @cbor.CborValue {
  @cbor.Array([
    sample_value(),
    @cbor.Map([
      (@cbor.Text("payload"), benchmark_binary_value()),
      (
        @cbor.Text("metadata"),
        @cbor.Map([
          (@cbor.Text("version"), @cbor.Integer(1L)),
          (@cbor.Text("stable"), @cbor.Simple(21)),
        ]),
      ),
    ]),
  ])
}

///|
fn run_benchmark(iterations : Int) -> Unit {
  if iterations <= 0 {
    println("iterations must be a positive integer")
    return
  }
  let cases : Array[(String, @cbor.CborValue)] = [
    ("small-map", sample_value()),
    ("utf8-text", benchmark_utf8_value()),
    ("binary-256", benchmark_binary_value()),
    ("nested-value", benchmark_nested_value()),
  ]
  for item in cases {
    run_benchmark_case(item.0, item.1, iterations)
  }
}

///|
fn run_application_demo() -> Unit raise {
  let payload = @cbor.Map([
    (@cbor.Text("user"), @cbor.Text("Ada")),
    (@cbor.Text("token"), @cbor.Text("demo-secret")),
    (
      @cbor.Text("items"),
      @cbor.Array([@cbor.Integer(1L), @cbor.Integer(2L), @cbor.Integer(3L)]),
    ),
  ])
  let request = @cbor.app_request("cli-request", "ingest", payload)
  let response = @cbor.app_response(
    "cli-response",
    "cli-request",
    @cbor.Simple(21),
  )
  let batch = @cbor.app_batch_new(producer="cli-demo", sequence=1UL, messages=[
    request, response,
  ])
  let batch_bytes = @cbor.encode_app_batch(batch)
  let frame_bytes = @cbor.encode_cbor_frames(
    [@cbor.app_batch_to_cbor(batch)],
    4096,
  )
  let rules = [
    @cbor.validation_required_text("user"),
    @cbor.validation_text_length("user", 1, 64),
    @cbor.validation_array_length("items", 1, 16),
  ]
  let report = @cbor.validate_object_payload(payload, rules)
  let user = @cbor.cbor_query_text(payload, [@cbor.Key("user")])
  let redacted = @cbor.cbor_redacted_preview(payload, ["token"], 160)
  println("Application batch:")
  println(@cbor.app_batch_summary(batch))
  println("Batch bytes: \{batch_bytes.length()}")
  println("Framed bytes: \{frame_bytes.length()}")
  println("Query user: \{user}")
  println("Validation: \{@cbor.validation_report_to_string(report)}")
  println("Redacted: \{redacted}")
}

///|
fn parse_iterations(raw : String) -> Int? {
  Some(@string.parse_int(raw)) catch {
    _ => None
  }
}

///|
fn is_command_token(arg : String) -> Bool {
  arg == "decode-hex" ||
  arg == "roundtrip-hex" ||
  arg == "benchmark" ||
  arg == "application-demo" ||
  arg == "--help" ||
  arg == "-h"
}

///|
fn command_index(args : Array[String]) -> Int? {
  for i = 0; i < args.length(); i = i + 1 {
    if is_command_token(args[i]) {
      return Some(i)
    }
  }
  None
}

///|
fn main {
  let args = @env.args()
  match command_index(args) {
    None => run_demo()
    Some(index) =>
      match args[index] {
        "decode-hex" =>
          if index + 1 >= args.length() {
            print_usage()
          } else {
            run_decode_hex(args[index + 1]) catch {
              message => println(message)
            }
          }
        "roundtrip-hex" =>
          if index + 1 >= args.length() {
            print_usage()
          } else {
            run_roundtrip_hex(args[index + 1]) catch {
              message => println(message)
            }
          }
        "benchmark" => {
          let iterations = if index + 1 < args.length() {
            match parse_iterations(args[index + 1]) {
              Some(value) => value
              None => {
                println("iterations must be an integer")
                return
              }
            }
          } else {
            10000
          }
          run_benchmark(iterations)
        }
        "application-demo" =>
          run_application_demo() catch {
            message => println(message)
          }
        "--help" => print_usage()
        "-h" => print_usage()
        _ => print_usage()
      }
  }
}