use crate::{
    Color, Line, SceneShape, Shape, ThreeDCamera, VideoConfig, find_shape_indices, lerp_shape,
    partial_vector_path,
};

#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum RateFunc {
    #[default]
    Linear,
    Smooth,
    ThereAndBack,
    RushInto,
    RushFrom,
    SlowInto,
    ThereAndBackWithPause,
    Wiggle,
    Lingering,
    ExponentialDecay,
    QuadIn,
    QuadOut,
    QuadInOut,
    CubicIn,
    CubicOut,
    CubicInOut,
    QuartIn,
    QuartOut,
    QuartInOut,
    QuintIn,
    QuintOut,
    QuintInOut,
    SineIn,
    SineOut,
    SineInOut,
    ExpoIn,
    ExpoOut,
    ExpoInOut,
    CircIn,
    CircOut,
    CircInOut,
    BackIn,
    BackOut,
    BackInOut,
    BounceIn,
    BounceOut,
    BounceInOut,
    ElasticIn,
    ElasticOut,
    ElasticInOut,
}

impl RateFunc {
    pub fn apply(&self, alpha: f32) -> f32 {
        let alpha = alpha.clamp(0.0, 1.0);
        match self {
            RateFunc::Linear => alpha,
            RateFunc::Smooth => alpha * alpha * (3.0 - 2.0 * alpha),
            RateFunc::ThereAndBack => {
                if alpha <= 0.5 {
                    2.0 * alpha
                } else {
                    2.0 * (1.0 - alpha)
                }
            }
            RateFunc::RushInto => 2.0 * smoothstep(alpha * 0.5),
            RateFunc::RushFrom => 2.0 * smoothstep(0.5 * (alpha + 1.0)) - 1.0,
            RateFunc::SlowInto => (1.0 - (1.0 - alpha) * (1.0 - alpha)).sqrt(),
            RateFunc::ThereAndBackWithPause => {
                const PAUSE_RATIO: f32 = 1.0 / 3.0;
                let scale = 2.0 / (1.0 - PAUSE_RATIO);
                if alpha < 0.5 - PAUSE_RATIO * 0.5 {
                    smoothstep(scale * alpha)
                } else if alpha < 0.5 + PAUSE_RATIO * 0.5 {
                    1.0
                } else {
                    smoothstep(scale - scale * alpha)
                }
            }
            RateFunc::Wiggle => {
                self::RateFunc::ThereAndBack.apply(alpha)
                    * (2.0 * std::f32::consts::PI * alpha).sin()
            }
            RateFunc::Lingering => {
                if alpha >= 0.8 {
                    1.0
                } else {
                    alpha / 0.8
                }
            }
            RateFunc::ExponentialDecay => {
                if alpha == 1.0 {
                    1.0
                } else {
                    1.0 - (-alpha / 0.1).exp()
                }
            }
            RateFunc::QuadIn => alpha * alpha,
            RateFunc::QuadOut => 1.0 - (1.0 - alpha) * (1.0 - alpha),
            RateFunc::QuadInOut => {
                if alpha < 0.5 {
                    2.0 * alpha * alpha
                } else {
                    1.0 - (-2.0 * alpha + 2.0).powi(2) * 0.5
                }
            }
            RateFunc::CubicIn => alpha.powi(3),
            RateFunc::CubicOut => 1.0 - (1.0 - alpha).powi(3),
            RateFunc::CubicInOut => {
                if alpha < 0.5 {
                    4.0 * alpha.powi(3)
                } else {
                    1.0 - (-2.0 * alpha + 2.0).powi(3) * 0.5
                }
            }
            RateFunc::QuartIn => alpha.powi(4),
            RateFunc::QuartOut => 1.0 - (1.0 - alpha).powi(4),
            RateFunc::QuartInOut => {
                if alpha < 0.5 {
                    8.0 * alpha.powi(4)
                } else {
                    1.0 - (-2.0 * alpha + 2.0).powi(4) * 0.5
                }
            }
            RateFunc::QuintIn => alpha.powi(5),
            RateFunc::QuintOut => 1.0 - (1.0 - alpha).powi(5),
            RateFunc::QuintInOut => {
                if alpha < 0.5 {
                    16.0 * alpha.powi(5)
                } else {
                    1.0 - (-2.0 * alpha + 2.0).powi(5) * 0.5
                }
            }
            RateFunc::SineIn => 1.0 - (alpha * std::f32::consts::FRAC_PI_2).cos(),
            RateFunc::SineOut => (alpha * std::f32::consts::FRAC_PI_2).sin(),
            RateFunc::SineInOut => -((std::f32::consts::PI * alpha).cos() - 1.0) * 0.5,
            RateFunc::ExpoIn => {
                if alpha == 0.0 {
                    0.0
                } else {
                    2.0_f32.powf(10.0 * alpha - 10.0)
                }
            }
            RateFunc::ExpoOut => {
                if alpha == 1.0 {
                    1.0
                } else {
                    1.0 - 2.0_f32.powf(-10.0 * alpha)
                }
            }
            RateFunc::ExpoInOut => {
                if alpha == 0.0 {
                    0.0
                } else if alpha == 1.0 {
                    1.0
                } else if alpha < 0.5 {
                    2.0_f32.powf(20.0 * alpha - 10.0) * 0.5
                } else {
                    (2.0 - 2.0_f32.powf(-20.0 * alpha + 10.0)) * 0.5
                }
            }
            RateFunc::CircIn => 1.0 - (1.0 - alpha * alpha).sqrt(),
            RateFunc::CircOut => (1.0 - (alpha - 1.0).powi(2)).sqrt(),
            RateFunc::CircInOut => {
                if alpha < 0.5 {
                    (1.0 - (1.0 - (2.0 * alpha).powi(2)).sqrt()) * 0.5
                } else {
                    ((1.0 - (-2.0 * alpha + 2.0).powi(2)).sqrt() + 1.0) * 0.5
                }
            }
            RateFunc::BackIn => {
                const C1: f32 = 1.70158;
                const C3: f32 = C1 + 1.0;
                C3 * alpha.powi(3) - C1 * alpha.powi(2)
            }
            RateFunc::BackOut => {
                const C1: f32 = 1.70158;
                const C3: f32 = C1 + 1.0;
                1.0 + C3 * (alpha - 1.0).powi(3) + C1 * (alpha - 1.0).powi(2)
            }
            RateFunc::BackInOut => {
                const C1: f32 = 1.70158;
                const C2: f32 = C1 * 1.525;
                if alpha < 0.5 {
                    ((2.0 * alpha).powi(2) * ((C2 + 1.0) * 2.0 * alpha - C2)) * 0.5
                } else {
                    (((2.0 * alpha - 2.0).powi(2) * ((C2 + 1.0) * (2.0 * alpha - 2.0) + C2)) + 2.0)
                        * 0.5
                }
            }
            RateFunc::BounceIn => 1.0 - bounce_out(1.0 - alpha),
            RateFunc::BounceOut => bounce_out(alpha),
            RateFunc::BounceInOut => {
                if alpha < 0.5 {
                    (1.0 - bounce_out(1.0 - 2.0 * alpha)) * 0.5
                } else {
                    (1.0 + bounce_out(2.0 * alpha - 1.0)) * 0.5
                }
            }
            RateFunc::ElasticIn => {
                if alpha == 0.0 {
                    0.0
                } else if alpha == 1.0 {
                    1.0
                } else {
                    let c4 = (2.0 * std::f32::consts::PI) / 3.0;
                    -2.0_f32.powf(10.0 * alpha - 10.0) * ((alpha * 10.0 - 10.75) * c4).sin()
                }
            }
            RateFunc::ElasticOut => {
                if alpha == 0.0 {
                    0.0
                } else if alpha == 1.0 {
                    1.0
                } else {
                    let c4 = (2.0 * std::f32::consts::PI) / 3.0;
                    2.0_f32.powf(-10.0 * alpha) * ((alpha * 10.0 - 0.75) * c4).sin() + 1.0
                }
            }
            RateFunc::ElasticInOut => {
                if alpha == 0.0 {
                    0.0
                } else if alpha == 1.0 {
                    1.0
                } else {
                    let c5 = (2.0 * std::f32::consts::PI) / 4.5;
                    if alpha < 0.5 {
                        -(2.0_f32.powf(20.0 * alpha - 10.0) * ((20.0 * alpha - 11.125) * c5).sin())
                            * 0.5
                    } else {
                        (2.0_f32.powf(-20.0 * alpha + 10.0) * ((20.0 * alpha - 11.125) * c5).sin())
                            * 0.5
                            + 1.0
                    }
                }
            }
        }
    }
}

fn smoothstep(alpha: f32) -> f32 {
    let alpha = alpha.clamp(0.0, 1.0);
    alpha * alpha * (3.0 - 2.0 * alpha)
}

fn bounce_out(alpha: f32) -> f32 {
    const N1: f32 = 7.5625;
    const D1: f32 = 2.75;
    if alpha < 1.0 / D1 {
        N1 * alpha * alpha
    } else if alpha < 2.0 / D1 {
        let t = alpha - 1.5 / D1;
        N1 * t * t + 0.75
    } else if alpha < 2.5 / D1 {
        let t = alpha - 2.25 / D1;
        N1 * t * t + 0.9375
    } else {
        let t = alpha - 2.625 / D1;
        N1 * t * t + 0.984375
    }
}

#[derive(Clone, Debug)]
pub enum Animation {
    FadeIn {
        target: SceneShape,
    },
    FadeOut {
        target: SceneShape,
    },
    Transform {
        from: SceneShape,
        to: SceneShape,
    },
    Hold {
        shapes: Vec<SceneShape>,
    },
    /// Progressive draw of a sequence of targets.
    /// Targets reveal one after another with configurable overlap; piece `k`
    /// receives its local alpha from a sliding window in `[0, 1]`. With the
    /// default `lag_ratio = 0.7` and `N = 2` (shaft + tip), the shaft draws
    /// across `[0, 0.59]` of the segment and the tip across `[0.41, 1.0]` so
    /// the two overlap visibly rather than popping in sequentially.
    ShowCreation {
        targets: Vec<SceneShape>,
    },
    Group {
        items: Vec<GroupAnimation>,
    },
}

#[derive(Clone, Debug)]
pub struct GroupAnimation {
    pub animation: Animation,
    pub start: f32,
    pub end: f32,
}

impl Animation {
    pub fn apply(&self, base: &[SceneShape], alpha: f32) -> Vec<SceneShape> {
        match self {
            Animation::FadeIn { target } => {
                let mut out = base.to_vec();
                let mut s = target.clone();
                s.set_alpha(alpha);
                out.push(s);
                out
            }
            Animation::FadeOut { target } => {
                let mut out = base.to_vec();
                let target_indices = find_shape_indices(base, target.id);
                if target_indices.is_empty() {
                    let mut s = target.clone();
                    s.set_alpha(1.0 - alpha);
                    out.push(s);
                } else {
                    for index in target_indices {
                        out[index].set_alpha(1.0 - alpha);
                    }
                }
                out
            }
            Animation::Transform { from, to } => {
                let mut out = base.to_vec();
                let from_indices = find_shape_indices(base, from.id);
                let layer = from_indices
                    .first()
                    .map(|index| out[*index].layer)
                    .unwrap_or(from.layer);
                let interpolated = SceneShape::new_with_layer(
                    from.id,
                    layer,
                    lerp_shape(&from.shape, &to.shape, alpha),
                );
                if let Some(first_index) = from_indices.first().copied() {
                    out.retain(|shape| shape.id != from.id);
                    out.insert(first_index.min(out.len()), interpolated);
                } else {
                    out.push(interpolated);
                }
                out
            }
            Animation::Hold { shapes } => shapes.clone(),
            Animation::ShowCreation { targets } => {
                let mut out = base.to_vec();
                for (i, child_alpha) in distribute_show_creation(targets.len(), alpha)
                    .into_iter()
                    .enumerate()
                {
                    out.push(partial_shape(&targets[i], child_alpha));
                }
                out
            }
            Animation::Group { items } => {
                let mut out = base.to_vec();
                for item in items {
                    let child_alpha = item.local_alpha(alpha);
                    if child_alpha <= 0.0 && item.animation.is_additive_reveal() {
                        continue;
                    }
                    if child_alpha >= 1.0 {
                        out = item.animation.final_state(&out, RateFunc::Linear);
                    } else {
                        out = item.animation.apply(&out, child_alpha);
                    }
                }
                out
            }
        }
    }

    pub fn final_state(&self, base: &[SceneShape], rate: RateFunc) -> Vec<SceneShape> {
        const EPSILON: f32 = 1e-6;
        let terminal_alpha = rate.apply(1.0);
        match self {
            Animation::Group { .. } => self.apply(base, terminal_alpha),
            Animation::FadeIn { .. } if terminal_alpha <= EPSILON => base.to_vec(),
            Animation::FadeOut { .. } | Animation::Transform { .. }
                if terminal_alpha <= EPSILON =>
            {
                base.to_vec()
            }
            Animation::ShowCreation { .. } if terminal_alpha <= EPSILON => base.to_vec(),
            Animation::FadeOut { target } if terminal_alpha >= 1.0 - EPSILON => {
                let mut out = base.to_vec();
                out.retain(|shape| shape.id != target.id);
                out
            }
            Animation::Transform { from, to } if terminal_alpha >= 1.0 - EPSILON => {
                let mut out = base.to_vec();
                let from_indices = find_shape_indices(base, from.id);
                let layer = from_indices
                    .first()
                    .map(|index| out[*index].layer)
                    .unwrap_or(from.layer);
                let final_shape = SceneShape::new_with_layer(from.id, layer, to.shape.clone());
                if let Some(first_index) = from_indices.first().copied() {
                    out.retain(|shape| shape.id != from.id);
                    out.insert(first_index.min(out.len()), final_shape);
                } else {
                    out.push(final_shape);
                }
                out
            }
            Animation::ShowCreation { targets } if terminal_alpha >= 1.0 - EPSILON => {
                let mut out = base.to_vec();
                for t in targets {
                    out.push(t.clone());
                }
                out
            }
            _ => self.apply(base, terminal_alpha),
        }
    }

    fn is_additive_reveal(&self) -> bool {
        matches!(
            self,
            Animation::FadeIn { .. } | Animation::ShowCreation { .. } | Animation::Group { .. }
        )
    }
}

impl GroupAnimation {
    fn local_alpha(&self, alpha: f32) -> f32 {
        if self.end <= self.start {
            return (alpha >= self.end) as u8 as f32;
        }
        ((alpha - self.start) / (self.end - self.start)).clamp(0.0, 1.0)
    }
}

/// Split a global `alpha` across `n` ShowCreation targets using a lag-ratio
/// grouped-animation schedule.
///
/// Piece `k` occupies the sub-window `[k * lag * d, k * lag * d + d]` of
/// `[0, 1]`, where `d = 1 / (1 + lag * (n - 1))`. With `lag = 0.7`:
/// - `n = 1`: piece 0 covers `[0, 1]` (no scheduling needed).
/// - `n = 2`: piece 0 → `[0, 0.59]`, piece 1 → `[0.41, 1.0]`. shaft+tip overlap.
/// - `n = 3`: pieces span `[0, 0.42]`, `[0.29, 0.71]`, `[0.58, 1.0]`.
///
/// Lag = 1.0 is fully sequential (no overlap); lag = 0.0 is fully simultaneous
/// (all pieces progress together). We pick 0.7 as a default — enough overlap
/// that the eye reads a single fluid motion, not enough that the trailing
/// piece pops out of the leading one.
///
/// Each piece's returned alpha is local to its own window, clamped to `[0, 1]`.
fn distribute_show_creation(n: usize, alpha: f32) -> Vec<f32> {
    if n == 0 {
        return Vec::new();
    }
    let alpha = alpha.clamp(0.0, 1.0);
    if n == 1 {
        return vec![alpha];
    }
    const LAG_RATIO: f32 = 0.7;
    let duration = 1.0 / (1.0 + LAG_RATIO * (n as f32 - 1.0));
    let mut out = Vec::with_capacity(n);
    for k in 0..n {
        let start_k = (k as f32) * LAG_RATIO * duration;
        let local = ((alpha - start_k) / duration).clamp(0.0, 1.0);
        out.push(local);
    }
    out
}

/// Reveal a fraction of `shape` from its start (path/line/perimeter beginning).
///
/// Path-like shapes are trimmed by arc length; `Line` is shortened along its
/// vector; `Polygon` traces its perimeter. Other variants fall back to scaling
/// the color alpha channel.
pub fn partial_shape(shape: &SceneShape, alpha: f32) -> SceneShape {
    let new_kind = match &shape.shape {
        Shape::StrokedPath { path, thickness } => Shape::StrokedPath {
            path: partial_vector_path(path, alpha),
            thickness: *thickness,
        },
        Shape::Path(path) => Shape::Path(partial_vector_path(path, alpha)),
        Shape::Line(line) => {
            let nx = line.x1 + (line.x2 - line.x1) * alpha;
            let ny = line.y1 + (line.y2 - line.y1) * alpha;
            Shape::Line(Line {
                x1: line.x1,
                y1: line.y1,
                x2: nx,
                y2: ny,
                thickness: line.thickness,
                color: line.color,
            })
        }
        // Closed shapes fade their alpha rather than perimeter-trace; perimeter
        // trace produces a wandering outline that reads as a separate animation
        // from a co-revealing shaft, breaking the "single continuous draw" feel.
        other => fade_shape_alpha(other, alpha),
    };
    SceneShape {
        id: shape.id,
        layer: shape.layer,
        shape: new_kind,
    }
}

fn fade_shape_alpha(shape: &Shape, alpha: f32) -> Shape {
    let mut clone = shape.clone();
    clone.set_alpha(alpha);
    clone
}

#[derive(Clone, Debug)]
pub struct Segment {
    pub start_frame: u32,
    pub end_frame: u32,
    pub base: Vec<SceneShape>,
    pub animation: Animation,
    pub rate: RateFunc,
}

#[derive(Clone, Debug)]
pub struct Timeline {
    pub segments: Vec<Segment>,
    pub fps: u32,
    pub background: Color,
    pub width: u32,
    pub height: u32,
}

impl Timeline {
    pub fn total_frames(&self) -> u32 {
        self.segments.last().map_or(0, |s| s.end_frame)
    }

    pub fn config(&self) -> VideoConfig {
        VideoConfig {
            width: self.width,
            height: self.height,
            fps: self.fps,
            total_frames: self.total_frames(),
        }
    }

    pub fn shapes_at(&self, frame_idx: u32) -> Vec<SceneShape> {
        for seg in &self.segments {
            if frame_idx >= seg.start_frame && frame_idx < seg.end_frame {
                // On the last in-range frame, render the terminal state so
                // animations actually reach `alpha = 1.0`. With the linear
                // `(frame - start) / span` mapping the max raw_alpha is
                // `(span - 1) / span`, which leaves FadeIn/ShowCreation visibly
                // un-finished when a segment has no successor to absorb it.
                if frame_idx + 1 == seg.end_frame {
                    return seg.animation.final_state(&seg.base, seg.rate);
                }
                let span = (seg.end_frame - seg.start_frame) as f32;
                let raw_alpha = if span > 0.0 {
                    (frame_idx - seg.start_frame) as f32 / span
                } else {
                    1.0
                };
                let alpha = seg.rate.apply(raw_alpha);
                return seg.animation.apply(&seg.base, alpha);
            }
        }
        Vec::new()
    }
}

#[derive(Clone, Debug)]
pub struct ValueSegment {
    pub start_frame: u32,
    pub end_frame: u32,
    pub from: f32,
    pub to: f32,
    pub rate: RateFunc,
}

impl ValueSegment {
    fn value_at(&self, frame_idx: u32) -> f32 {
        if frame_idx.saturating_add(1) == self.end_frame {
            return self.final_value();
        }
        let span = (self.end_frame - self.start_frame) as f32;
        let raw_alpha = if span > 0.0 {
            (frame_idx - self.start_frame) as f32 / span
        } else {
            1.0
        };
        let alpha = self.rate.apply(raw_alpha);
        self.from + (self.to - self.from) * alpha
    }

    fn final_value(&self) -> f32 {
        let alpha = self.rate.apply(1.0);
        self.from + (self.to - self.from) * alpha
    }
}

#[derive(Clone, Debug)]
pub struct ValueTimeline {
    pub initial: f32,
    pub segments: Vec<ValueSegment>,
}

impl ValueTimeline {
    pub fn new(initial: f32) -> Self {
        Self {
            initial,
            segments: Vec::new(),
        }
    }

    pub fn final_value(&self) -> f32 {
        self.segments
            .last()
            .map_or(self.initial, ValueSegment::final_value)
    }

    pub fn value_at(&self, frame_idx: u32) -> f32 {
        let mut current = self.initial;
        for segment in &self.segments {
            if frame_idx >= segment.start_frame && frame_idx < segment.end_frame {
                return segment.value_at(frame_idx);
            }
            if frame_idx >= segment.end_frame {
                current = segment.final_value();
            }
        }
        current
    }
}

#[derive(Clone, Debug)]
pub struct CameraSegment {
    pub start_frame: u32,
    pub end_frame: u32,
    pub from: ThreeDCamera,
    pub to: ThreeDCamera,
    pub rate: RateFunc,
}

impl CameraSegment {
    fn camera_at(&self, frame_idx: u32) -> ThreeDCamera {
        if frame_idx.saturating_add(1) == self.end_frame {
            return self.final_camera();
        }
        let span = (self.end_frame - self.start_frame) as f32;
        let raw_alpha = if span > 0.0 {
            (frame_idx - self.start_frame) as f32 / span
        } else {
            1.0
        };
        let alpha = self.rate.apply(raw_alpha);
        lerp_camera(self.from, self.to, alpha)
    }

    fn final_camera(&self) -> ThreeDCamera {
        lerp_camera(self.from, self.to, self.rate.apply(1.0))
    }
}

#[derive(Clone, Debug)]
pub struct CameraTimeline {
    pub initial: ThreeDCamera,
    pub segments: Vec<CameraSegment>,
}

impl CameraTimeline {
    pub fn new(initial: ThreeDCamera) -> Self {
        Self {
            initial,
            segments: Vec::new(),
        }
    }

    pub fn final_camera(&self) -> ThreeDCamera {
        self.segments
            .last()
            .map_or(self.initial, CameraSegment::final_camera)
    }

    pub fn camera_at(&self, frame_idx: u32) -> ThreeDCamera {
        let mut current = self.initial;
        for segment in &self.segments {
            if frame_idx >= segment.start_frame && frame_idx < segment.end_frame {
                return segment.camera_at(frame_idx);
            }
            if frame_idx >= segment.end_frame {
                current = segment.final_camera();
            }
        }
        current
    }
}

fn lerp_camera(from: ThreeDCamera, to: ThreeDCamera, alpha: f32) -> ThreeDCamera {
    ThreeDCamera {
        yaw: from.yaw + (to.yaw - from.yaw) * alpha,
        pitch: from.pitch + (to.pitch - from.pitch) * alpha,
        distance: from.distance + (to.distance - from.distance) * alpha,
        focal_length: from.focal_length + (to.focal_length - from.focal_length) * alpha,
        scale: from.scale + (to.scale - from.scale) * alpha,
    }
}
