///|
/// BiocNeighbors example: Nearest neighbor search for single-cell analysis.
/// Demonstrates KMKNN, Annoy, and BruteForce methods with various distance metrics.
///
/// Run: moon run examples/bioc_neighbors_demo/main.mbt
fn main {
  println("=== BiocNeighbors Demo ===")
  println("")

  // === Create sample data ===
  println("--- 1. Create Sample Data (2D points) ---")

  let data : Array[Array[Double]] = [
    [0.0, 0.0],
    [1.0, 0.0],
    [0.0, 1.0],
    [1.0, 1.0],
    [2.0, 2.0],
    [3.0, 2.0],
    [2.0, 3.0],
    [3.0, 3.0],
    [10.0, 10.0],
    [11.0, 10.0],
    [10.0, 11.0],
    [11.0, 11.0],
  ]
  let n_points = data.length()
  let n_dim = 2

  println("  Created \{n_points} points in \{n_dim}D space")
  for i = 0; i < n_points; i = i + 1 {
    println("    Point \{i}: (\{data[i][0]}, \{data[i][1]})")
  }
  println("")

  // === Brute Force KNN ===
  println("--- 2. Brute Force KNN ---")
  let query : Array[Array[Double]] = [[0.0, 0.0]]
  let result1 = @bio.knn_brute_force(
    data, n_points, n_dim, query, 1, 3, "euclidean",
  )
  println("  Query: (0.0, 0.0)")
  println("  k=3, euclidean distance")
  println("  Found neighbors:")
  for i = 0; i < result1.indices[0].length(); i = i + 1 {
    let idx = result1.indices[0][i]
    let dist = result1.distances[0][i]
    println(
      "    Neighbor \{i}: index=\{idx}, point=(\{data[idx][0]}, \{data[idx][1]}), dist=\{dist}",
    )
  }
  println("")

  // === Build KMKNN Index ===
  println("--- 3. KMKNN (K-Means based KNN) ---")
  let kmknn_opts = @bio.knn_options_full(
    @bio.kmknn_method(),
    3,
    "euclidean",
    50,
    -1,
  )
  let kmknn_index = @bio.build_kmknn_index(data, n_points, n_dim, kmknn_opts)
  println("  Index built: \{kmknn_index.centroids.length()} centroids")
  for c = 0; c < kmknn_index.centroids.length(); c = c + 1 {
    println(
      "    Centroid \{c}: (\{kmknn_index.centroids[c][0]}, \{kmknn_index.centroids[c][1]})",
    )
  }

  let query2 : Array[Array[Double]] = [[0.0, 0.0], [10.0, 10.0]]
  let kmknn_result = @bio.knn_kmknn(kmknn_index, query2, 2, kmknn_opts)
  println("  Query 1: (0.0, 0.0)")
  for i = 0; i < kmknn_result.indices[0].length(); i = i + 1 {
    let idx = kmknn_result.indices[0][i]
    let dist = kmknn_result.distances[0][i]
    println("    Neighbor \{i}: index=\{idx}, dist=\{dist}")
  }
  println("  Query 2: (10.0, 10.0)")
  for i = 0; i < kmknn_result.indices[1].length(); i = i + 1 {
    let idx = kmknn_result.indices[1][i]
    let dist = kmknn_result.distances[1][i]
    println("    Neighbor \{i}: index=\{idx}, dist=\{dist}")
  }
  println("")

  // === Build Annoy Index ===
  println("--- 4. Annoy (Approximate Nearest Neighbors) ---")
  let annoy_opts = @bio.knn_options_full(
    @bio.annoy_method(),
    3,
    "euclidean",
    5,
    -1,
  )
  let annoy_index = @bio.build_annoy_index(data, n_points, n_dim, annoy_opts)
  println(
    "  Index built: \{annoy_index.tree_nodes.length()} tree nodes (5 trees)",
  )

  let annoy_result = @bio.knn_annoy(annoy_index, query2, 2, annoy_opts)
  println("  Query 1: (0.0, 0.0)")
  for i = 0; i < annoy_result.indices[0].length(); i = i + 1 {
    let idx = annoy_result.indices[0][i]
    let dist = annoy_result.distances[0][i]
    println("    Neighbor \{i}: index=\{idx}, dist=\{dist}")
  }
  println("  Query 2: (10.0, 10.0)")
  for i = 0; i < annoy_result.indices[1].length(); i = i + 1 {
    let idx = annoy_result.indices[1][i]
    let dist = annoy_result.distances[1][i]
    println("    Neighbor \{i}: index=\{idx}, dist=\{dist}")
  }
  println("")

  // === Distance Metrics ===
  println("--- 5. Distance Metrics ---")
  let p1 = [1.0, 2.0, 3.0]
  let p2 = [4.0, 6.0, 3.0]
  println("  Point 1: (1.0, 2.0, 3.0)")
  println("  Point 2: (4.0, 6.0, 3.0)")
  println("    Euclidean: \{@bio.euclidean_distance(p1, p2)}")
  println("    Manhattan: \{@bio.manhattan_distance(p1, p2)}")
  println("    Cosine:    \{@bio.cosine_distance(p1, p2)}")
  println("")

  // === Run KNN with Index ===
  println("--- 6. Unified run_knn Interface ---")
  let brute_opts = @bio.knn_options_full(
    @bio.brute_force_method(),
    2,
    "euclidean",
    50,
    -1,
  )
  let brute_index = @bio.build_knn_index(data, n_points, n_dim, brute_opts)
  let query3 : Array[Array[Double]] = [[0.5, 0.5]]
  let final_result = @bio.run_knn(brute_index, query3, 1, brute_opts)
  println("  Query: (0.5, 0.5)")
  println("  k=2, euclidean distance")
  for i = 0; i < final_result.indices[0].length(); i = i + 1 {
    let idx = final_result.indices[0][i]
    let dist = final_result.distances[0][i]
    println(
      "    Neighbor \{i}: index=\{idx}, point=(\{data[idx][0]}, \{data[idx][1]}), dist=\{dist}",
    )
  }
  println("")

  // === Edge Cases ===
  println("--- 7. Edge Cases ---")

  // Empty data
  let empty_data : Array[Array[Double]] = []
  let empty_result = @bio.knn_brute_force(
    empty_data, 0, 2, query, 1, 2, "euclidean",
  )
  println("  Empty data: n_query=\{empty_result.n_query}, k=\{empty_result.k}")

  // k > n_points
  let big_k_result = @bio.knn_brute_force(
    data, n_points, n_dim, query, 1, 100, "euclidean",
  )
  println(
    "  k > n_points: k=\{big_k_result.k}, indices length=\{big_k_result.indices[0].length()}",
  )

  // Single point
  let single_data = [[5.0, 5.0]]
  let single_result = @bio.knn_brute_force(
    single_data, 1, 2, query, 1, 1, "euclidean",
  )
  println(
    "  Single point: k=\{single_result.k}, indices=\{single_result.indices[0][0]}",
  )

  println("")
  println("=== Demo Complete ===")
}