use {
    crate::{
        cmm::{cmm_description::ColorDescription, cmm_render_intent::RenderIntent},
        gfx_api::{GfxTexture, SampleRect},
        ifs::wl_surface::{SurfaceBuffer, WlSurface},
        rect::Rect,
        state::State,
        theme::Color,
        tree::{LatchListener, NodeId, OutputNode},
        utils::{clonecell::CloneCell, event_listener::EventListener},
    },
    ahash::AHashMap,
    std::{
        cell::{Cell, RefCell},
        rc::{Rc, Weak},
    },
};

const DEFAULT_DURATION_MS: u32 = 160;
const SPAWN_IN_INITIAL_SCALE_NUMERATOR: i32 = 4;
const SPAWN_IN_INITIAL_SCALE_DENOMINATOR: i32 = 5;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum AnimationCurve {
    Linear,
    Ease,
    EaseIn,
    EaseOut,
    EaseInOut,
}

impl AnimationCurve {
    pub fn from_config(value: u32) -> Self {
        match value {
            0 => Self::Linear,
            1 => Self::Ease,
            2 => Self::EaseIn,
            4 => Self::EaseInOut,
            _ => Self::EaseOut,
        }
    }

    fn sample(self, t: f64) -> f64 {
        let t = t.clamp(0.0, 1.0);
        match self {
            Self::Linear => t,
            Self::Ease => cubic_bezier(0.25, 0.1, 0.25, 1.0, t),
            Self::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, t),
            Self::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, t),
            Self::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, t),
        }
    }
}

pub struct AnimationState {
    pub enabled: Cell<bool>,
    pub duration_ms: Cell<u32>,
    pub curve: Cell<AnimationCurve>,
    windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
    tick: CloneCell<Option<Rc<AnimationTick>>>,
}

pub struct RetainedToplevel {
    pub offset: (i32, i32),
    pub surface: RetainedSurface,
}

pub struct RetainedSurface {
    pub offset: (i32, i32),
    pub size: (i32, i32),
    pub content: RetainedContent,
    pub below: Vec<RetainedSurface>,
    pub above: Vec<RetainedSurface>,
}

pub enum RetainedContent {
    Texture {
        texture: Rc<dyn GfxTexture>,
        buffer: Rc<SurfaceBuffer>,
        source: SampleRect,
        alpha: Option<f32>,
        color_description: Rc<ColorDescription>,
        render_intent: RenderIntent,
        alpha_mode: crate::gfx_api::AlphaMode,
        opaque: bool,
    },
    Color {
        color: Color,
        alpha: Option<f32>,
        color_description: Rc<ColorDescription>,
        render_intent: RenderIntent,
    },
}

impl RetainedToplevel {
    pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option<Rc<Self>> {
        Some(Rc::new(Self {
            offset,
            surface: RetainedSurface::capture(surface, (0, 0))?,
        }))
    }
}

impl RetainedSurface {
    fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option<Self> {
        let buffer = surface.buffer.get()?;
        let size = surface.buffer_abs_pos.get().size();
        let source = *surface.buffer_points_norm.borrow();
        let color_description = surface.color_description();
        let render_intent = surface.render_intent();
        let alpha_mode = surface.alpha_mode();
        let alpha = surface.alpha();
        let content = match buffer.buffer.buf.get_stable_texture() {
            Some(texture) => RetainedContent::Texture {
                opaque: surface.opaque(),
                texture,
                buffer,
                source,
                alpha,
                color_description,
                render_intent,
                alpha_mode,
            },
            None => {
                let color = buffer.buffer.buf.color?;
                RetainedContent::Color {
                    color: Color::from_u32(
                        color_description.eotf,
                        alpha_mode,
                        color[0],
                        color[1],
                        color[2],
                        color[3],
                    ),
                    alpha,
                    color_description,
                    render_intent,
                }
            }
        };
        let mut below = vec![];
        let mut above = vec![];
        if let Some(children) = surface.children.borrow().as_deref() {
            for child in children.below.iter() {
                if child.pending.get() {
                    continue;
                }
                let pos = child.sub_surface.position.get();
                below.push(Self::capture(&child.sub_surface.surface, pos)?);
            }
            for child in children.above.iter() {
                if child.pending.get() {
                    continue;
                }
                let pos = child.sub_surface.position.get();
                above.push(Self::capture(&child.sub_surface.surface, pos)?);
            }
        }
        Some(Self {
            offset,
            size,
            content,
            below,
            above,
        })
    }
}

impl Default for AnimationState {
    fn default() -> Self {
        Self {
            enabled: Cell::new(false),
            duration_ms: Cell::new(DEFAULT_DURATION_MS),
            curve: Cell::new(AnimationCurve::EaseOut),
            windows: Default::default(),
            tick: Default::default(),
        }
    }
}

impl AnimationState {
    pub fn clear(&self) {
        self.windows.borrow_mut().clear();
        if let Some(tick) = self.tick.take() {
            tick.detach();
        }
    }

    pub fn set_target(
        &self,
        node_id: NodeId,
        old: Rect,
        new: Rect,
        retained: Option<Rc<RetainedToplevel>>,
        now_nsec: u64,
        duration_ms: u32,
        curve: AnimationCurve,
    ) -> bool {
        if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 {
            self.windows.borrow_mut().remove(&node_id);
            return false;
        }
        let duration_nsec = duration_ms as u64 * 1_000_000;
        let mut windows = self.windows.borrow_mut();
        let (from, retained) = match windows.get(&node_id) {
            Some(anim) if anim.to == new => return false,
            Some(anim) => (anim.rect_at(now_nsec), anim.retained.clone().or(retained)),
            None => (old, retained),
        };
        if from == new {
            windows.remove(&node_id);
            return false;
        }
        windows.insert(
            node_id,
            WindowAnimation {
                from,
                to: new,
                start_nsec: now_nsec,
                duration_nsec,
                curve,
                last_damage: from,
                retained,
            },
        );
        true
    }

    pub fn set_spawn_in(
        &self,
        node_id: NodeId,
        target: Rect,
        retained: Option<Rc<RetainedToplevel>>,
        now_nsec: u64,
        duration_ms: u32,
    ) -> bool {
        let start = spawn_in_start_rect(target);
        self.set_target(
            node_id,
            start,
            target,
            retained,
            now_nsec,
            duration_ms,
            AnimationCurve::Linear,
        )
    }

    pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
        let windows = self.windows.borrow();
        match windows.get(&node_id) {
            Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
            _ => layout,
        }
    }

    pub fn retained_snapshot(
        &self,
        node_id: NodeId,
        now_nsec: u64,
    ) -> Option<Rc<RetainedToplevel>> {
        let windows = self.windows.borrow();
        match windows.get(&node_id) {
            Some(anim) if !anim.done(now_nsec) => anim.retained.clone(),
            _ => None,
        }
    }

    fn damage_active(&self, state: &State, now_nsec: u64) -> bool {
        let mut damages = vec![];
        let mut any_active = false;
        {
            let mut windows = self.windows.borrow_mut();
            windows.retain(|_, anim| {
                let current = anim.rect_at(now_nsec);
                let damage = anim.last_damage.union(current).union(anim.to);
                damages.push(expand_damage_rect(
                    damage,
                    state.theme.sizes.border_width.get().max(0),
                ));
                anim.last_damage = current;
                let active = !anim.done(now_nsec);
                any_active |= active;
                active
            });
        }
        for damage in damages {
            state.damage(damage);
        }
        any_active
    }

    pub(crate) fn tick_is_active(&self) -> bool {
        self.tick.is_some()
    }

    pub(crate) fn set_tick(&self, tick: Rc<AnimationTick>) {
        self.tick.set(Some(tick));
    }

    pub(crate) fn clear_tick(&self) {
        self.tick.take();
    }
}

struct WindowAnimation {
    from: Rect,
    to: Rect,
    start_nsec: u64,
    duration_nsec: u64,
    curve: AnimationCurve,
    last_damage: Rect,
    retained: Option<Rc<RetainedToplevel>>,
}

impl WindowAnimation {
    fn done(&self, now_nsec: u64) -> bool {
        now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
    }

    fn rect_at(&self, now_nsec: u64) -> Rect {
        if self.duration_nsec == 0 {
            return self.to;
        }
        let elapsed = now_nsec.saturating_sub(self.start_nsec);
        let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
        let t = self.curve.sample(t);
        lerp_rect(self.from, self.to, t)
    }
}

pub struct AnimationTick {
    state: Weak<State>,
    slf: Weak<dyn LatchListener>,
    latch_listeners: RefCell<Vec<EventListener<dyn LatchListener>>>,
}

impl AnimationTick {
    pub fn new(state: &Rc<State>, slf: &Weak<Self>) -> Self {
        let slf: Weak<dyn LatchListener> = slf.clone();
        Self {
            state: Rc::downgrade(state),
            slf,
            latch_listeners: Default::default(),
        }
    }

    pub fn attach(&self, output: &OutputNode) {
        let listener = EventListener::new(self.slf.clone());
        listener.attach(&output.latch_event);
        self.latch_listeners.borrow_mut().push(listener);
    }

    pub fn detach(&self) {
        for listener in self.latch_listeners.borrow_mut().drain(..) {
            listener.detach();
        }
    }
}

impl LatchListener for AnimationTick {
    fn after_latch(self: Rc<Self>, _on: &OutputNode, _tearing: bool) {
        let Some(state) = self.state.upgrade() else {
            self.detach();
            return;
        };
        let active = state.animations.damage_active(&state, state.now_nsec());
        if !active {
            self.detach();
            state.animations.clear_tick();
        }
    }
}

pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect {
    fn scaled_dimension(value: i32) -> i32 {
        let scaled = (value as i64 * SPAWN_IN_INITIAL_SCALE_NUMERATOR as i64
            / SPAWN_IN_INITIAL_SCALE_DENOMINATOR as i64) as i32;
        scaled.clamp(1, value.max(1))
    }

    let width = scaled_dimension(target.width());
    let height = scaled_dimension(target.height());
    Rect::new_sized_saturating(
        target.x1() + (target.width() - width) / 2,
        target.y1() + (target.height() - height) / 2,
        width,
        height,
    )
}

fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
    fn lerp(from: i32, to: i32, t: f64) -> i32 {
        (from as f64 + (to as f64 - from as f64) * t).round() as i32
    }
    Rect::new_saturating(
        lerp(from.x1(), to.x1(), t),
        lerp(from.y1(), to.y1(), t),
        lerp(from.x2(), to.x2(), t),
        lerp(from.y2(), to.y2(), t),
    )
}

pub(crate) fn expand_damage_rect(rect: Rect, width: i32) -> Rect {
    Rect::new_saturating(
        rect.x1().saturating_sub(width),
        rect.y1().saturating_sub(width),
        rect.x2().saturating_add(width),
        rect.y2().saturating_add(width),
    )
}

fn cubic_bezier(x1: f64, y1: f64, x2: f64, y2: f64, x: f64) -> f64 {
    fn bezier(a: f64, b: f64, t: f64) -> f64 {
        let inv = 1.0 - t;
        3.0 * inv * inv * t * a + 3.0 * inv * t * t * b + t * t * t
    }
    let mut lo = 0.0;
    let mut hi = 1.0;
    let mut t = x;
    for _ in 0..12 {
        let bx = bezier(x1, x2, t);
        if (bx - x).abs() < 0.000_001 {
            break;
        }
        if bx < x {
            lo = t;
        } else {
            hi = t;
        }
        t = (lo + hi) * 0.5;
    }
    bezier(y1, y2, t)
}

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

    #[test]
    fn linear_rect_interpolation_is_symmetric() {
        let a = Rect::new_sized_saturating(0, 0, 100, 100);
        let b = Rect::new_sized_saturating(100, 40, 200, 80);
        assert_eq!(lerp_rect(a, b, 0.25), lerp_rect(b, a, 0.75));
    }

    #[test]
    fn unchanged_target_does_not_restart() {
        let state = AnimationState::default();
        let id = NodeId(1);
        let a = Rect::new_sized_saturating(0, 0, 100, 100);
        let b = Rect::new_sized_saturating(100, 0, 100, 100);
        assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear));
        assert!(!state.set_target(id, a, b, None, 80_000_000, 160, AnimationCurve::Linear));
        assert_eq!(
            state.visual_rect(id, b, 80_000_000),
            Rect::new_sized_saturating(50, 0, 100, 100)
        );
    }

    #[test]
    fn changed_target_restarts_from_current_visual_rect() {
        let state = AnimationState::default();
        let id = NodeId(1);
        let a = Rect::new_sized_saturating(0, 0, 100, 100);
        let b = Rect::new_sized_saturating(100, 0, 100, 100);
        let c = Rect::new_sized_saturating(200, 0, 100, 100);
        assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear));
        assert!(state.set_target(id, a, c, None, 80_000_000, 160, AnimationCurve::Linear));
        assert_eq!(
            state.visual_rect(id, c, 80_000_000),
            Rect::new_sized_saturating(50, 0, 100, 100)
        );
        assert_eq!(
            state.visual_rect(id, c, 160_000_000),
            Rect::new_sized_saturating(125, 0, 100, 100)
        );
    }

    #[test]
    fn spawn_in_start_rect_is_centered_and_non_empty() {
        let target = Rect::new_sized_saturating(10, 20, 100, 50);
        assert_eq!(
            spawn_in_start_rect(target),
            Rect::new_sized_saturating(20, 25, 80, 40)
        );
    }

    #[test]
    fn spawn_in_uses_linear_curve() {
        let state = AnimationState::default();
        let id = NodeId(1);
        let target = Rect::new_sized_saturating(10, 20, 100, 50);
        assert!(state.set_spawn_in(id, target, None, 0, 160));
        assert_eq!(
            state.visual_rect(id, target, 80_000_000),
            Rect::new_sized_saturating(15, 23, 90, 45)
        );
    }
}
