use dioxus_core::ScopeState;
use dioxus_hooks::{use_effect, use_memo, use_state, UseFutureDep, UseState};
use freya_engine::prelude::Color;
use freya_node_state::Parse;
use std::time::Duration;
use tokio::time::interval;
use uuid::Uuid;
use crate::{Animation, TransitionAnimation};
#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Transition {
    Size(f64, f64),
    Color(Color, Color),
}
impl Transition {
    pub fn new_size(start: f64, end: f64) -> Self {
        Self::Size(start, end)
    }
    pub fn new_color(start: &str, end: &str) -> Self {
        let start = Color::parse(start).unwrap();
        let end = Color::parse(end).unwrap();
        Self::Color(start, end)
    }
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub enum TransitionState {
    Size(f64),
    Color(Color),
}
impl From<&Transition> for TransitionState {
    fn from(value: &Transition) -> Self {
        match *value {
            Transition::Size(start, _) => Self::Size(start),
            Transition::Color(start, _) => Self::Color(start),
        }
    }
}
impl TransitionState {
    pub fn set_value(&mut self, animate: &Transition, value: f64) {
        match (self, animate) {
            (Self::Size(current), Transition::Size(start, end)) => {
                let road = *end - *start;
                let walked = (road / 100.0) * value;
                *current = walked;
            }
            (Self::Color(current), Transition::Color(start, end)) => {
                let apply_index = |v: u8, d: u8, value: f64| -> u8 {
                    let road = if d > v { d - v } else { v - d };
                    let walked = (road as f64 / 100.0) * value;
                    if d > v {
                        v + walked.round() as u8
                    } else {
                        v - walked.round() as u8
                    }
                };
                let r = apply_index(start.r(), end.r(), value);
                let g = apply_index(start.g(), end.g(), value);
                let b = apply_index(start.b(), end.b(), value);
                *current = Color::from_rgb(r, g, b)
            }
            _ => {}
        }
    }
    pub fn clear(&mut self, animate: &Transition) {
        match (self, animate) {
            (Self::Size(current), Transition::Size(start, _)) => {
                *current = *start;
            }
            (Self::Color(current), Transition::Color(start, _)) => {
                *current = *start;
            }
            _ => {}
        }
    }
    pub fn as_size(&self) -> f64 {
        self.to_size().unwrap()
    }
    pub fn as_color(&self) -> String {
        self.to_color().unwrap()
    }
    pub fn to_size(&self) -> Option<f64> {
        match self {
            Self::Size(current) => Some(*current),
            _ => None,
        }
    }
    pub fn to_color(&self) -> Option<String> {
        match self {
            Self::Color(current) => Some(format!(
                "rgb({}, {}, {})",
                current.r(),
                current.g(),
                current.b()
            )),
            _ => None,
        }
    }
    pub fn to_raw_color(&self) -> Option<Color> {
        match self {
            Self::Color(current) => Some(*current),
            _ => None,
        }
    }
}
#[derive(Clone)]
pub struct TransitionsManager<'a> {
    transitions: &'a Vec<Transition>,
    transitions_storage: &'a UseState<Vec<TransitionState>>,
    transition_animation: TransitionAnimation,
    current_animation_id: &'a UseState<Option<Uuid>>,
    cx: &'a ScopeState,
}
impl<'a> TransitionsManager<'a> {
    pub fn reverse(&self) {
        self.clear();
        let animation = self.transition_animation.to_animation(100.0..=0.0);
        self.run_with_animation(animation);
    }
    pub fn start(&self) {
        self.clear();
        let animation = self.transition_animation.to_animation(0.0..=100.0);
        self.run_with_animation(animation);
    }
    fn run_with_animation(&self, mut animation: Animation) {
        let animation_id = Uuid::new_v4();
        let transitions = self.transitions.clone();
        let transitions_storage = self.transitions_storage.clone();
        let current_animation_id = self.current_animation_id.clone();
        current_animation_id.set(Some(animation_id));
        self.cx.spawn(async move {
            let mut ticker = interval(Duration::from_millis(1));
            let mut index = 0;
            loop {
                if *current_animation_id.current() == Some(animation_id) {
                    if animation.is_finished() {
                        current_animation_id.set(None);
                        break;
                    }
                    let value = animation.move_value(index);
                    transitions_storage.with_mut(|storage| {
                        for (i, storage) in storage.iter_mut().enumerate() {
                            if let Some(conf) = transitions.get(i) {
                                storage.set_value(conf, value);
                            }
                        }
                    });
                    index += 1;
                    ticker.tick().await;
                } else {
                    break;
                }
            }
        });
    }
    pub fn clear(&self) {
        self.current_animation_id.set(None);
        self.transitions_storage.with_mut(|storage| {
            for (i, storage) in storage.iter_mut().enumerate() {
                if let Some(conf) = self.transitions.get(i) {
                    storage.clear(conf);
                }
            }
        })
    }
    pub fn is_animating(&self) -> bool {
        self.current_animation_id.is_some()
    }
    pub fn is_at_start(&self) -> bool {
        if let Some(storage) = self.get(0) {
            let anim = self.transitions[0];
            match anim {
                Transition::Size(start, _) => start == storage.to_size().unwrap_or(start),
                Transition::Color(start, _) => start == storage.to_raw_color().unwrap_or(start),
            }
        } else {
            true
        }
    }
    pub fn get(&self, index: usize) -> Option<TransitionState> {
        self.transitions_storage.current().get(index).copied()
    }
}
pub fn use_animation_transition<D>(
    cx: &ScopeState,
    transition: TransitionAnimation,
    dependencies: D,
    mut init: impl Fn(D::Out) -> Vec<Transition>,
) -> TransitionsManager
where
    D: UseFutureDep,
{
    let current_animation_id = use_state(cx, || None);
    let transitions = use_memo(cx, dependencies.clone(), &mut init);
    let transitions_storage = use_state(cx, || animations_map(transitions));
    use_effect(cx, dependencies, {
        let storage_setter = transitions_storage.setter();
        move |v| {
            storage_setter(animations_map(&init(v)));
            async move {}
        }
    });
    TransitionsManager {
        current_animation_id,
        transitions,
        transitions_storage,
        cx,
        transition_animation: transition,
    }
}
fn animations_map(animations: &[Transition]) -> Vec<TransitionState> {
    animations
        .iter()
        .map(TransitionState::from)
        .collect::<Vec<TransitionState>>()
}
#[cfg(test)]
mod test {
    use std::time::Duration;
    use crate::{use_animation_transition, Transition, TransitionAnimation};
    use dioxus_hooks::use_effect;
    use freya::prelude::*;
    use freya_testing::launch_test;
    use tokio::time::sleep;
    #[tokio::test]
    pub async fn track_progress() {
        fn use_animation_transition_app(cx: Scope) -> Element {
            let animation =
                use_animation_transition(cx, TransitionAnimation::new_linear(50), (), |_| {
                    vec![Transition::new_size(0.0, 100.0)]
                });
            let progress = animation.get(0).unwrap().as_size();
            use_effect(cx, (), move |_| {
                animation.start();
                async move {}
            });
            render!(rect {
                width: "{progress}",
            })
        }
        let mut utils = launch_test(use_animation_transition_app);
        utils.wait_for_update().await;
        assert_eq!(utils.root().get(0).layout().unwrap().width(), 0.0);
        utils.wait_for_update().await;
        utils.wait_for_update().await;
        let width = utils.root().get(0).layout().unwrap().width();
        assert!(width > 0.0);
        assert!(width < 100.0);
        sleep(Duration::from_millis(50)).await;
        utils.wait_for_update().await;
        let width = utils.root().get(0).layout().unwrap().width();
        assert_eq!(width, 100.0);
    }
}