// Spatial events — data-driven events tied to world positions.
//
// Used for gameplay triggers (footsteps, impacts, AOE effects) that
// interact with the observer's perceptual context and the physics system.

use serde::{Deserialize, Serialize};

/// A spatial event occurring at a world position.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpatialEvent {
    /// Unique event ID.
    pub id: u32,
    /// World-space position.
    pub position: [f32; 3],
    /// Effective radius (meters).
    pub radius: f32,
    /// Blend curve for intensity falloff.
    #[serde(default)]
    pub blend_curve: BlendCurve,
    /// Who is authoritative for this event.
    #[serde(default)]
    pub authority: EventAuthority,
    /// Semantic tag name (e.g., "interact.examine", "signal.impacted").
    #[serde(default)]
    pub tag: Option<String>,
    /// Time-to-live in seconds (0 = persistent).
    #[serde(default)]
    pub ttl: f32,
    /// Current age in seconds.
    #[serde(default)]
    pub age: f32,
}

/// Intensity blend curve from center to edge of radius.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
pub enum BlendCurve {
    /// Constant intensity (1.0 everywhere within radius).
    #[default]
    Constant,
    /// Linear falloff from 1.0 at center to 0.0 at edge.
    Linear,
    /// Smooth (cosine) falloff.
    Smooth,
    /// Inverse square falloff.
    InverseSquare,
}

impl BlendCurve {
    /// Evaluate intensity at a given normalized distance (0.0 = center, 1.0 = edge).
    pub fn evaluate(&self, t: f32) -> f32 {
        let t = t.clamp(0.0, 1.0);
        match self {
            BlendCurve::Constant => 1.0,
            BlendCurve::Linear => 1.0 - t,
            BlendCurve::Smooth => {
                let s = t * std::f32::consts::PI;
                (1.0 + s.cos()) * 0.5
            }
            BlendCurve::InverseSquare => {
                let d = 1.0 + t * 9.0; // 1.0 at center, 10.0 at edge
                1.0 / (d * d)
            }
        }
    }
}

/// Who controls this event's lifecycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum EventAuthority {
    /// Server-authoritative (SpacetimeDB reducer).
    #[default]
    Server,
    /// Client-predicted (rollback if server disagrees).
    ClientPredicted,
    /// Local-only (cosmetic, never replicated).
    Local,
}

/// Collection of active spatial events with spatial query support.
pub struct SpatialEventMap {
    events: Vec<SpatialEvent>,
    next_id: u32,
}

impl Default for SpatialEventMap {
    fn default() -> Self {
        Self::new()
    }
}

impl SpatialEventMap {
    pub fn new() -> Self {
        Self {
            events: Vec::new(),
            next_id: 1,
        }
    }

    /// Add a spatial event. Returns the assigned event ID.
    pub fn add(&mut self, mut event: SpatialEvent) -> u32 {
        let id = self.next_id;
        self.next_id += 1;
        event.id = id;
        self.events.push(event);
        id
    }

    /// Remove an event by ID.
    pub fn remove(&mut self, id: u32) {
        self.events.retain(|e| e.id != id);
    }

    /// Query events within radius of a point.
    /// Returns an iterator to avoid per-frame Vec allocation (Clean Compute).
    pub fn query_radius(&self, position: [f32; 3], radius: f32) -> impl Iterator<Item = &SpatialEvent> {
        let r2 = radius * radius;
        self.events.iter().filter(move |e| {
            let dx = e.position[0] - position[0];
            let dy = e.position[1] - position[1];
            let dz = e.position[2] - position[2];
            dx * dx + dy * dy + dz * dz <= r2
        })
    }

    /// Update all events: age, expire TTL-based events.
    pub fn update(&mut self, dt: f32) {
        for event in &mut self.events {
            event.age += dt;
        }
        self.events.retain(|e| e.ttl <= 0.0 || e.age < e.ttl);
    }

    /// Number of active events.
    pub fn len(&self) -> usize {
        self.events.len()
    }

    pub fn is_empty(&self) -> bool {
        self.events.is_empty()
    }

    /// Iterate all active events.
    pub fn iter(&self) -> impl Iterator<Item = &SpatialEvent> {
        self.events.iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn blend_curve_evaluation() {
        // Constant is always 1.0.
        assert!((BlendCurve::Constant.evaluate(0.0) - 1.0).abs() < 0.001);
        assert!((BlendCurve::Constant.evaluate(1.0) - 1.0).abs() < 0.001);

        // Linear: 1.0 at center, 0.0 at edge.
        assert!((BlendCurve::Linear.evaluate(0.0) - 1.0).abs() < 0.001);
        assert!((BlendCurve::Linear.evaluate(1.0) - 0.0).abs() < 0.001);
        assert!((BlendCurve::Linear.evaluate(0.5) - 0.5).abs() < 0.001);

        // Smooth: 1.0 at center, 0.0 at edge, smooth transition.
        assert!((BlendCurve::Smooth.evaluate(0.0) - 1.0).abs() < 0.001);
        assert!((BlendCurve::Smooth.evaluate(1.0) - 0.0).abs() < 0.001);

        // InverseSquare: 1.0 at center, small at edge.
        assert!((BlendCurve::InverseSquare.evaluate(0.0) - 1.0).abs() < 0.001);
        assert!(BlendCurve::InverseSquare.evaluate(1.0) < 0.02);
    }

    #[test]
    fn radius_query() {
        let mut map = SpatialEventMap::new();
        map.add(SpatialEvent {
            id: 0,
            position: [5.0, 0.0, 0.0],
            radius: 1.0,
            blend_curve: BlendCurve::Linear,
            authority: EventAuthority::Server,
            tag: Some("signal.impacted".into()),
            ttl: 0.0,
            age: 0.0,
        });
        map.add(SpatialEvent {
            id: 0,
            position: [100.0, 0.0, 0.0],
            radius: 1.0,
            blend_curve: BlendCurve::default(),
            authority: EventAuthority::default(),
            tag: None,
            ttl: 0.0,
            age: 0.0,
        });

        let near: Vec<_> = map.query_radius([4.0, 0.0, 0.0], 2.0).collect();
        assert_eq!(near.len(), 1);
        assert_eq!(near[0].tag.as_deref(), Some("signal.impacted"));
    }

    #[test]
    fn tag_uniqueness_and_ttl() {
        let mut map = SpatialEventMap::new();
        let id1 = map.add(SpatialEvent {
            id: 0,
            position: [0.0; 3],
            radius: 1.0,
            blend_curve: BlendCurve::default(),
            authority: EventAuthority::default(),
            tag: Some("a".into()),
            ttl: 1.0,
            age: 0.0,
        });
        let id2 = map.add(SpatialEvent {
            id: 0,
            position: [0.0; 3],
            radius: 1.0,
            blend_curve: BlendCurve::default(),
            authority: EventAuthority::default(),
            tag: Some("b".into()),
            ttl: 0.5,
            age: 0.0,
        });
        assert_ne!(id1, id2);
        assert_eq!(map.len(), 2);

        // After 0.6s, event 2 should expire.
        map.update(0.6);
        assert_eq!(map.len(), 1);
        assert_eq!(map.iter().next().unwrap().tag.as_deref(), Some("a"));
    }
}
