use std::collections::{HashMap, HashSet};
use std::time::Instant;

use halley_config::PointerBindingAction;
use halley_core::cluster::ClusterId;
use halley_core::field::{NodeId, Vec2};
use smithay::input::pointer::{CursorIcon, MotionEvent, PointerHandle};
use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface};
use smithay::utils::SERIAL_COUNTER;
use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_constraint};

use crate::compositor::ctx::PointerCtx;
use crate::compositor::interaction::drag::DragCtx;
use crate::compositor::interaction::resize::ResizeCtx;
use crate::compositor::interaction::state::NodeMoveAnim;
use crate::compositor::root::Halley;
use crate::input::active_node_surface_transform_screen_details;

#[derive(Clone)]
pub(crate) struct BloomDragCtx {
    pub(crate) cluster_id: ClusterId,
    pub(crate) member_id: NodeId,
    pub(crate) monitor: String,
    pub(crate) slot_screen: (f32, f32),
}

#[derive(Clone)]
pub(crate) struct OverflowDragCtx {
    pub(crate) cluster_id: ClusterId,
    pub(crate) member_id: NodeId,
    pub(crate) monitor: String,
}

#[derive(Clone, Copy)]
pub(crate) struct HitNode {
    pub(crate) node_id: NodeId,
    pub(crate) move_surface: bool,
    pub(crate) is_core: bool,
}

pub(crate) const CORE_BLOOM_HOLD_MS: u64 = 1_700;

#[derive(Clone)]
pub(crate) struct PointerState {
    pub(crate) world: Vec2,
    pub(crate) screen: (f32, f32),
    pub(crate) workspace_size: (i32, i32),
    pub(crate) hover_node: Option<NodeId>,
    pub(crate) intercepted_buttons: HashMap<u32, PointerBindingAction>,
    pub(crate) intercepted_binding_buttons: HashSet<u32>,
    pub(crate) drag: Option<DragCtx>,
    pub(crate) resize: Option<ResizeCtx>,
    pub(crate) move_anim: HashMap<NodeId, NodeMoveAnim>,
    pub(crate) bloom_drag: Option<BloomDragCtx>,
    pub(crate) overflow_drag: Option<OverflowDragCtx>,
    pub(crate) panning: bool,
    pub(crate) pan_monitor: Option<String>,
    pub(crate) pan_last_screen: (f32, f32),
    pub(crate) left_button_down: bool,
    pub(crate) hover_started_at: Option<Instant>,
    pub(crate) preview_block_until: Option<Instant>,
    pub(crate) resize_trace_node: Option<NodeId>,
    pub(crate) resize_trace_until: Option<Instant>,
    pub(crate) resize_trace_last_at: Option<Instant>,
}

impl Default for PointerState {
    fn default() -> Self {
        Self {
            world: Vec2 { x: 0.0, y: 0.0 },
            screen: (0.0, 0.0),
            workspace_size: (1, 1),
            hover_node: None,
            intercepted_buttons: HashMap::new(),
            intercepted_binding_buttons: HashSet::new(),
            drag: None,
            resize: None,
            move_anim: HashMap::new(),
            bloom_drag: None,
            overflow_drag: None,
            panning: false,
            pan_monitor: None,
            pan_last_screen: (0.0, 0.0),
            left_button_down: false,
            hover_started_at: None,
            preview_block_until: None,
            resize_trace_node: None,
            resize_trace_until: None,
            resize_trace_last_at: None,
        }
    }
}

pub(crate) fn cursor_position_hint(
    ctx: &mut PointerCtx<'_>,
    surface: &WlSurface,
    pointer: &PointerHandle<Halley>,
    location: smithay::utils::Point<f64, smithay::utils::Logical>,
) {
    apply_cursor_position_hint(ctx.st, surface, pointer, location);
}

pub(crate) fn set_cursor_override_icon(st: &mut Halley, icon: Option<CursorIcon>) {
    st.input.interaction_state.cursor_override_icon = icon;
    st.input.interaction_state.cursor_override_until_ms = None;
}

pub(crate) fn set_temporary_cursor_override_icon(
    st: &mut Halley,
    icon: CursorIcon,
    now: Instant,
    duration_ms: u64,
) {
    st.input.interaction_state.cursor_override_icon = Some(icon);
    st.input.interaction_state.cursor_override_until_ms =
        Some(st.now_ms(now).saturating_add(duration_ms.max(1)));
    st.request_maintenance();
}

pub(crate) fn activate_pointer_constraint_for_surface(st: &mut Halley, surface: &WlSurface) {
    let Some(pointer) = st.platform.seat.get_pointer() else {
        return;
    };
    with_pointer_constraint(surface, &pointer, |constraint| {
        if let Some(constraint) = constraint
            && !constraint.is_active()
        {
            constraint.activate();
        }
    });
}

pub(crate) fn clear_pointer_focus(st: &mut Halley) {
    let Some(pointer) = st.platform.seat.get_pointer() else {
        return;
    };
    st.input.interaction_state.grabbed_layer_surface = None;
    if pointer.is_grabbed() {
        pointer.unset_grab(st, SERIAL_COUNTER.next_serial(), 0);
    }
    let location = pointer.current_location();
    pointer.motion(
        st,
        None,
        &MotionEvent {
            location,
            serial: SERIAL_COUNTER.next_serial(),
            time: 0,
        },
    );
    pointer.frame(st);
}

pub(crate) fn center_pointer_on_node(st: &mut Halley, node_id: NodeId, now: Instant) -> bool {
    let Some(pointer) = st.platform.seat.get_pointer() else {
        return false;
    };
    let Some(node_pos) = st.model.field.node(node_id).map(|node| node.pos) else {
        return false;
    };

    let monitor = st
        .model
        .monitor_state
        .node_monitor
        .get(&node_id)
        .cloned()
        .unwrap_or_else(|| st.model.monitor_state.current_monitor.clone());
    let Some(space) = st.model.monitor_state.monitors.get(monitor.as_str()) else {
        return false;
    };
    let monitor_width = space.width;
    let monitor_height = space.height;
    let monitor_offset_x = space.offset_x;
    let monitor_offset_y = space.offset_y;

    let previous_monitor = st.begin_temporary_render_monitor(monitor.as_str());
    let (local_sx, local_sy) = crate::presentation::world_to_screen(
        st,
        monitor_width,
        monitor_height,
        node_pos.x,
        node_pos.y,
    );
    let local_sx_f = local_sx.clamp(0, monitor_width.saturating_sub(1).max(0)) as f32;
    let local_sy_f = local_sy.clamp(0, monitor_height.saturating_sub(1).max(0)) as f32;
    let global_sx = monitor_offset_x as f32 + local_sx_f;
    let global_sy = monitor_offset_y as f32 + local_sy_f;
    st.input.interaction_state.pending_pointer_screen_hint = Some((global_sx, global_sy));

    let focus = crate::input::pointer::focus::pointer_focus_for_screen(
        st,
        monitor_width,
        monitor_height,
        local_sx_f,
        local_sy_f,
        now,
        None,
    );
    let cam_scale = st.camera_render_scale().max(0.001) as f64;
    pointer.motion(
        st,
        focus,
        &MotionEvent {
            location: (local_sx_f as f64 / cam_scale, local_sy_f as f64 / cam_scale).into(),
            serial: SERIAL_COUNTER.next_serial(),
            time: 0,
        },
    );
    pointer.frame(st);
    st.end_temporary_render_monitor(previous_monitor);
    st.request_maintenance();
    true
}

pub(crate) fn apply_cursor_position_hint(
    st: &mut Halley,
    surface: &WlSurface,
    pointer: &PointerHandle<Halley>,
    location: smithay::utils::Point<f64, smithay::utils::Logical>,
) {
    let Some(node_id) = st.model.surface_to_node.get(&surface.id()).copied() else {
        return;
    };
    let monitor = st
        .model
        .monitor_state
        .node_monitor
        .get(&node_id)
        .cloned()
        .unwrap_or_else(|| st.model.monitor_state.current_monitor.clone());
    let (ws_w, ws_h, _, _) = st.local_screen_in_monitor(monitor.as_str(), 0.0, 0.0);
    let previous_monitor = st.begin_temporary_render_monitor(monitor.as_str());
    let Some(xform) =
        active_node_surface_transform_screen_details(st, ws_w, ws_h, node_id, Instant::now(), None)
    else {
        st.end_temporary_render_monitor(previous_monitor);
        return;
    };

    let sx =
        (xform.origin_x + location.x as f32 * xform.scale).clamp(0.0, (ws_w.max(1) - 1) as f32);
    let sy =
        (xform.origin_y + location.y as f32 * xform.scale).clamp(0.0, (ws_h.max(1) - 1) as f32);
    st.input.interaction_state.pending_pointer_screen_hint = Some((sx, sy));

    let cam_scale = st.camera_render_scale().max(0.001) as f64;
    let focus_origin = smithay::utils::Point::<f64, smithay::utils::Logical>::from((
        xform.origin_x as f64 / cam_scale,
        xform.origin_y as f64 / cam_scale,
    ));
    pointer.motion(
        st,
        Some((surface.clone(), focus_origin)),
        &MotionEvent {
            location: (sx as f64 / cam_scale, sy as f64 / cam_scale).into(),
            serial: SERIAL_COUNTER.next_serial(),
            time: 0,
        },
    );
    pointer.frame(st);
    st.end_temporary_render_monitor(previous_monitor);
}

pub(crate) fn release_active_pointer_constraint(st: &mut Halley) -> bool {
    let Some(pointer) = st.platform.seat.get_pointer() else {
        return false;
    };
    let Some(surface) = pointer.current_focus() else {
        return false;
    };
    let mut released = false;
    with_pointer_constraint(&surface, &pointer, |constraint| {
        if let Some(constraint) = constraint
            && constraint.is_active()
        {
            constraint.deactivate();
            released = true;
        }
    });
    if released {
        clear_pointer_focus(st);
        st.input.interaction_state.reset_input_state_requested = true;
    }
    released
}

pub(crate) fn active_constrained_pointer_surface(st: &Halley) -> Option<(WlSurface, bool)> {
    let pointer = st.platform.seat.get_pointer()?;
    let surface = pointer.current_focus()?;
    let is_locked = with_pointer_constraint(&surface, &pointer, |constraint| {
        let active = constraint
            .as_deref()
            .is_some_and(PointerConstraint::is_active);
        let locked = matches!(constraint.as_deref(), Some(PointerConstraint::Locked(_)));
        if active { Some(locked) } else { None }
    })?;
    Some((surface, is_locked))
}
