///|
fn run_iterations(
  fixture : Fixture,
  operation : Operation,
  iterations : Int32,
) -> UInt32 raise @capnp.CapnpError {
  let mut checksum = 0U
  for i in 0.. UInt32 {
  first * ((iterations + 1) / 2).reinterpret_as_uint() +
  second * (iterations / 2).reinterpret_as_uint()
}

///|
fn measured_batch(
  fixture : Fixture,
  operation : Operation,
  iterations : Int32,
  first : UInt32,
  second : UInt32,
) -> (Double, UInt32) raise {
  let start = @bench.monotonic_clock_start()
  let checksum = run_iterations(fixture, operation, iterations)
  let elapsed_us = @bench.monotonic_clock_end(start)
  assert_eq(checksum, expected_checksum(first, second, iterations))
  assert_true(elapsed_us >= 0.0 && elapsed_us < 1.0e15)
  (elapsed_us, checksum)
}

///|
fn measure(
  fixture : Fixture,
  operation : Operation,
  round_ms : Int32,
  warmup_ms : Int32,
  rounds : Int32,
) -> Unit raise {
  let first = run_one(fixture, operation, 0)
  let second = run_one(fixture, operation, 1)
  let target_us = round_ms.to_double() * 1000.0
  let mut iterations = 1
  let mut calibration_us = 0.0
  // Calibration and warmup are excluded from the reported rounds. The cap
  // bounds work on unexpectedly coarse/broken clocks; zero timing is an error.
  while iterations <= 16777216 {
    let (elapsed, _) = measured_batch(
      fixture, operation, iterations, first, second,
    )
    calibration_us = elapsed
    if elapsed >= target_us || iterations == 16777216 {
      break
    }
    iterations *= 2
  }
  assert_true(calibration_us > 0.0)
  let warmup_start = @bench.monotonic_clock_start()
  let mut warmup_batches = 0
  while @bench.monotonic_clock_end(warmup_start) <
        warmup_ms.to_double() * 1000.0 {
    let (_, _) = measured_batch(fixture, operation, iterations, first, second)
    warmup_batches += 1
  }
  let warmup_us = @bench.monotonic_clock_end(warmup_start)
  // JS may optimize during warmup. Recheck the batch size with the warmed
  // code before fixing the iteration count for all reported rounds.
  while iterations <= 16777216 {
    let (elapsed, _) = measured_batch(
      fixture, operation, iterations, first, second,
    )
    calibration_us = elapsed
    if elapsed >= target_us || iterations == 16777216 {
      break
    }
    iterations *= 2
  }
  let times : Array[Double] = []
  let checksums : Array[UInt32] = []
  for _ in 0.. 0.0)
    times.push(elapsed)
    checksums.push(checksum)
  }
  let sorted = times.copy()
  sorted.sort()
  let median_us = sorted[rounds / 2]
  let ns_per_op = median_us * 1000.0 / iterations.to_double()
  let wire_bytes = fixture.plain[0].length()
  let packed_bytes = fixture.packed[0].length()
  let input_bytes = match operation {
    Encode => 0
    Unpack => packed_bytes
    _ => wire_bytes
  }
  let rate = 1000000.0 * iterations.to_double() / median_us
  let wire_mib_per_second = rate * wire_bytes.to_double() / 1048576.0
  let raw_times = times.map(t => t.to_string()).join(",")
  let raw_checksums = checksums.map(c => c.to_string()).join(",")
  println(
    "{\"operation\":\"\{operation.name()}\",\"sample_count\":\{fixture.samples.length()},\"wire_bytes\":\{wire_bytes},\"packed_bytes\":\{packed_bytes},\"input_bytes\":\{input_bytes},\"iterations\":\{iterations},\"round_target_ms\":\{round_ms},\"calibration_us\":\{calibration_us},\"warmup_us\":\{warmup_us},\"warmup_batches\":\{warmup_batches},\"round_us\":[\{raw_times}],\"checksums\":[\{raw_checksums}],\"variant_checksums\":[\{first},\{second}],\"median_ns_per_op\":\{ns_per_op},\"operations_per_second\":\{rate},\"wire_mib_per_second\":\{wire_mib_per_second}}",
  )
}

///|
fn parse_positive(text : String, maximum : Int32) -> Int32? {
  if text.length() == 0 {
    return None
  }
  let mut result = 0
  for c in text {
    let digit = c.to_int() - '0'.to_int()
    if digit < 0 || digit > 9 || result > (maximum - digit) / 10 {
      return None
    }
    result = result * 10 + digit
  }
  if result < 1 || result > maximum {
    None
  } else {
    Some(result)
  }
}

///|
fn main raise {
  let args = @stdio.args()
  guard args.length() == 5 else {
    println("Usage: benchmarks SAMPLE_COUNT ROUND_MS WARMUP_MS ODD_ROUNDS")
    @stdio.exit(2)
    return
  }
  guard (
      parse_positive(args[1], 65536),
      parse_positive(args[2], 60000),
      parse_positive(args[3], 60000),
      parse_positive(args[4], 99),
    )
    is (Some(count), Some(round_ms), Some(warmup_ms), Some(rounds)) &&
    rounds >= 3 &&
    rounds % 2 == 1 else {
    println(
      "Invalid arguments: samples 1..65536; round/warmup ms 1..60000; odd rounds 3..99",
    )
    @stdio.exit(2)
    return
  }
  let fixture = make_fixture(count)
  for operation in [Encode, SelectedRead, FullScan, Pack, Unpack, OpaqueCopy] {
    measure(fixture, operation, round_ms, warmup_ms, rounds)
  }
}