// Runnable example: a toy search-engine inverted index using RoaringBitmap
// for postings lists. Demonstrates the three things this library is built
// to do well:
//   1. Fast set intersection/union for multi-term queries
//   2. Automatic compression (a term that appears in a long consecutive
//      run of document IDs collapses into a handful of bytes)
//   3. Portable serialization, so the index built here could be written to
//      disk or shipped to a service in another language
//
// Run with: moon run examples/inverted_index

///|
fn build_postings(
  term : String,
  doc_ids : Array[UInt],
) -> (String, @roaring.RoaringBitmap) {
  (term, @roaring.RoaringBitmap::from_array(doc_ids))
}

///|
fn main {
  // Pretend corpus: doc IDs 0..999. "machine" and "learning" co-occur in a
  // consecutive block of documents (e.g. a single imported dataset), while
  // "rust" is scattered across the corpus.
  let machine_docs = []
  for i = 100; i < 400; i = i + 1 {
    machine_docs.push(i.reinterpret_as_uint())
  }
  let learning_docs = []
  for i = 250; i < 500; i = i + 1 {
    learning_docs.push(i.reinterpret_as_uint())
  }
  let rust_docs = [5U, 42U, 250U, 300U, 999U]

  let (_, machine) = build_postings("machine", machine_docs)
  let (_, learning) = build_postings("learning", learning_docs)
  let (_, rust) = build_postings("rust", rust_docs)

  println("Postings list sizes:")
  println("  machine:  \{machine.size()} docs")
  println("  learning: \{learning.size()} docs")
  println("  rust:     \{rust.size()} docs")
  println("")

  // Query: "machine" AND "learning" -> documents containing both terms.
  let machine_and_learning = machine.intersect(learning)
  println("\"machine\" AND \"learning\": \{machine_and_learning.size()} docs")
  println(
    "  range: [\{machine_and_learning.min().unwrap()}, \{machine_and_learning.max().unwrap()}]",
  )

  // Query: "machine" AND "learning" AND "rust" -> narrow further.
  let all_three = machine_and_learning.intersect(rust)
  println("\"machine\" AND \"learning\" AND \"rust\": \{all_three.size()} docs")
  let ids = all_three.to_array().map(fn(v) { v.to_string() })
  println("  doc IDs: [\{ids.join(", ")}]")
  println("")

  // Query: "machine" OR "rust" -> documents containing either term.
  let machine_or_rust = machine.union(rust)
  println("\"machine\" OR \"rust\": \{machine_or_rust.size()} docs")
  println("")

  // Compression check: the "machine" postings list is 300 consecutive
  // document IDs. Confirm it collapsed into a Run container instead of
  // paying for an 8KB bitmap or a 600-byte array.
  let stats = machine.get_stats()
  println("\"machine\" postings internal representation:")
  println(
    "  containers: \{stats.total_containers} (array=\{stats.array_containers}, bitmap=\{stats.bitmap_containers}, run=\{stats.run_containers})",
  )
  println(
    "  bytes used: \{stats.total_bytes_used} (vs. \{machine.size() * 2} bytes as a plain UInt16 array)",
  )
  println("")

  // Portability check: serialize to the CRoaring-compatible format and
  // read it back. In a real system this is what lets a Rust or Go service
  // consume the same postings list this MoonBit indexer produced.
  let bytes = machine.serialize()
  println(
    "Serialized \"machine\" postings to \{bytes.length()} bytes (portable format)",
  )
  let restored = match @roaring.RoaringBitmap::deserialize(bytes) {
    Ok(r) => r
    Err(e) => {
      println("deserialize failed: \{e}")
      panic()
    }
  }
  println("Round-trip check: \{restored.equals(machine)}")
}