use ahash::AHashMap;
use bevy::{
    input::{mouse::MouseButtonInput, ButtonState},
    prelude::*,
    window::PrimaryWindow,
};
use de_core::{
    gamestate::GameState, schedule::InputSchedule, screengeom::ScreenRect, state::AppState,
};
use crate::hud::HudNodes;
const DRAGGING_THRESHOLD: f32 = 0.02;
const DOUBLE_CLICK_TIME: f64 = 0.5;
pub(super) struct InputPlugin;
impl Plugin for InputPlugin {
    fn build(&self, app: &mut App) {
        app.add_event::<MouseClickedEvent>()
            .add_event::<MouseDoubleClickedEvent>()
            .add_event::<MouseDraggedEvent>()
            .add_systems(OnEnter(AppState::InGame), setup)
            .add_systems(OnExit(AppState::InGame), cleanup)
            .add_systems(
                InputSchedule,
                (
                    update_position.in_set(MouseSet::Position),
                    update_drags
                        .run_if(resource_exists_and_changed::<MousePosition>)
                        .in_set(MouseSet::Drags)
                        .after(MouseSet::Position),
                    update_buttons
                        .in_set(MouseSet::SingeButton)
                        .after(MouseSet::Drags),
                    check_double_click
                        .in_set(MouseSet::Buttons)
                        .after(MouseSet::SingeButton),
                )
                    .run_if(in_state(GameState::Playing)),
            );
    }
}
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemSet)]
pub(crate) enum MouseSet {
    Position,
    Drags,
    SingeButton,
    Buttons,
}
#[derive(Event)]
pub(crate) struct MouseClickedEvent {
    button: MouseButton,
    position: Vec2,
}
impl MouseClickedEvent {
    fn new(button: MouseButton, position: Vec2) -> Self {
        Self { button, position }
    }
    pub(crate) fn button(&self) -> MouseButton {
        self.button
    }
    pub(crate) fn position(&self) -> Vec2 {
        self.position
    }
}
#[derive(Event)]
pub(crate) struct MouseDoubleClickedEvent {
    button: MouseButton,
}
impl MouseDoubleClickedEvent {
    fn new(button: MouseButton) -> Self {
        Self { button }
    }
    pub(crate) fn button(&self) -> MouseButton {
        self.button
    }
}
#[derive(Event)]
pub(crate) struct MouseDraggedEvent {
    button: MouseButton,
    rect: Option<ScreenRect>,
    update_type: DragUpdateType,
}
impl MouseDraggedEvent {
    fn new(button: MouseButton, rect: Option<ScreenRect>, update_type: DragUpdateType) -> Self {
        Self {
            button,
            rect,
            update_type,
        }
    }
    pub(crate) fn button(&self) -> MouseButton {
        self.button
    }
    pub(crate) fn rect(&self) -> Option<ScreenRect> {
        self.rect
    }
    pub(crate) fn update_type(&self) -> DragUpdateType {
        self.update_type
    }
}
#[derive(Clone, Copy)]
pub(crate) enum DragUpdateType {
    Moved,
    Released,
}
#[derive(Default, Resource)]
pub(crate) struct MousePosition(Option<Vec2>);
impl MousePosition {
    pub(crate) fn ndc(&self) -> Option<Vec2> {
        self.0.map(|p| Vec2::new(2. * p.x - 1., 1. - 2. * p.y))
    }
    fn position(&self) -> Option<Vec2> {
        self.0
    }
    fn set_position(&mut self, position: Option<Vec2>) {
        self.0 = position;
    }
}
#[derive(Default, Resource)]
struct MouseDragStates(AHashMap<MouseButton, DragState>);
impl MouseDragStates {
    fn set(&mut self, button: MouseButton, position: Option<Vec2>) {
        self.0.insert(button, DragState::new(position));
    }
    fn resolve(&mut self, button: MouseButton) -> Option<DragResolution> {
        self.0.remove(&button).and_then(DragState::resolve)
    }
    fn update(&mut self, position: Option<Vec2>) -> AHashMap<MouseButton, Option<ScreenRect>> {
        let mut updates = AHashMap::new();
        for (&button, drag) in self.0.iter_mut() {
            if let Some(update) = drag.update(position) {
                updates.insert(button, update);
            }
        }
        updates
    }
}
struct DragState {
    start: Option<Vec2>,
    stop: Option<Vec2>,
    active: bool,
}
impl DragState {
    fn new(start: Option<Vec2>) -> Self {
        Self {
            start,
            stop: start,
            active: false,
        }
    }
    fn resolve(self) -> Option<DragResolution> {
        match self.start {
            Some(start) => match (self.active, self.stop) {
                (true, Some(stop)) => Some(DragResolution::Rect(Some(ScreenRect::from_points(
                    start, stop,
                )))),
                (true, None) => Some(DragResolution::Rect(None)),
                (false, Some(stop)) => Some(DragResolution::Point(stop)),
                (false, None) => None,
            },
            None => None,
        }
    }
    fn update(&mut self, position: Option<Vec2>) -> Option<Option<ScreenRect>> {
        let changed = self.stop != position;
        self.stop = position;
        if let Some(start) = self.start {
            let rect = match self.stop {
                Some(stop) => {
                    self.active |= start.distance(stop) >= DRAGGING_THRESHOLD;
                    Some(ScreenRect::from_points(start, stop))
                }
                None => None,
            };
            if self.active && changed {
                return Some(rect);
            }
        }
        None
    }
}
enum DragResolution {
    Point(Vec2),
    Rect(Option<ScreenRect>),
}
fn setup(mut commands: Commands) {
    commands.init_resource::<MousePosition>();
    commands.init_resource::<MouseDragStates>();
}
fn cleanup(mut commands: Commands) {
    commands.remove_resource::<MousePosition>();
    commands.remove_resource::<MouseDragStates>();
}
fn update_position(
    window_query: Query<&Window, With<PrimaryWindow>>,
    hud: HudNodes,
    mut mouse: ResMut<MousePosition>,
) {
    let window = window_query.single();
    let position = window
        .cursor_position()
        .filter(|&position| !hud.contains_point(position))
        .map(|position| position / Vec2::new(window.width(), window.height()))
        .map(|normalised_position| normalised_position.clamp(Vec2::ZERO, Vec2::ONE));
    if mouse.position() != position {
        mouse.set_position(position)
    }
}
fn update_drags(
    mouse_position: Res<MousePosition>,
    mut mouse_state: ResMut<MouseDragStates>,
    mut drags: EventWriter<MouseDraggedEvent>,
) {
    let resolutions = mouse_state.update(mouse_position.ndc());
    for (&button, &rect) in resolutions.iter() {
        drags.send(MouseDraggedEvent::new(button, rect, DragUpdateType::Moved));
    }
}
fn update_buttons(
    mouse_position: Res<MousePosition>,
    mut mouse_state: ResMut<MouseDragStates>,
    mut input_events: EventReader<MouseButtonInput>,
    mut clicks: EventWriter<MouseClickedEvent>,
    mut drags: EventWriter<MouseDraggedEvent>,
) {
    for event in input_events.read() {
        match event.state {
            ButtonState::Released => {
                if let Some(drag_resolution) = mouse_state.resolve(event.button) {
                    match drag_resolution {
                        DragResolution::Point(position) => {
                            clicks.send(MouseClickedEvent::new(event.button, position));
                        }
                        DragResolution::Rect(rect) => {
                            drags.send(MouseDraggedEvent::new(
                                event.button,
                                rect,
                                DragUpdateType::Released,
                            ));
                        }
                    }
                }
            }
            ButtonState::Pressed => {
                mouse_state.set(event.button, mouse_position.ndc());
            }
        }
    }
}
fn check_double_click(
    mut clicks: EventReader<MouseClickedEvent>,
    mut double_clicks: EventWriter<MouseDoubleClickedEvent>,
    mut last_click_position: Local<Option<Vec2>>,
    mut last_click_time: Local<f64>,
    time: Res<Time>,
) {
    for mouse_clicked in clicks.read() {
        let current_time = time.elapsed_seconds_f64();
        if last_click_position.map_or(true, |p| {
            p.distance(mouse_clicked.position()) < DRAGGING_THRESHOLD
        }) {
            if (current_time - *last_click_time) < DOUBLE_CLICK_TIME {
                double_clicks.send(MouseDoubleClickedEvent::new(mouse_clicked.button()));
            }
        }
        *last_click_time = time.elapsed_seconds_f64();
        *last_click_position = Some(mouse_clicked.position());
    }
}