use crate::ecs::primitives::EasingFunction;
use nalgebra_glm::{Quat, Vec3};
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct CutsceneShot {
    pub eye: Vec3,
    pub target: Vec3,
    pub field_of_view_degrees: f32,
    pub roll_degrees: f32,
}

impl CutsceneShot {
    pub fn new(eye: Vec3, target: Vec3) -> Self {
        Self {
            eye,
            target,
            field_of_view_degrees: 45.0,
            roll_degrees: 0.0,
        }
    }

    pub fn with_field_of_view(mut self, field_of_view_degrees: f32) -> Self {
        self.field_of_view_degrees = field_of_view_degrees;
        self
    }

    pub fn with_roll(mut self, roll_degrees: f32) -> Self {
        self.roll_degrees = roll_degrees;
        self
    }

    pub fn interpolate(self, other: CutsceneShot, factor: f32) -> CutsceneShot {
        CutsceneShot {
            eye: self.eye + (other.eye - self.eye) * factor,
            target: self.target + (other.target - self.target) * factor,
            field_of_view_degrees: self.field_of_view_degrees
                + (other.field_of_view_degrees - self.field_of_view_degrees) * factor,
            roll_degrees: self.roll_degrees + (other.roll_degrees - self.roll_degrees) * factor,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum CutsceneAction {
    CameraMove {
        from: CutsceneShot,
        to: CutsceneShot,
    },
    CameraFollow {
        actor: String,
        eye_offset: Vec3,
        look_height: f32,
        field_of_view_degrees: f32,
    },
    CameraPath {
        waypoints: Vec<CutsceneShot>,
    },
    CameraShake {
        amplitude: f32,
        frequency: f32,
    },
    Handheld {
        position_amplitude: f32,
        target_amplitude: f32,
        frequency: f32,
    },
    ActorMove {
        actor: String,
        from: Vec3,
        to: Vec3,
    },
    ActorTurn {
        actor: String,
        from_yaw_radians: f32,
        to_yaw_radians: f32,
    },
    ActorVisible {
        actor: String,
        visible: bool,
    },
    ActorAnimation {
        actor: String,
        clip: String,
        looping: bool,
        speed: f32,
        blend: f32,
    },
    CameraLookAt {
        actor: String,
        look_height: f32,
        damping: f32,
    },
    Dialogue {
        speaker: Option<String>,
        line: String,
    },
    Title {
        line: String,
    },
    Fade {
        from: f32,
        to: f32,
        color: Vec3,
    },
    Letterbox {
        from: f32,
        to: f32,
    },
    FocusPull {
        from_distance: f32,
        to_distance: f32,
        range: f32,
    },
    Grade {
        exposure_ev: (f32, f32),
        saturation: (f32, f32),
        contrast: (f32, f32),
    },
    /// Fires a named marker the host app drains and reacts to. Keeps cutscenes
    /// pure data: the timeline names an event, the game decides what it means.
    Trigger {
        id: String,
    },
    /// Plays a one-shot sound registered with the audio engine under `clip`.
    Sound {
        clip: String,
        volume: f32,
    },
    /// Starts looping music registered with the audio engine under `track`.
    Music {
        track: String,
        volume: f32,
    },
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CutsceneEvent {
    pub start: f32,
    pub duration: f32,
    pub easing: EasingFunction,
    pub action: CutsceneAction,
}

impl CutsceneEvent {
    pub fn end(&self) -> f32 {
        self.start + self.duration
    }

    pub fn eased_progress(&self, time: f32) -> f32 {
        if self.duration <= 0.0 {
            return if time >= self.start { 1.0 } else { 0.0 };
        }
        let raw = ((time - self.start) / self.duration).clamp(0.0, 1.0);
        self.easing.evaluate(raw)
    }
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Cutscene {
    pub name: String,
    pub events: Vec<CutsceneEvent>,
}

impl Cutscene {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            events: Vec::new(),
        }
    }

    pub fn duration(&self) -> f32 {
        self.events
            .iter()
            .map(CutsceneEvent::end)
            .fold(0.0, f32::max)
    }

    fn push(
        mut self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        action: CutsceneAction,
    ) -> Self {
        self.events.push(CutsceneEvent {
            start,
            duration,
            easing,
            action,
        });
        self
    }

    pub fn camera(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: CutsceneShot,
        to: CutsceneShot,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::CameraMove { from, to },
        )
    }

    pub fn camera_follow(
        self,
        start: f32,
        duration: f32,
        actor: impl Into<String>,
        eye_offset: Vec3,
        look_height: f32,
        field_of_view_degrees: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::CameraFollow {
                actor: actor.into(),
                eye_offset,
                look_height,
                field_of_view_degrees,
            },
        )
    }

    pub fn camera_look_at(
        self,
        start: f32,
        duration: f32,
        actor: impl Into<String>,
        look_height: f32,
        damping: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::CameraLookAt {
                actor: actor.into(),
                look_height,
                damping,
            },
        )
    }

    pub fn camera_path(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        waypoints: Vec<CutsceneShot>,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::CameraPath { waypoints },
        )
    }

    pub fn handheld(
        self,
        start: f32,
        duration: f32,
        position_amplitude: f32,
        target_amplitude: f32,
        frequency: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::Handheld {
                position_amplitude,
                target_amplitude,
                frequency,
            },
        )
    }

    pub fn camera_shake(self, start: f32, duration: f32, amplitude: f32, frequency: f32) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::CameraShake {
                amplitude,
                frequency,
            },
        )
    }

    pub fn camera_cut(self, start: f32, shot: CutsceneShot) -> Self {
        self.push(
            start,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::CameraMove {
                from: shot,
                to: shot,
            },
        )
    }

    pub fn actor_move(
        self,
        actor: impl Into<String>,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: Vec3,
        to: Vec3,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::ActorMove {
                actor: actor.into(),
                from,
                to,
            },
        )
    }

    pub fn actor_turn(
        self,
        actor: impl Into<String>,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from_yaw_radians: f32,
        to_yaw_radians: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::ActorTurn {
                actor: actor.into(),
                from_yaw_radians,
                to_yaw_radians,
            },
        )
    }

    pub fn actor_animation(
        self,
        actor: impl Into<String>,
        start: f32,
        clip: impl Into<String>,
        looping: bool,
        speed: f32,
    ) -> Self {
        self.actor_animation_blended(actor, start, clip, looping, speed, 0.2)
    }

    pub fn actor_animation_blended(
        self,
        actor: impl Into<String>,
        start: f32,
        clip: impl Into<String>,
        looping: bool,
        speed: f32,
        blend: f32,
    ) -> Self {
        self.push(
            start,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::ActorAnimation {
                actor: actor.into(),
                clip: clip.into(),
                looping,
                speed,
                blend,
            },
        )
    }

    pub fn actor_place(self, actor: impl Into<String>, time: f32, position: Vec3) -> Self {
        self.push(
            time,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::ActorMove {
                actor: actor.into(),
                from: position,
                to: position,
            },
        )
    }

    pub fn actor_face(self, actor: impl Into<String>, time: f32, yaw_radians: f32) -> Self {
        self.push(
            time,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::ActorTurn {
                actor: actor.into(),
                from_yaw_radians: yaw_radians,
                to_yaw_radians: yaw_radians,
            },
        )
    }

    pub fn actor_visible(self, actor: impl Into<String>, time: f32, visible: bool) -> Self {
        self.push(
            time,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::ActorVisible {
                actor: actor.into(),
                visible,
            },
        )
    }

    pub fn dialogue(
        self,
        start: f32,
        duration: f32,
        speaker: Option<&str>,
        line: impl Into<String>,
    ) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::Dialogue {
                speaker: speaker.map(str::to_string),
                line: line.into(),
            },
        )
    }

    pub fn title(self, start: f32, duration: f32, line: impl Into<String>) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::Title { line: line.into() },
        )
    }

    pub fn fade(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: f32,
        to: f32,
        color: Vec3,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::Fade { from, to, color },
        )
    }

    pub fn fade_in(self, start: f32, duration: f32) -> Self {
        self.fade(
            start,
            duration,
            EasingFunction::QuadOut,
            1.0,
            0.0,
            Vec3::zeros(),
        )
    }

    pub fn fade_out(self, start: f32, duration: f32) -> Self {
        self.fade(
            start,
            duration,
            EasingFunction::QuadIn,
            0.0,
            1.0,
            Vec3::zeros(),
        )
    }

    pub fn letterbox(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: f32,
        to: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::Letterbox { from, to },
        )
    }

    pub fn letterbox_in(self, start: f32, duration: f32) -> Self {
        self.letterbox(start, duration, EasingFunction::CubicOut, 0.0, 1.0)
    }

    pub fn letterbox_out(self, start: f32, duration: f32) -> Self {
        self.letterbox(start, duration, EasingFunction::CubicIn, 1.0, 0.0)
    }

    pub fn focus_pull(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from_distance: f32,
        to_distance: f32,
        range: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::FocusPull {
                from_distance,
                to_distance,
                range,
            },
        )
    }

    pub fn grade(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        exposure_ev: (f32, f32),
        saturation: (f32, f32),
        contrast: (f32, f32),
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::Grade {
                exposure_ev,
                saturation,
                contrast,
            },
        )
    }

    pub fn trigger(self, start: f32, id: impl Into<String>) -> Self {
        self.push(
            start,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::Trigger { id: id.into() },
        )
    }

    pub fn sound(self, start: f32, clip: impl Into<String>, volume: f32) -> Self {
        self.push(
            start,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::Sound {
                clip: clip.into(),
                volume,
            },
        )
    }

    pub fn music(self, start: f32, track: impl Into<String>, volume: f32) -> Self {
        self.push(
            start,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::Music {
                track: track.into(),
                volume,
            },
        )
    }

    /// Appends an action that begins `gap` seconds after everything authored so
    /// far ends. Lets a scene be written as a sequence without hand counting
    /// timestamps.
    pub fn then(
        self,
        gap: f32,
        duration: f32,
        easing: EasingFunction,
        action: CutsceneAction,
    ) -> Self {
        let start = self.duration() + gap;
        self.push(start, duration, easing, action)
    }

    pub fn then_dialogue(
        self,
        gap: f32,
        duration: f32,
        speaker: Option<&str>,
        line: impl Into<String>,
    ) -> Self {
        let start = self.duration() + gap;
        self.dialogue(start, duration, speaker, line)
    }

    /// Pushes a back to back run of dialogue lines, each `(speaker, line,
    /// duration)`, separated by `gap` seconds, starting at `start`.
    pub fn dialogue_sequence(
        mut self,
        start: f32,
        gap: f32,
        lines: &[(Option<&str>, &str, f32)],
    ) -> Self {
        let mut cursor = start;
        for (speaker, line, duration) in lines {
            self = self.dialogue(cursor, *duration, *speaker, *line);
            cursor += duration + gap;
        }
        self
    }
}

fn catmull_rom(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
    let t2 = t * t;
    let t3 = t2 * t;
    0.5 * ((2.0 * p1)
        + (-p0 + p2) * t
        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
        + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
}

fn catmull_rom_vec3(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
    Vec3::new(
        catmull_rom(p0.x, p1.x, p2.x, p3.x, t),
        catmull_rom(p0.y, p1.y, p2.y, p3.y, t),
        catmull_rom(p0.z, p1.z, p2.z, p3.z, t),
    )
}

pub fn sample_camera_path(waypoints: &[CutsceneShot], progress: f32) -> CutsceneShot {
    match waypoints.len() {
        0 => CutsceneShot::new(Vec3::zeros(), -Vec3::z()),
        1 => waypoints[0],
        _ => {
            let segments = waypoints.len() - 1;
            let scaled = (progress.clamp(0.0, 1.0) * segments as f32).min(segments as f32 - 1e-4);
            let index = scaled.floor() as usize;
            let local = scaled - index as f32;
            let point = |offset: isize| -> CutsceneShot {
                let clamped = (index as isize + offset).clamp(0, waypoints.len() as isize - 1);
                waypoints[clamped as usize]
            };
            let p0 = point(-1);
            let p1 = point(0);
            let p2 = point(1);
            let p3 = point(2);
            CutsceneShot {
                eye: catmull_rom_vec3(p0.eye, p1.eye, p2.eye, p3.eye, local),
                target: catmull_rom_vec3(p0.target, p1.target, p2.target, p3.target, local),
                field_of_view_degrees: catmull_rom(
                    p0.field_of_view_degrees,
                    p1.field_of_view_degrees,
                    p2.field_of_view_degrees,
                    p3.field_of_view_degrees,
                    local,
                ),
                roll_degrees: catmull_rom(
                    p0.roll_degrees,
                    p1.roll_degrees,
                    p2.roll_degrees,
                    p3.roll_degrees,
                    local,
                ),
            }
        }
    }
}

pub fn camera_look_rotation(eye: Vec3, target: Vec3, roll_degrees: f32) -> Quat {
    let direction = target - eye;
    if direction.magnitude_squared() < f32::EPSILON {
        return Quat::identity();
    }
    let direction = direction.normalize();
    let yaw = (-direction.x).atan2(-direction.z);
    let pitch = (-direction.y).clamp(-1.0, 1.0).asin();
    let yaw_rotation = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y());
    let pitch_rotation = nalgebra_glm::quat_angle_axis(-pitch, &Vec3::x());
    let look = yaw_rotation * pitch_rotation;
    if roll_degrees.abs() < f32::EPSILON {
        return look;
    }
    let roll = nalgebra_glm::quat_angle_axis(roll_degrees.to_radians(), &direction);
    roll * look
}
