use std::collections::VecDeque;
use std::time::Instant;
use dioxus::prelude::*;
use freya_elements::elements as dioxus_elements;
use freya_elements::events::{touch::TouchPhase, TouchEvent};
const DOUBLE_TAP_DISTANCE: f64 = 100.0;
const DOUBLE_TAP_TIMEOUT: u128 = 300; const DOUBLE_TAP_MIN: u128 = 40; const MAX_EVENTS_QUEUE: usize = 20;
#[derive(Debug, PartialEq, Eq)]
pub enum Gesture {
    TapUp,
    TapDown,
    DoubleTap,
}
#[derive(Props)]
pub struct GestureAreaProps<'a> {
    pub children: Element<'a>,
    pub ongesture: EventHandler<'a, Gesture>,
}
type EventsQueue = VecDeque<(Instant, TouchEvent)>;
#[allow(non_snake_case)]
pub fn GestureArea<'a>(cx: Scope<'a, GestureAreaProps<'a>>) -> Element {
    let touch_events = use_ref::<EventsQueue>(cx, VecDeque::new);
    use_effect(cx, touch_events, move |_| {
        if touch_events.read().len() > MAX_EVENTS_QUEUE {
            touch_events.write_silent().pop_front();
        }
        let find_previous_event = |start_time: &Instant,
                                   events: &EventsQueue,
                                   target_phase: TouchPhase|
         -> Option<(Instant, TouchEvent)> {
            let mut start = false;
            for (time, event) in events.iter().rev() {
                if time == start_time {
                    start = true;
                    continue;
                }
                if event.phase == target_phase && start {
                    return Some((*time, event.clone()));
                }
            }
            None
        };
        let touch_events = touch_events.read();
        let event = touch_events.iter().last();
        if let Some((time, event)) = event {
            let phase = event.get_touch_phase();
            match phase {
                TouchPhase::Started => {
                    cx.props.ongesture.call(Gesture::TapDown);
                    let last_ended_event =
                        find_previous_event(time, &touch_events, TouchPhase::Ended);
                    let last_started_event =
                        find_previous_event(time, &touch_events, TouchPhase::Started);
                    if let Some(((ended_time, ended_event), (started_time, _))) =
                        last_ended_event.zip(last_started_event)
                    {
                        let is_ended_close = event
                            .get_screen_coordinates()
                            .distance_to(ended_event.get_screen_coordinates())
                            < DOUBLE_TAP_DISTANCE;
                        let is_ended_mature = ended_time.elapsed().as_millis() >= DOUBLE_TAP_MIN;
                        let is_started_recent =
                            started_time.elapsed().as_millis() <= DOUBLE_TAP_TIMEOUT;
                        if is_ended_close && is_ended_mature && is_started_recent {
                            cx.props.ongesture.call(Gesture::DoubleTap);
                        }
                    }
                }
                TouchPhase::Ended => {
                    cx.props.ongesture.call(Gesture::TapUp);
                }
                _ => {}
            }
        }
        async move {}
    });
    let ontouchcancel = |e: TouchEvent| {
        touch_events.write().push_back((Instant::now(), e));
    };
    let ontouchend = |e: TouchEvent| {
        touch_events.write().push_back((Instant::now(), e));
    };
    let ontouchmove = |e: TouchEvent| {
        touch_events.write().push_back((Instant::now(), e));
    };
    let ontouchstart = |e: TouchEvent| {
        touch_events.write().push_back((Instant::now(), e));
    };
    render!(
        rect {
            ontouchcancel: ontouchcancel,
            ontouchend: ontouchend,
            ontouchmove: ontouchmove,
            ontouchstart: ontouchstart,
            &cx.props.children
        }
    )
}
#[cfg(test)]
mod test {
    use std::time::Duration;
    use freya::prelude::*;
    use freya_elements::events::touch::TouchPhase;
    use freya_testing::{launch_test, FreyaEvent};
    use tokio::time::sleep;
    use crate::gesture_area::DOUBLE_TAP_MIN;
    #[tokio::test]
    pub async fn double_tap() {
        fn dobule_tap_app(cx: Scope) -> Element {
            let value = use_state(cx, || "EMPTY".to_string());
            let ongesture = |e: Gesture| {
                value.set(format!("{e:?}"));
            };
            render!(
                GestureArea {
                    ongesture: ongesture,
                    "{value}"
                }
            )
        }
        let mut utils = launch_test(dobule_tap_app);
        utils.wait_for_update().await;
        assert_eq!(utils.root().get(0).get(0).text(), Some("EMPTY"));
        utils.push_event(FreyaEvent::Touch {
            name: "touchstart".to_string(),
            location: (1.0, 1.0).into(),
            phase: TouchPhase::Started,
            finger_id: 0,
            force: None,
        });
        utils.push_event(FreyaEvent::Touch {
            name: "touchend".to_string(),
            location: (1.0, 1.0).into(),
            phase: TouchPhase::Ended,
            finger_id: 0,
            force: None,
        });
        utils.wait_for_update().await;
        utils.wait_for_update().await;
        sleep(Duration::from_millis(DOUBLE_TAP_MIN as u64)).await;
        utils.push_event(FreyaEvent::Touch {
            name: "touchstart".to_string(),
            location: (1.0, 1.0).into(),
            phase: TouchPhase::Started,
            finger_id: 0,
            force: None,
        });
        utils.wait_for_update().await;
        utils.wait_for_update().await;
        assert_eq!(utils.root().get(0).get(0).text(), Some("DoubleTap"));
    }
    #[tokio::test]
    pub async fn tap_up_down() {
        fn tap_up_down_app(cx: Scope) -> Element {
            let value = use_state(cx, || "EMPTY".to_string());
            let ongesture = |e: Gesture| {
                value.set(format!("{e:?}"));
            };
            render!(
                GestureArea {
                    ongesture: ongesture,
                    "{value}"
                }
            )
        }
        let mut utils = launch_test(tap_up_down_app);
        utils.wait_for_update().await;
        assert_eq!(utils.root().get(0).get(0).text(), Some("EMPTY"));
        utils.push_event(FreyaEvent::Touch {
            name: "touchstart".to_string(),
            location: (1.0, 1.0).into(),
            phase: TouchPhase::Started,
            finger_id: 0,
            force: None,
        });
        utils.wait_for_update().await;
        utils.wait_for_update().await;
        assert_eq!(utils.root().get(0).get(0).text(), Some("TapDown"));
        utils.push_event(FreyaEvent::Touch {
            name: "touchend".to_string(),
            location: (1.0, 1.0).into(),
            phase: TouchPhase::Ended,
            finger_id: 0,
            force: None,
        });
        utils.wait_for_update().await;
        utils.wait_for_update().await;
        assert_eq!(utils.root().get(0).get(0).text(), Some("TapUp"));
    }
}