//! rAF and scroll driver bindings.

use crate::advanced::{AngleTween, AnimationGroup, Mat4Tween, QuaternionTween};
use crate::error::non_negative;
use crate::keyframe::{KeyframeTrack, KeyframeTrack2D, KeyframeTrack3D, KeyframeTrack4D};
use crate::path::MotionPath;
use crate::physics::{Inertia, Inertia2D};
use crate::spring::{Spring, Spring2D, Spring3D, Spring4D};
use crate::timeline::Timeline;
use crate::tween::{Tween, Tween2D, Tween3D, Tween4D};
use animato_core::Update;
use animato_driver::ScrollDriver as CoreScrollDriver;
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::*;

struct DriverSlot {
    id: u32,
    animation: Box<dyn Update + Send>,
    active: bool,
    label: Option<String>,
    kind: &'static str,
    progress: Box<dyn Fn() -> f32 + Send>,
    state: Box<dyn Fn() -> String + Send>,
}

impl core::fmt::Debug for DriverSlot {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("DriverSlot")
            .field("id", &self.id)
            .field("label", &self.label)
            .field("kind", &self.kind)
            .finish()
    }
}

/// Snapshot metadata for JavaScript-owned rAF animations.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct RafSnapshot {
    pub(crate) id: u32,
    pub(crate) label: Option<String>,
    pub(crate) kind: &'static str,
    pub(crate) progress: f32,
    pub(crate) state: String,
}

/// requestAnimationFrame timestamp driver for JavaScript-owned animations.
#[wasm_bindgen(js_name = RafDriver)]
#[derive(Debug)]
pub struct RafDriver {
    slots: Vec<DriverSlot>,
    next_id: u32,
    last_timestamp_ms: Option<f64>,
    paused: bool,
    time_scale: f32,
    max_dt: f32,
    completed_count: usize,
}

#[wasm_bindgen(js_class = RafDriver)]
impl RafDriver {
    /// Create an empty rAF driver.
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        Self {
            slots: Vec::new(),
            next_id: 1,
            last_timestamp_ms: None,
            paused: false,
            time_scale: 1.0,
            max_dt: 0.25,
            completed_count: 0,
        }
    }

    /// Register a scalar tween and return its id.
    #[wasm_bindgen(js_name = addTween)]
    pub fn add_tween(&mut self, tween: &Tween) -> u32 {
        let progress_tween = tween.clone();
        let state_tween = tween.clone();
        self.add_boxed_with_meta(
            Box::new(tween.shared()),
            "tween",
            None,
            Box::new(move || progress_tween.progress()),
            Box::new(move || state_tween.state()),
        )
    }

    /// Register a 2D tween.
    #[wasm_bindgen(js_name = addTween2D)]
    pub fn add_tween_2d(&mut self, tween: &Tween2D) -> u32 {
        self.add_boxed(Box::new(tween.shared()))
    }

    /// Register a 3D tween.
    #[wasm_bindgen(js_name = addTween3D)]
    pub fn add_tween_3d(&mut self, tween: &Tween3D) -> u32 {
        self.add_boxed(Box::new(tween.shared()))
    }

    /// Register a 4D tween.
    #[wasm_bindgen(js_name = addTween4D)]
    pub fn add_tween_4d(&mut self, tween: &Tween4D) -> u32 {
        self.add_boxed(Box::new(tween.shared()))
    }

    /// Register an angle tween.
    #[wasm_bindgen(js_name = addAngleTween)]
    pub fn add_angle_tween(&mut self, tween: &AngleTween) -> u32 {
        self.add_boxed(Box::new(tween.shared()))
    }

    /// Register a quaternion tween.
    #[wasm_bindgen(js_name = addQuaternionTween)]
    pub fn add_quaternion_tween(&mut self, tween: &QuaternionTween) -> u32 {
        self.add_boxed(Box::new(tween.shared()))
    }

    /// Register a matrix tween.
    #[wasm_bindgen(js_name = addMat4Tween)]
    pub fn add_mat4_tween(&mut self, tween: &Mat4Tween) -> u32 {
        self.add_boxed(Box::new(tween.shared()))
    }

    /// Register a scalar spring.
    #[wasm_bindgen(js_name = addSpring)]
    pub fn add_spring(&mut self, spring: &Spring) -> u32 {
        let progress_spring = spring.clone();
        let state_spring = spring.clone();
        self.add_boxed_with_meta(
            Box::new(spring.shared()),
            "spring",
            None,
            Box::new(move || {
                if progress_spring.is_settled() {
                    1.0
                } else {
                    0.0
                }
            }),
            Box::new(move || {
                if state_spring.is_settled() {
                    "complete".to_owned()
                } else {
                    "playing".to_owned()
                }
            }),
        )
    }

    /// Register a 2D spring.
    #[wasm_bindgen(js_name = addSpring2D)]
    pub fn add_spring_2d(&mut self, spring: &Spring2D) -> u32 {
        self.add_boxed(Box::new(spring.shared()))
    }

    /// Register a 3D spring.
    #[wasm_bindgen(js_name = addSpring3D)]
    pub fn add_spring_3d(&mut self, spring: &Spring3D) -> u32 {
        self.add_boxed(Box::new(spring.shared()))
    }

    /// Register a 4D spring.
    #[wasm_bindgen(js_name = addSpring4D)]
    pub fn add_spring_4d(&mut self, spring: &Spring4D) -> u32 {
        self.add_boxed(Box::new(spring.shared()))
    }

    /// Register a scalar keyframe track.
    #[wasm_bindgen(js_name = addKeyframes)]
    pub fn add_keyframes(&mut self, track: &KeyframeTrack) -> u32 {
        let progress_track = track.clone();
        let state_track = track.clone();
        self.add_boxed_with_meta(
            Box::new(track.shared()),
            "keyframe",
            None,
            Box::new(move || progress_track.progress()),
            Box::new(move || {
                if state_track.is_complete() {
                    "complete".to_owned()
                } else {
                    "playing".to_owned()
                }
            }),
        )
    }

    /// Register a 2D keyframe track.
    #[wasm_bindgen(js_name = addKeyframes2D)]
    pub fn add_keyframes_2d(&mut self, track: &KeyframeTrack2D) -> u32 {
        self.add_boxed(Box::new(track.shared()))
    }

    /// Register a 3D keyframe track.
    #[wasm_bindgen(js_name = addKeyframes3D)]
    pub fn add_keyframes_3d(&mut self, track: &KeyframeTrack3D) -> u32 {
        self.add_boxed(Box::new(track.shared()))
    }

    /// Register a 4D keyframe track.
    #[wasm_bindgen(js_name = addKeyframes4D)]
    pub fn add_keyframes_4d(&mut self, track: &KeyframeTrack4D) -> u32 {
        self.add_boxed(Box::new(track.shared()))
    }

    /// Register a timeline.
    #[wasm_bindgen(js_name = addTimeline)]
    pub fn add_timeline(&mut self, timeline: &Timeline) -> u32 {
        let progress_timeline = timeline.clone();
        let state_timeline = timeline.clone();
        self.add_boxed_with_meta(
            Box::new(timeline.shared()),
            "timeline",
            None,
            Box::new(move || progress_timeline.progress()),
            Box::new(move || state_timeline.state()),
        )
    }

    /// Register an animation group.
    #[wasm_bindgen(js_name = addAnimationGroup)]
    pub fn add_animation_group(&mut self, group: &AnimationGroup) -> u32 {
        let progress_group = group.clone();
        let state_group = group.clone();
        self.add_boxed_with_meta(
            Box::new(group.shared()),
            "group",
            None,
            Box::new(move || progress_group.progress()),
            Box::new(move || {
                if state_group.is_complete() {
                    "complete".to_owned()
                } else {
                    "playing".to_owned()
                }
            }),
        )
    }

    /// Register a motion path.
    #[wasm_bindgen(js_name = addMotionPath)]
    pub fn add_motion_path(&mut self, motion: &MotionPath) -> u32 {
        self.add_boxed(Box::new(motion.shared()))
    }

    /// Register scalar inertia.
    #[wasm_bindgen(js_name = addInertia)]
    pub fn add_inertia(&mut self, inertia: &Inertia) -> u32 {
        self.add_boxed(Box::new(inertia.shared()))
    }

    /// Register 2D inertia.
    #[wasm_bindgen(js_name = addInertia2D)]
    pub fn add_inertia_2d(&mut self, inertia: &Inertia2D) -> u32 {
        self.add_boxed(Box::new(inertia.shared()))
    }

    /// Tick from a browser rAF timestamp in milliseconds.
    ///
    /// Returns the seconds delta applied to animations.
    pub fn tick(&mut self, timestamp_ms: f64) -> f32 {
        if !timestamp_ms.is_finite() {
            return 0.0;
        }
        let raw_dt = match self.last_timestamp_ms.replace(timestamp_ms) {
            Some(last) => ((timestamp_ms - last) / 1000.0).max(0.0) as f32,
            None => 0.0,
        };
        if self.paused {
            return 0.0;
        }
        let dt = raw_dt.min(self.max_dt) * self.time_scale;
        self.tick_dt(dt);
        dt
    }

    /// Tick by an explicit seconds delta.
    #[wasm_bindgen(js_name = tickDt)]
    pub fn tick_dt(&mut self, dt: f32) {
        let dt = non_negative(dt, 0.0);
        for slot in &mut self.slots {
            slot.active = slot.animation.update(dt);
        }
        self.completed_count = self
            .completed_count
            .saturating_add(self.slots.iter().filter(|slot| !slot.active).count());
        self.slots.retain(|slot| slot.active);
    }

    /// Pause ticking.
    pub fn pause(&mut self) {
        self.paused = true;
    }

    /// Resume ticking.
    pub fn resume(&mut self) {
        self.paused = false;
    }

    /// Whether ticking is paused.
    #[wasm_bindgen(js_name = isPaused)]
    pub fn is_paused(&self) -> bool {
        self.paused
    }

    /// Reset stored timestamp.
    #[wasm_bindgen(js_name = resetTimestamp)]
    pub fn reset_timestamp(&mut self) {
        self.last_timestamp_ms = None;
    }

    /// Set time scale.
    #[wasm_bindgen(js_name = setTimeScale)]
    pub fn set_time_scale(&mut self, scale: f32) {
        self.time_scale = non_negative(scale, 1.0);
    }

    /// Set maximum accepted frame delta.
    #[wasm_bindgen(js_name = setMaxDt)]
    pub fn set_max_dt(&mut self, max_dt: f32) {
        self.max_dt = non_negative(max_dt, 0.25);
    }

    /// Cancel an animation by id.
    pub fn cancel(&mut self, id: u32) {
        self.slots.retain(|slot| slot.id != id);
    }

    /// Cancel all animations.
    #[wasm_bindgen(js_name = cancelAll)]
    pub fn cancel_all(&mut self) {
        self.slots.clear();
    }

    /// Number of active animations.
    #[wasm_bindgen(js_name = activeCount)]
    pub fn active_count(&self) -> usize {
        self.slots.len()
    }

    /// Number of animations completed by this driver.
    #[wasm_bindgen(js_name = completedCount)]
    pub fn completed_count(&self) -> usize {
        self.completed_count
    }

    /// Number of inspectable snapshots.
    #[wasm_bindgen(js_name = snapshotCount)]
    pub fn snapshot_count(&self) -> usize {
        self.slots.len()
    }

    /// Snapshot animation id by index, or zero when out of range.
    #[wasm_bindgen(js_name = snapshotId)]
    pub fn snapshot_id(&self, index: usize) -> u32 {
        self.slots.get(index).map_or(0, |slot| slot.id)
    }

    /// Snapshot label by index.
    #[wasm_bindgen(js_name = snapshotLabel)]
    pub fn snapshot_label(&self, index: usize) -> String {
        self.slots
            .get(index)
            .and_then(|slot| slot.label.as_ref())
            .cloned()
            .unwrap_or_default()
    }

    /// Snapshot kind by index.
    #[wasm_bindgen(js_name = snapshotKind)]
    pub fn snapshot_kind(&self, index: usize) -> String {
        self.slots
            .get(index)
            .map_or_else(String::new, |slot| slot.kind.to_owned())
    }

    /// Snapshot progress by index.
    #[wasm_bindgen(js_name = snapshotProgress)]
    pub fn snapshot_progress(&self, index: usize) -> f32 {
        self.slots
            .get(index)
            .map_or(0.0, |slot| (slot.progress)().clamp(0.0, 1.0))
    }

    /// Snapshot state by index.
    #[wasm_bindgen(js_name = snapshotState)]
    pub fn snapshot_state(&self, index: usize) -> String {
        self.slots
            .get(index)
            .map_or_else(String::new, |slot| (slot.state)())
    }

    /// Whether an animation id is active.
    #[wasm_bindgen(js_name = isActive)]
    pub fn is_active(&self, id: u32) -> bool {
        self.slots.iter().any(|slot| slot.id == id)
    }

    fn add_boxed(&mut self, animation: Box<dyn Update + Send>) -> u32 {
        self.add_boxed_with_meta(
            animation,
            "custom",
            None,
            Box::new(|| 0.0),
            Box::new(|| "playing".to_owned()),
        )
    }

    fn add_boxed_with_meta(
        &mut self,
        animation: Box<dyn Update + Send>,
        kind: &'static str,
        label: Option<String>,
        progress: Box<dyn Fn() -> f32 + Send>,
        state: Box<dyn Fn() -> String + Send>,
    ) -> u32 {
        let id = self.next_id;
        self.next_id = self.next_id.saturating_add(1).max(1);
        self.slots.push(DriverSlot {
            id,
            animation,
            active: true,
            label,
            kind,
            progress,
            state,
        });
        id
    }

    pub(crate) fn snapshots(&self) -> Vec<RafSnapshot> {
        self.slots
            .iter()
            .map(|slot| RafSnapshot {
                id: slot.id,
                label: slot.label.clone(),
                kind: slot.kind,
                progress: (slot.progress)().clamp(0.0, 1.0),
                state: (slot.state)(),
            })
            .collect()
    }
}

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

/// Scroll position driver for scroll-linked animations.
#[wasm_bindgen(js_name = ScrollDriver)]
#[derive(Clone, Debug)]
pub struct ScrollDriver {
    inner: Arc<Mutex<CoreScrollDriver>>,
}

#[wasm_bindgen(js_class = ScrollDriver)]
impl ScrollDriver {
    /// Create a scroll driver with a min and max scroll position.
    #[wasm_bindgen(constructor)]
    pub fn new(min: f32, max: f32) -> Self {
        Self {
            inner: Arc::new(Mutex::new(CoreScrollDriver::new(min, max))),
        }
    }

    /// Register a tween.
    #[wasm_bindgen(js_name = addTween)]
    pub fn add_tween(&self, tween: &Tween) {
        self.inner
            .lock()
            .expect("animato-js scroll driver lock poisoned")
            .add(tween.shared());
    }

    /// Set the current scroll position.
    #[wasm_bindgen(js_name = setPosition)]
    pub fn set_position(&self, position: f32) {
        self.inner
            .lock()
            .expect("animato-js scroll driver lock poisoned")
            .set_position(position);
    }

    /// Current normalized scroll progress.
    pub fn progress(&self) -> f32 {
        self.inner
            .lock()
            .expect("animato-js scroll driver lock poisoned")
            .progress()
    }

    /// Current scroll position.
    pub fn position(&self) -> f32 {
        self.inner
            .lock()
            .expect("animato-js scroll driver lock poisoned")
            .position()
    }

    /// Number of registered animations.
    #[wasm_bindgen(js_name = animationCount)]
    pub fn animation_count(&self) -> usize {
        self.inner
            .lock()
            .expect("animato-js scroll driver lock poisoned")
            .animation_count()
    }

    /// Remove completed animations.
    #[wasm_bindgen(js_name = clearCompleted)]
    pub fn clear_completed(&self) {
        self.inner
            .lock()
            .expect("animato-js scroll driver lock poisoned")
            .clear_completed();
    }
}

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

    #[test]
    fn raf_driver_ticks_tween() {
        let tween = Tween::new(0.0, 1.0, 1.0);
        let mut driver = RafDriver::new();
        let id = driver.add_tween(&tween);
        driver.tick_dt(0.5);
        assert!(driver.is_active(id));
        assert_eq!(tween.value(), 0.5);
    }

    #[test]
    fn invalid_index_helper_errors() {
        assert!(require_index(4, 2, "test").is_err());
    }
}
