///|
/// Benchmark harness: `moon run --release benches`.
///
/// Measures apply / diff / merge over object- and array-shaped documents
/// of growing size. Documents are generated deterministically (seeded
/// RNG); every timed closure re-uses the same inputs because the library
/// is immutable — applying a patch never consumes the input document.
fn main {
  let bench = @bench.Bench()
  let sizes = [1_000, 10_000, 100_000]
  // iterations per size: big documents are slower, use fewer repeats
  let counts = [500, 100, 10]
  for i, n in sizes {
    let runs = counts[i]
    // chacha8 requires a 32-byte seed; this one is fixed so runs are
    // reproducible (same documents and patches on every machine)
    let r = @random.Rand::chacha8(seed=b"0123456789abcdef0123456789abcdef")
    // object apply: 100 mixed operations on an n-key object
    let obj = make_object(n)
    let obj_patch = make_object_patch(n, r)
    bench.bench(
      () => ignore(@lib.apply_patch(obj, obj_patch)),
      name="apply/object-\{n} (100 ops)",
      count=runs.reinterpret_as_uint(),
    )
    // array apply: 100 replace+append operations on an n-item array
    let arr = make_array(n)
    let arr_patch = make_array_patch(n, r)
    bench.bench(
      () => ignore(@lib.apply_patch(arr, arr_patch)),
      name="apply/array-\{n} (100 ops)",
      count=runs.reinterpret_as_uint(),
    )
    // diff between two n-key objects (10% changed / 5% removed / 5% added)
    let original = make_object(n)
    let modified = tweaked_object(n)
    bench.bench(
      () => ignore(@lib.diff(original, modified)),
      name="diff/object-\{n}",
      count=runs.reinterpret_as_uint(),
    )
    // diff between two n-item arrays (10% changed, 10% grown)
    let arr_a = make_array(n)
    let arr_b = tweaked_array(n)
    bench.bench(
      () => ignore(@lib.diff(arr_a, arr_b)),
      name="diff/array-\{n}",
      count=runs.reinterpret_as_uint(),
    )
    // merge patch into an object of n/10 sub-objects with 10 members each
    let target = nested_object(n)
    let merge_p = merge_patch_for(n)
    bench.bench(
      () => ignore(@lib.merge_patch(target, merge_p)),
      name="merge/nested-\{n}",
      count=runs.reinterpret_as_uint(),
    )
  }
  println(bench.dump_summaries())
}

///|
/// An object with n members: `k` keys with mixed number/string/bool
/// values, plus `d` keys reserved for remove operations.
fn make_object(n : Int) -> Json {
  let m : Map[String, Json] = Map([])
  for i = 0; i < n; i = i + 1 {
    let v : Json = match i % 3 {
      0 => Json::number(i.to_double())
      1 => Json::string("v\{i}")
      _ => if i % 2 == 0 { Json::boolean(true) } else { Json::boolean(false) }
    }
    m.set("k\{i}", v)
  }
  for i = 0; i < OPS; i = i + 1 {
    m.set("d\{i}", Json::number(i.to_double()))
  }
  Json::object(m)
}

///|
/// An array with n items: mostly numbers, every 4th a string.
fn make_array(n : Int) -> Json {
  let a : Array[Json] = []
  for i = 0; i < n; i = i + 1 {
    if i % 4 == 3 {
      a.push(Json::string("s\{i}"))
    } else {
      a.push(Json::number(i.to_double()))
    }
  }
  Json::array(a)
}

///|
/// A patch with OPS operations over disjoint key families so that every
/// operation sequence is valid: replaces touch `k*`, adds create `new*`,
/// removes delete `d*` (never re-referenced afterwards).
fn make_object_patch(n : Int, r : @random.Rand) -> Json {
  let ops : Array[Json] = []
  for i = 0; i < OPS; i = i + 1 {
    let op : Map[String, Json] = Map([])
    let kind = r.int(limit=3)
    if kind == 0 {
      op.set("op", Json::string("replace"))
      op.set("path", Json::string("/k\{r.int(limit=n)}"))
      op.set("value", Json::number(i.to_double()))
    } else if kind == 1 {
      op.set("op", Json::string("add"))
      op.set("path", Json::string("/new\{i}"))
      op.set("value", Json::string("added-\{i}"))
    } else {
      op.set("op", Json::string("remove"))
      op.set("path", Json::string("/d\{i}"))
    }
    ops.push(Json::object(op))
  }
  Json::array(ops)
}

///|
/// A patch with OPS operations over an array: replaces never shift
/// indices and `-` always appends, so any mix is valid in any order.
fn make_array_patch(n : Int, r : @random.Rand) -> Json {
  let ops : Array[Json] = []
  for i = 0; i < OPS; i = i + 1 {
    let op : Map[String, Json] = Map([])
    if r.int(limit=4) == 0 {
      op.set("op", Json::string("add"))
      op.set("path", Json::string("/-"))
      op.set("value", Json::number(i.to_double()))
    } else {
      op.set("op", Json::string("replace"))
      op.set("path", Json::string("/\{r.int(limit=n)}"))
      op.set("value", Json::number(i.to_double()))
    }
    ops.push(Json::object(op))
  }
  Json::array(ops)
}

///|
/// A deterministic variant of `make_object`: every 10th value changed,
/// every 20th key missing, plus n/20 extra keys.
fn tweaked_object(n : Int) -> Json {
  let m : Map[String, Json] = Map([])
  for i = 0; i < n; i = i + 1 {
    if i % 20 == 5 {
      continue // removed in the modified version
    }
    let v : Json = match i % 3 {
      0 =>
        if i % 10 == 0 {
          Json::number(-1.0)
        } else {
          Json::number(i.to_double())
        }
      1 =>
        if i % 10 == 0 {
          Json::string("changed-\{i}")
        } else {
          Json::string("v\{i}")
        }
      _ => if i % 2 == 0 { Json::boolean(true) } else { Json::boolean(false) }
    }
    m.set("k\{i}", v)
  }
  for i = 0; i < n / 20; i = i + 1 {
    m.set("extra\{i}", Json::string("e\{i}"))
  }
  Json::object(m)
}

///|
/// A deterministic variant of `make_array`: every 10th value changed,
/// plus a 10% longer tail.
fn tweaked_array(n : Int) -> Json {
  let a : Array[Json] = []
  for i = 0; i < n + n / 10; i = i + 1 {
    if i >= n {
      a.push(Json::number(i.to_double()))
    } else if i % 4 == 3 {
      if i % 10 == 3 {
        a.push(Json::string("changed-\{i}"))
      } else {
        a.push(Json::string("s\{i}"))
      }
    } else if i % 10 == 0 {
      a.push(Json::number(-1.0))
    } else {
      a.push(Json::number(i.to_double()))
    }
  }
  Json::array(a)
}

///|
/// An object of n/10 sub-objects with 10 members each — merge patches
/// recurse into these, so this exercises the deep-merge path.
fn nested_object(n : Int) -> Json {
  let m : Map[String, Json] = Map([])
  for i = 0; i < n / 10; i = i + 1 {
    let inner : Map[String, Json] = Map([])
    for j = 0; j < 10; j = j + 1 {
      inner.set("m\{j}", Json::number((i * 10 + j).to_double()))
    }
    m.set("o\{i}", Json::object(inner))
  }
  Json::object(m)
}

///|
/// A merge patch over `nested_object`: replace one member per third
/// sub-object, delete (null) another, and add fresh sub-objects.
fn merge_patch_for(n : Int) -> Json {
  let m : Map[String, Json] = Map([])
  for i = 0; i < n / 30; i = i + 1 {
    let inner : Map[String, Json] = Map([])
    inner.set("m0", Json::number(-1.0))
    inner.set("m1", Json::null())
    m.set("o\{i * 3}", Json::object(inner))
  }
  for i = 0; i < n / 100; i = i + 1 {
    let inner : Map[String, Json] = Map([])
    inner.set("x", Json::string("new-\{i}"))
    m.set("added\{i}", Json::object(inner))
  }
  Json::object(m)
}

///|
/// Patch operations per apply benchmark.
const OPS : Int = 100