//! Scalar `Index2DF32`: native f32 build, conservative superset of `Index2D`.
#![cfg(feature = "f32-storage")]

use std::collections::HashSet;

use packed_spatial_index::{Box2D, Index2DBuilder, Triangle2D};
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};

fn build_both(
    seed: u64,
    n: usize,
) -> (
    packed_spatial_index::Index2D,
    packed_spatial_index::Index2DF32,
) {
    let mut rng = StdRng::seed_from_u64(seed);
    let mut b1 = Index2DBuilder::new(n).node_size(16);
    let mut b2 = Index2DBuilder::new(n).node_size(16);
    for _ in 0..n {
        let (x, y) = (
            rng.random_range(0.0..1000.0f64),
            rng.random_range(0.0..1000.0),
        );
        let b = Box2D::new(
            x,
            y,
            x + rng.random_range(0.1..8.0),
            y + rng.random_range(0.1..8.0),
        );
        b1.add(b);
        b2.add(b);
    }
    (b1.finish().unwrap(), b2.finish_f32().unwrap())
}

#[test]
fn scalar_f32_neighbors_exact_matches_f64_and_simd() {
    use packed_spatial_index::{Index2DF32, Point2D};
    let mut rng = StdRng::seed_from_u64(0x77AA);
    let n = 5_000;
    let mut boxes = Vec::with_capacity(n);
    let mut b1 = Index2DBuilder::new(n).node_size(16);
    let mut b2 = Index2DBuilder::new(n).node_size(16);
    for _ in 0..n {
        let x = rng.random_range(0.0..1000.0f64);
        let y = rng.random_range(0.0..1000.0);
        let bx = Box2D::new(
            x,
            y,
            x + rng.random_range(0.1..8.0),
            y + rng.random_range(0.1..8.0),
        );
        boxes.push(bx);
        b1.add(bx);
        b2.add(bx);
    }
    let f64_index = b1.finish().unwrap();
    let scalar: Index2DF32 = b2.finish_f32().unwrap();
    #[cfg(feature = "simd")]
    let simd = {
        let mut bs = Index2DBuilder::new(n).node_size(16);
        for &bx in &boxes {
            bs.add(bx);
        }
        bs.finish_simd_f32().unwrap()
    };

    let mut q = StdRng::seed_from_u64(0x9090);
    for _ in 0..150 {
        let p = Point2D::new(q.random_range(0.0..1000.0), q.random_range(0.0..1000.0));
        // Exact f32-refined kNN equals the f64 index's kNN (set-equal; ties are
        // vanishingly rare with random float gap distances).
        let exact: HashSet<usize> = scalar
            .neighbors_exact(p, 5, |i| boxes[i])
            .into_iter()
            .collect();
        let f64n: HashSet<usize> = f64_index.neighbors(p, 5).into_iter().collect();
        assert_eq!(
            exact, f64n,
            "scalar f32 exact kNN must match f64 kNN at {p:?}"
        );
        // Conservative kNN returns k results.
        assert_eq!(scalar.neighbors(p, 5).len(), 5);
        // The scalar and SIMD f32 indexes are field-identical, so kNN is identical.
        #[cfg(feature = "simd")]
        assert_eq!(scalar.neighbors(p, 5), simd.neighbors(p, 5));
    }
}

#[test]
fn search_is_a_conservative_superset() {
    let (f64_index, f32_index) = build_both(0x2F32, 20_000);
    assert_eq!(f32_index.num_items(), 20_000);
    let mut rng = StdRng::seed_from_u64(0xBEEF);
    let (mut tot64, mut tot32) = (0usize, 0usize);
    for _ in 0..200 {
        let x = rng.random_range(0.0..1000.0);
        let y = rng.random_range(0.0..1000.0);
        let q = Box2D::new(x, y, x + 40.0, y + 40.0);
        let exact: HashSet<usize> = f64_index.search(q).into_iter().collect();
        let got: HashSet<usize> = f32_index.search(q).into_iter().collect();
        assert!(exact.is_subset(&got), "compact index missed a hit");
        tot64 += exact.len();
        tot32 += got.len();
    }
    assert!(
        tot32 <= tot64 + tot64 / 100 + 200,
        "fp blowup: {tot32} vs {tot64}"
    );
}

#[cfg(feature = "simd")]
#[test]
fn simd_f32_search_matches_scalar_f32_exactly() {
    // Both round the query inward, so the SIMD and scalar f32 indexes return the
    // identical tight superset (not just each a superset of f64).
    let mut rng = StdRng::seed_from_u64(0x51D2);
    let bs: Vec<Box2D> = (0..20_000)
        .map(|_| {
            let (x, y) = (
                rng.random_range(0.0..1000.0f64),
                rng.random_range(0.0..1000.0),
            );
            Box2D::new(
                x,
                y,
                x + rng.random_range(0.1..8.0),
                y + rng.random_range(0.1..8.0),
            )
        })
        .collect();
    let mut b_scalar = Index2DBuilder::new(bs.len()).node_size(16);
    let mut b_simd = Index2DBuilder::new(bs.len()).node_size(16);
    for &b in &bs {
        b_scalar.add(b);
        b_simd.add(b);
    }
    let scalar = b_scalar.finish_f32().unwrap();
    let simd = b_simd.finish_simd_f32().unwrap();
    let mut rng = StdRng::seed_from_u64(0x6061);
    for _ in 0..200 {
        let x = rng.random_range(0.0..1000.0);
        let y = rng.random_range(0.0..1000.0);
        let q = Box2D::new(x, y, x + 40.0, y + 40.0);
        let mut a = scalar.search(q);
        let mut b = simd.search(q);
        a.sort_unstable();
        b.sort_unstable();
        assert_eq!(a, b);
    }
}

#[test]
fn search_exact_matches_f64_exactly() {
    // The inward-rounded f32 query compares f32-vs-f32 yet, with box_at returning
    // the true f64 boxes, exact search has no false positives -> identical to the
    // f64 index's search.
    let mut rng = StdRng::seed_from_u64(0x2E7A);
    let bs: Vec<Box2D> = (0..20_000)
        .map(|_| {
            let (x, y) = (
                rng.random_range(0.0..1000.0f64),
                rng.random_range(0.0..1000.0),
            );
            Box2D::new(
                x,
                y,
                x + rng.random_range(0.1..8.0),
                y + rng.random_range(0.1..8.0),
            )
        })
        .collect();
    let mut b1 = Index2DBuilder::new(bs.len()).node_size(16);
    let mut b2 = Index2DBuilder::new(bs.len()).node_size(16);
    for &b in &bs {
        b1.add(b);
        b2.add(b);
    }
    let f64_index = b1.finish().unwrap();
    let compact = b2.finish_f32().unwrap();

    let mut rng = StdRng::seed_from_u64(0x9B9B);
    for _ in 0..200 {
        let x = rng.random_range(0.0..1000.0);
        let y = rng.random_range(0.0..1000.0);
        let q = Box2D::new(x, y, x + 40.0, y + 40.0);
        let mut exact = compact.search_exact(q, |id| bs[id]);
        let mut f64_hits = f64_index.search(q);
        exact.sort_unstable();
        f64_hits.sort_unstable();
        assert_eq!(exact, f64_hits);
        let conservative: HashSet<usize> = compact.search(q).into_iter().collect();
        assert!(exact.iter().all(|id| conservative.contains(id)));
        assert_eq!(compact.any_exact(q, |id| bs[id]), !exact.is_empty());
        if let Some(f) = compact.first_exact(q, |id| bs[id]) {
            assert!(exact.contains(&f));
        }
    }
}

#[test]
fn from_triangles_builds_a_queryable_index() {
    let tris: Vec<Triangle2D> = (0..300)
        .map(|i| {
            let v = i as f64;
            Triangle2D::new([v, v], [v + 1.0, v], [v, v + 2.0])
        })
        .collect();
    let index = packed_spatial_index::Index2DF32::from_triangles(&tris).unwrap();
    assert_eq!(index.num_items(), 300);
    assert!(!index.search(Box2D::new(9.0, 9.0, 14.0, 14.0)).is_empty());
}
