// examples/bench — the reproducible performance bench. One run on the §29
// shape (100x768 float32, 307328 bytes on disk) measures four paths through
// the library:
//
//   [1] fs read     @fs.read_file_to_bytes of the on-disk file (warm cache)
//   [2] decode      validate() + payload slice -- no element materialisation
//   [3] decode+to_f32  decode() then to_f32(), the full-load path (76800 floats
//                   out, no pre-allocation in flat_range — this is the measured
//                   status quo — this bench measures, it does not optimise)
//   [4] row pass    100 x to_f32_chunk(k*768, 768) on a pre-decoded array --
//                   the S4 bounded-allocation path (768 floats out per row)
//
// The input file is synthesized in-process (128-byte v1.0 header + 307200-byte
// payload) so a run never depends on fixture files. The payload is all zeros:
// the byte loops under measurement (slice_payload's copy, read_f32) are
// straight-line and data-independent, so zero bytes do not flatter the numbers.
//
// Honest scope: single-machine microbenchmarks, machine-specific -- these are
// NOT cross-library comparisons and must not be quoted as such.
//
// Usage (from the repo root):
//   moon run examples/bench --target native --release
//
// Stats come from core/bench's single_bench (auto batch sizing to ~100ms per
// batch, winsorised 5% tails, microsecond unit), imported as @core_bench because
// this example's own package is named `bench`. Summary is an opaque type, so
// fields are read back through its ToJson representation -- the designed
// public exit.

///|
// NPY v1.0 bytes for the §29 shape, built from the format layout itself rather
// than from a fixture: magic \x93NUMPY + version (1,0) + little-endian uint16
// dict length + dict padded so the magic..dict-end prefix lands on the 64-byte
// boundary (10 + 118 = 128 here). Mirrors the published N2 smoke construction.
fn synthetic_npy_100x768_f32() -> Bytes {
  let mut dict = "{'descr': ' Bytes {
  let res = try @fs.read_file_to_bytes(path) |> Ok catch {
    e => Err(e)
  }
  match res {
    Ok(b) => b
    Err(_) => abort("cannot read \{path} -- run from the repo root")
  }
}

///|
// Pull one numeric field out of a Summary's JSON form. JSON variants are
// matched unqualified because Json lives in builtin (in scope everywhere);
// same form as core's own json README checks.
fn num_field(j : Json, key : String) -> Double {
  let obj = match j {
    Object(m) => m
    _ => abort("summary json is not an object")
  }
  match obj.get(key) {
    Some(Number(n, ..)) => n
    _ => abort("summary json has no numeric field \{key}")
  }
}

///|
// One human-readable line plus one machine-readable JSON line per scenario.
// MB/s = bytes / mean_us (the 1e6 factors of "bytes / 1e6 / (mean_us / 1e6)"
// cancel, so the decimal-MB arithmetic is a straight division).
fn report(name : String, bytes : Int, s : @core_bench.Summary) -> Unit {
  let j = @json.to_json(s)
  let mean = num_field(j, "mean")
  let median = num_field(j, "median")
  let min_us = num_field(j, "min")
  let sd_pct = num_field(j, "std_dev_pct")
  let batch = num_field(j, "batch_size")
  let runs = num_field(j, "runs")
  println(
    "[n3] \{name} | mean \{mean}us | median \{median}us | min \{min_us}us | sd \{sd_pct}% | runs \{runs} x batch \{batch} | \{bytes.to_double() / mean} MB/s",
  )
  println("[n3-json] \{j.stringify()}")
}

///|
fn main {
  println(
    "[n3] moon-npy performance bench -- single-machine microbenchmark, no cross-library claims",
  )
  println("[n3] reproduce: moon run examples/bench --target native --release")
  let path = "_build/bench_100x768_f32.npy"
  let input = synthetic_npy_100x768_f32()
  println(
    "[n3] synthetic input: 100x768 float32 | \{input.length()} bytes (128B header + 307200B payload)",
  )
  let write_res = try @fs.write_bytes_to_file(path, input) |> Ok catch {
    e => Err(e)
  }
  match write_res {
    Ok(_) => ()
    Err(_) => abort("cannot write \{path} -- run from the repo root")
  }
  // Warm the page cache and sanity-check every measured path once before timing.
  let file_bytes = read_or_abort(path)
  if file_bytes.length() != input.length() {
    abort("file size changed between write and read")
  }
  let meta = match @reader.validate(file_bytes) {
    Ok(m) => m
    Err(_) => abort("sanity validate failed")
  }
  if meta.data_offset.to_int() != 128 ||
    meta.payload_len.to_int() != 100 * 768 * 4 {
    abort("unexpected header geometry")
  }
  let arr = match @reader.decode(file_bytes) {
    Ok(a) => a
    Err(_) => abort("sanity decode failed")
  }
  if arr.element_count.to_int() != 100 * 768 {
    abort("unexpected element_count")
  }
  let flat = match arr.to_f32() {
    Ok(v) => v
    Err(_) => abort("sanity to_f32 failed")
  }
  if flat.length() != 100 * 768 {
    abort("unexpected to_f32 length")
  }
  let row0 = match arr.to_f32_chunk(0, 768) {
    Ok(r) => r
    Err(_) => abort("sanity chunk failed")
  }
  let row99 = match arr.to_f32_chunk(99 * 768, 768) {
    Ok(r) => r
    Err(_) => abort("sanity chunk (last row) failed")
  }
  if row0.length() != 768 || row99.length() != 768 {
    abort("unexpected chunk length")
  }
  println(
    "[n3] sanity: validate/decode/to_f32/chunk OK (offset 128, elements 76800, rows 768)",
  )
  // Sinks keep consumed values live across the optimizer (the anti-DCE `keep`
  // pattern from core/bench). The printed values are meaningless sums, not
  // checks — the sanity block above is the correctness gate.
  let mut bytes_sink = 0
  let mut elements_sink : Int64 = 0
  let mut value_sink : Float = 0.0
  let s1 = @core_bench.single_bench(name="fs read", fn() {
    bytes_sink = bytes_sink + read_or_abort(path).length()
  })
  report("fs read (warm cache, 307328B)", file_bytes.length(), s1)
  let s2 = @core_bench.single_bench(name="decode", fn() {
    let a = match @reader.decode(file_bytes) {
      Ok(a) => a
      Err(_) => abort("decode failed in bench")
    }
    elements_sink = elements_sink + a.element_count
  })
  report("decode (validate + slice_payload)", file_bytes.length(), s2)
  let s3 = @core_bench.single_bench(name="decode+to_f32", fn() {
    let a = match @reader.decode(file_bytes) {
      Ok(a) => a
      Err(_) => abort("decode failed in bench")
    }
    let v = match a.to_f32() {
      Ok(v) => v
      Err(_) => abort("to_f32 failed in bench")
    }
    value_sink = value_sink + v[0]
  })
  report("decode + to_f32 (76800 floats out)", file_bytes.length(), s3)
  let s4 = @core_bench.single_bench(name="row pass", fn() {
    for k = 0; k < 100; k = k + 1 {
      let r = match arr.to_f32_chunk(k * 768, 768) {
        Ok(r) => r
        Err(_) => abort("chunk failed in bench")
      }
      value_sink = value_sink + r[0]
    }
  })
  report(
    "row pass 100 x to_f32_chunk(768) (768 floats out/row)",
    100 * 768 * 4,
    s4,
  )
  println(
    "[n3] sinks: \{bytes_sink} | \{elements_sink} | \{value_sink} (anti-DCE, not checks)",
  )
  println("[n3] done")
}