// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Throughput for the one-shot codecs: compress and decompress a fixed body and
/// report megabytes per second, taking the best of several runs after a warm-up.
/// The body is a deterministic mix of words and numbers that compresses about
/// three to one — close to text and source — and the generator is written out
/// below rather than loaded, so the same bytes can be fed to an external zlib for
/// an honest comparison.
///
///     moon run src/examples/throughput --target native --release
///
/// The numbers are machine-specific and the build type matters — a debug build is
/// several times slower than a release one — so quote release-build numbers and
/// say so. `--release` is the flag that produces them.
fn corpus() -> Bytes {
  // A deterministic mix of words and numbers, so it compresses about three to
  // one — close to text and source — rather than the two-hundred-to-one a body
  // of one repeated sentence reaches, where the rate measures memcpy more than
  // a decoder. The generator is written out here so an external zlib can be fed
  // exactly the same bytes and the comparison is honest.
  let words = [
    "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota",
    "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi",
  ]
  let out : Array[Byte] = []
  let mut state : UInt32 = 123456789U
  while out.length() < 1500000 {
    state = state * 1103515245U + 12345U
    let word = words[(state >> 16).reinterpret_as_int() % 16]
    for b in @utf8.encode(word) {
      out.push(b)
    }
    out.push(b' ')
    let n = ((state >> 8) % 1000U).reinterpret_as_int()
    for ch in n.to_string() {
      out.push((ch.to_int() & 0xFF).to_byte())
    }
    out.push(b' ')
  }
  Bytes::from_array(out)
}

///|
/// The median of ten timed runs, in microseconds, after one warm-up.
///
/// A median rather than a best: the best run on a shared machine is the one that
/// happened to have the machine to itself, which flatters every figure on the
/// page equally and tells a reader nothing about what they will see.
fn median(action : () -> Unit raise) -> Double raise {
  action()
  let runs : Array[Double] = []
  for _ in 0..<10 {
    let start = @bench.monotonic_clock_start()
    action()
    runs.push(@bench.monotonic_clock_end(start))
  }
  runs.sort()
  (runs[4] + runs[5]) / 2.0
}

///|
fn report(name : String, bytes : Int32, micros : Double) -> Unit {
  // Bytes per microsecond is megabytes per second: 1e6 bytes over 1e6 µs.
  let rate = if micros <= 0.0 { 0.0 } else { bytes.to_double() / micros }
  println("\{name}: \{rate} MB/s")
}

///|
fn main raise {
  let data = corpus()
  let n = data.length()
  println("body: \{n} bytes of words and numbers")
  let mut d = b""
  report("deflate write", n, median(fn() { d = @deflate.deflate(data[:]) }))
  let mut db = b""
  report("deflate read ", n, median(fn() raise { db = @deflate.inflate(d[:]) }))
  let mut g = b""
  report("gzip    write", n, median(fn() { g = @gzip.gzip(data[:]) }))
  let mut gb = b""
  report("gzip    read ", n, median(fn() raise { gb = @gzip.gunzip(g[:]) }))
  let mut z = b""
  report("zlib    write", n, median(fn() { z = @zlib.zlib(data[:]) }))
  let mut zb = b""
  report("zlib    read ", n, median(fn() raise { zb = @zlib.unzlib(z[:]) }))
  let mut s = b""
  report("zstd    write", n, median(fn() { s = @zstd.zstd(data[:]) }))
  let mut sb = b""
  report("zstd    read ", n, median(fn() raise { sb = @zstd.unzstd(s[:]) }))
  let mut br = b""
  report("brotli  write", n, median(fn() { br = @brotli.brotli(data[:]) }))
  let mut brb = b""
  report(
    "brotli  read ",
    n,
    median(fn() raise { brb = @brotli.unbrotli(br[:]) }),
  )
  if db.length() != n ||
    gb.length() != n ||
    zb.length() != n ||
    sb.length() != n ||
    brb.length() != n {
    abort("a round trip did not return the body")
  }
}