//! Pure performance benchmark: no atomics, no diagnostics, just arithmetic work.
//!
//! Each entity is attracted to 4 fixed-position "star" bodies via Newtonian
//! gravity (G * M * m / r^2), with a softening term to avoid singularities.
//! This gives each entity ~4 sqrt + 4 div of real work per tick, making
//! the benchmark compute-bound rather than memory-bound.
//!
//! Pipeline:
//!   GravityProcessor (read Pos, read Mass, write Vel)  ──>  MoveProcessor (read Vel, write Pos)
//!
//! Run with: `cargo run -p latticeon --example ecs_perf --release`

use std::marker::PhantomData;
use std::time::Instant;

use latticeon::ecs::prelude::*;

// ---------------------------------------------------------------------------
// Components — all f32 pairs to keep entity size small and block count high.
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Component)]
struct Pos(f32, f32);

#[derive(Debug, Clone, Component)]
struct Vel(f32, f32);

#[derive(Debug, Clone, Component)]
struct Mass(f32);

// ---------------------------------------------------------------------------
// Fixed attractor stars for Newtonian gravity.
// ---------------------------------------------------------------------------

const STARS: [(f32, f32, f32); 4] = [
    ( 300.0,  300.0, 1e6),
    (-300.0,  300.0, 1e6),
    ( 300.0, -300.0, 1e6),
    (-300.0, -300.0, 1e6),
];
const G: f32 = 6.674e-5;
const SOFTENING_SQ: f32 = 10.0;
const DT: f32 = 1.0 / 60.0;

// ---------------------------------------------------------------------------
// Processors — pure arithmetic, zero synchronisation.
// ---------------------------------------------------------------------------

#[derive(Default, Processor)]
struct GravityProcessor {
    #[read]  _pos:  PhantomData<Pos>,
    #[read]  _mass: PhantomData<Mass>,
    #[write] _vel:  PhantomData<Vel>,
}

impl GravityProcessorRun for GravityProcessor {
    #[inline]
    fn run(&self, view: &mut GravityProcessorView<'_>, _commands: &Commands) {
        let px = view.pos.0;
        let py = view.pos.1;
        let m = view.mass.0;
        let mut ax = 0.0_f32;
        let mut ay = 0.0_f32;

        for &(sx, sy, sm) in &STARS {
            let dx = sx - px;
            let dy = sy - py;
            let dist_sq = dx * dx + dy * dy + SOFTENING_SQ;
            let inv_dist = 1.0 / dist_sq.sqrt();
            let force = G * sm * m * inv_dist * inv_dist;
            ax += force * dx * inv_dist / m;
            ay += force * dy * inv_dist / m;
        }

        view.vel.0 += ax * DT;
        view.vel.1 += ay * DT;
    }
}

#[derive(Default, Processor)]
#[after(GravityProcessor)]
struct MoveProcessor {
    #[read]  _vel: PhantomData<Vel>,
    #[write] _pos: PhantomData<Pos>,
}

impl MoveProcessorRun for MoveProcessor {
    #[inline]
    fn run(&self, view: &mut MoveProcessorView<'_>, _commands: &Commands) {
        view.pos.0 += view.vel.0 * DT;
        view.pos.1 += view.vel.1 * DT;
    }
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

const ENTITY_COUNT: usize = 10_000_000;
const TICK_COUNT: usize = 100;

fn main() {
    println!("ECS perf bench: {} entities, {} ticks", ENTITY_COUNT, TICK_COUNT);
    println!("rayon threads: {}\n", rayon::current_num_threads());

    // -- Spawn --
    let t0 = Instant::now();
    let mut universe = Universe::new();

    for i in 0..ENTITY_COUNT as u32 {
        let x = (i as f32 * 0.0001).sin() * 500.0;
        let y = (i as f32 * 0.0001).cos() * 500.0;
        let vx = (i % 7) as f32 - 3.0;
        let vy = (i % 5) as f32 - 2.0;
        let mass = 1.0 + (i % 100) as f32 * 0.1;
        universe.spawn((Pos(x, y), Vel(vx, vy), Mass(mass)));
    }
    let spawn_ms = t0.elapsed().as_secs_f64() * 1000.0;
    println!("Spawn:    {:.1} ms ({} entities)", spawn_ms, universe.entity_count());

    // -- Build schedule --
    let mut schedule = Schedule::new();
    schedule.add_processor(GravityProcessor::default());
    schedule.add_processor(MoveProcessor::default());

    // -- Warmup --
    schedule.run(&mut universe);

    // -- Timed run --
    let t1 = Instant::now();
    for _ in 1..TICK_COUNT {
        schedule.run(&mut universe);
    }
    let run_ms = t1.elapsed().as_secs_f64() * 1000.0;
    let ticks = (TICK_COUNT - 1) as f64;

    println!("Run:      {:.1} ms total ({} ticks)", run_ms, TICK_COUNT - 1);
    println!("          {:.3} ms/tick", run_ms / ticks);
    println!(
        "          {:.1} M entity-updates/sec",
        (ENTITY_COUNT as f64 * 2.0 * ticks) / (run_ms / 1000.0) / 1e6
    );

    // -- Sample --
    println!("\nSample entities after {} ticks:", TICK_COUNT);
    let mut count = 0;
    for (entity, pos, vel) in universe.query::<(&Pos, &Vel)>() {
        if count >= 5 { break; }
        println!(
            "  {:?}: pos=({:.2}, {:.2}), vel=({:.4}, {:.4})",
            entity, pos.0, pos.1, vel.0, vel.1,
        );
        count += 1;
    }
}
