//! Input-side state and event types: the touch/pointer tracking state, the
//! synthesized touch events and the handle gestures.

use super::*;

pub enum ArmedSsdTarget {
    Wayland(Rc<RefCell<Window>>),
    X11(WlSurface),
}

pub struct ArmedSsdButton {
    pub slot: TouchSlot,
    pub target: ArmedSsdTarget,
    pub action: SsdAction,
}

pub struct TouchState {
    pub user_gestures: Vec<GestureBinding>,

    pub(super) last_tap: Option<(Instant, Point<f64, Logical>)>,
    pub(super) pending_single_tap: Option<RegistrationToken>,
    pub(super) event_loop: LoopHandle<'static, Forge>,
    pub(super) velocity_timer: Option<RegistrationToken>,
    pub(super) repeat_timer: Option<RegistrationToken>,
    pub(super) input_surface: Option<InputSurface>,
    pub(super) tap_surface: Option<InputSurface>,
    pub(super) active_app_id: Option<String>,
    pub(super) velocity: Point<f64, Logical>,
    pub(super) position: Point<f64, Logical>,
    pub(super) events: Vec<TouchEvent>,
    pub(super) slot: Option<TouchSlot>,
    pub(super) start: TouchStart,
    pub(super) is_drag: bool,
}

impl TouchState {
    pub fn new(event_loop: LoopHandle<'static, Forge>) -> Self {
        Self {
            event_loop,
            pending_single_tap: None,
            velocity_timer: None,
            input_surface: None,
            user_gestures: Vec::new(),
            active_app_id: None,
            repeat_timer: None,
            tap_surface: None,
            last_tap: None,
            position: Point::default(),
            velocity: Point::default(),
            is_drag: false,
            events: Vec::new(),
            start: TouchStart::default(),
            slot: None,
        }
    }

    pub fn cancel_velocity(&mut self) {
        if let Some(token) = self.velocity_timer.take() {
            self.event_loop.remove(token);
        }
        self.velocity = Point::default();
    }

    pub fn start_key_repeat<F: FnMut() + 'static>(&mut self, mut callback: F) {
        let timer = Timer::from_duration(REPEAT_DELAY);
        let token = self
            .event_loop
            .insert_source(timer, move |_, _, _| {
                callback();
                TimeoutAction::ToDuration(REPEAT_RATE)
            })
            .expect("key repeat timer");
        self.repeat_timer = Some(token);
    }

    pub fn cancel_key_repeat(&mut self) {
        if let Some(token) = self.repeat_timer.take() {
            self.event_loop.remove(token);
        }
    }

    pub(super) fn start(&mut self, canvas: &Canvas, slot: TouchSlot, position: Point<f64, Logical>) {
        if self.slot.take().is_some() {
            self.last_tap = None;
            return;
        }
        self.slot = Some(slot);
        self.cancel_velocity();
        self.start = TouchStart::new(canvas, position);
        self.position = position;
        self.is_drag = false;
    }

    pub(super) fn has_velocity(&self) -> bool {
        self.velocity.x.abs() >= f64::EPSILON || self.velocity.y.abs() >= f64::EPSILON
    }

    #[inline]
    pub fn touching(&self) -> bool {
        self.slot.is_some()
    }

    pub fn position(&self) -> Option<Point<f64, Logical>> {
        self.slot.map(|_| self.position)
    }

    pub fn cursor_pos(&self) -> Point<f64, Logical> {
        self.position
    }

    /// Confine `pos` to the union of the screens' regions. Inside any rect
    /// it stands as-is; otherwise each axis of the move is tried alone so
    /// the pointer slides along a shared edge instead of sticking at the
    /// first screen's corner; fully outside, it clamps to the nearest point
    /// of the nearest rect.
    pub(super) fn clamp_point_impl(
        pos: Point<f64, Logical>,
        prev: Point<f64, Logical>,
        rects: &[Rectangle<f64, Logical>],
    ) -> Point<f64, Logical> {
        let inside = |p: Point<f64, Logical>| {
            rects.iter().any(|r| {
                p.x >= r.loc.x
                    && p.x <= r.loc.x + r.size.w
                    && p.y >= r.loc.y
                    && p.y <= r.loc.y + r.size.h
            })
        };
        if rects.is_empty() || inside(pos) {
            return pos;
        }
        let x_only: Point<f64, Logical> = (pos.x, prev.y).into();
        if inside(x_only) {
            return x_only;
        }
        // A horizontal push into a screen whose vertical range does not
        // reach the pointer (different scales leave neighbouring screens
        // with different logical heights) crosses anyway, entering at that
        // screen's nearer edge — instead of sliding forever along the
        // shared border.
        let y_dist = |r: &Rectangle<f64, Logical>| {
            if pos.y < r.loc.y {
                r.loc.y - pos.y
            } else if pos.y > r.loc.y + r.size.h {
                pos.y - (r.loc.y + r.size.h)
            } else {
                0.0
            }
        };
        if let Some(r) = rects
            .iter()
            .filter(|r| pos.x >= r.loc.x && pos.x <= r.loc.x + r.size.w)
            .min_by(|a, b| y_dist(a).total_cmp(&y_dist(b)))
        {
            return (pos.x, pos.y.clamp(r.loc.y, r.loc.y + r.size.h)).into();
        }
        let y_only: Point<f64, Logical> = (prev.x, pos.y).into();
        if inside(y_only) {
            return y_only;
        }
        rects
            .iter()
            .map(|r| {
                Point::<f64, Logical>::from((
                    pos.x.clamp(r.loc.x, r.loc.x + r.size.w),
                    pos.y.clamp(r.loc.y, r.loc.y + r.size.h),
                ))
            })
            .min_by(|a, b| {
                let da = (a.x - pos.x).powi(2) + (a.y - pos.y).powi(2);
                let db = (b.x - pos.x).powi(2) + (b.y - pos.y).powi(2);
                da.total_cmp(&db)
            })
            .unwrap_or(prev)
    }

    /// Pull the pointer back inside the union of the screens' regions when
    /// it shrank or moved under it — a scale change, a mode change, a
    /// hotplug. Motion clamps against the union it reads at the time; this
    /// covers the union moving instead.
    pub fn clamp_to_rects(&mut self, rects: &[Rectangle<f64, Logical>]) {
        self.position = Self::clamp_point_impl(self.position, self.position, rects);
    }

    pub(super) fn action(&mut self, canvas: &Canvas) -> Option<TouchAction> {
        if self.start.is_handle_gesture {
            match self.start.handle_direction {
                Some(HandleDirection::Horizontal) => return Some(HandleGesture::Horizontal.into()),
                Some(HandleDirection::Vertical) => {
                    return Some(HandleGesture::Vertical(self.position.y).into())
                },
                None => (),
            }

            if let Some(gesture) =
                HandleGesture::from_points(canvas, self.start.position, self.position)
            {
                self.start.handle_direction = Some(HandleDirection::from(&gesture));
                return Some(gesture.into());
            }
        }

        if !self.touching() && self.input_surface.is_none() {
            let app_id = self.active_app_id.as_ref();
            let mut gestures =
                self.matching_gestures(canvas, app_id, self.start.position, Some(self.position));

            if let Some(gesture) = gestures.next() {
                return Some(TouchAction::UserGesture(gesture.action.clone()));
            }
        }

        let delta = self.start.position - self.position;
        let dist_sq = delta.x.powi(2) + delta.y.powi(2);

        if self.is_drag || dist_sq > TAP_DISTANCE_SQ || self.start.time.elapsed() >= HOLD_DURATION {
            self.is_drag = true;
            return Some(TouchAction::Drag);
        }

        if self.touching() {
            return None;
        }

        if let Some((tap_time, tap_pos)) = self.last_tap.take() {
            let tap_delta = tap_pos - self.position;
            let tap_dist_sq = tap_delta.x.powi(2) + tap_delta.y.powi(2);

            if tap_time.elapsed() <= DOUBLE_TAP_TIMEOUT && tap_dist_sq <= DOUBLE_TAP_DISTANCE_SQ {
                return Some(TouchAction::DoubleTap);
            }
        }

        Some(TouchAction::Tap)
    }

    pub(super) fn matching_gestures<'a>(
        &'a self,
        canvas: &'a Canvas,
        app_id: Option<&'a String>,
        start: Point<f64, Logical>,
        end: Option<Point<f64, Logical>>,
    ) -> impl Iterator<Item = &'a GestureBinding> {
        let size = canvas.size().to_f64();
        let start_sector = GestureSector::from_point(size, start);
        let end_sector = end.map(|e| GestureSector::from_point(size, e));

        self.user_gestures.iter().filter(move |g| {
            g.start == start_sector
                && end_sector.is_none_or(|s| g.end == s)
                && g.app_id.matches(app_id)
        })
    }
}

#[derive(Debug)]
pub(super) struct TouchStart {
    pub(super) handle_direction: Option<HandleDirection>,
    pub(super) position: Point<f64, Logical>,
    pub(super) is_handle_gesture: bool,
    pub(super) is_top_gesture: bool,
    pub(super) time: Instant,
}

impl Default for TouchStart {
    fn default() -> Self {
        Self {
            time: Instant::now(),
            is_handle_gesture: false,
            is_top_gesture: false,
            handle_direction: None,
            position: Point::default(),
        }
    }
}

impl TouchStart {
    pub(super) fn new(canvas: &Canvas, position: Point<f64, Logical>) -> Self {
        Self {
            position,
            is_handle_gesture: HandleGesture::is_start(canvas, position),
            is_top_gesture: is_top_area(canvas, position),
            // DEBUG
            time: Instant::now(),
            handle_direction: None,
        }
    }
}

pub(super) fn is_top_area(canvas: &Canvas, position: Point<f64, Logical>) -> bool {
    // Cap the band at 12% of the output so it stays a top-edge affordance
    // (not 1/6 of an 800x600 screen) while keeping 100 logical px elsewhere.
    let band = TOP_GESTURE_HEIGHT.min(canvas.size().h as f64 * 0.12);
    position.y < band
}

#[derive(Debug)]
pub(super) enum TouchAction {
    HandleGesture(HandleGesture),
    UserGesture(GestureAction),
    Drag,
    Tap,
    DoubleTap,
}

impl From<HandleGesture> for TouchAction {
    fn from(g: HandleGesture) -> Self {
        Self::HandleGesture(g)
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HandleGesture {
    Vertical(f64),
    Horizontal,
}

impl HandleGesture {
    pub(super) fn from_points(
        canvas: &Canvas,
        start: Point<f64, Logical>,
        end: Point<f64, Logical>,
    ) -> Option<Self> {
        let delta = end - start;
        let len_sq = delta.x.powi(2) + delta.y.powi(2);

        if len_sq < TAP_DISTANCE_SQ {
            return None;
        }

        if delta.y.abs() >= delta.x.abs() {
            Some(Self::Vertical(end.y))
        } else if Self::is_start(canvas, end) {
            Some(Self::Horizontal)
        } else {
            None
        }
    }

    pub(super) fn is_start(canvas: &Canvas, position: Point<f64, Logical>) -> bool {
        let size = canvas.size().to_f64();
        let handle_rect = Rectangle::new((0.0, size.h - GESTURE_HANDLE_HEIGHT as f64).into(), size);
        handle_rect.contains(position)
    }
}

#[derive(Debug)]
pub(super) enum HandleDirection {
    Horizontal,
    Vertical,
}

impl From<&HandleGesture> for HandleDirection {
    fn from(g: &HandleGesture) -> Self {
        match g {
            HandleGesture::Horizontal => Self::Horizontal,
            HandleGesture::Vertical(_) => Self::Vertical,
        }
    }
}

#[derive(Copy, Clone, Debug)]
pub(super) struct TouchEvent {
    pub(super) position: Point<f64, Logical>,
    pub(super) ty: TouchEventType,
    pub(super) slot: TouchSlot,
    pub(super) time: u32,
    /// When true, this event was synthesized from a pointer click.
    /// Compositor gesture logic runs normally, but no wl_touch events
    /// are forwarded to the client (the client already gets wl_pointer).
    pub(super) from_pointer: bool,
}

impl TouchEvent {
    pub(super) fn new(ty: TouchEventType, slot: TouchSlot, time: u32, position: Point<f64, Logical>) -> Self {
        Self { slot, time, position, ty, from_pointer: false }
    }

    pub(super) fn from_pointer(ty: TouchEventType, slot: TouchSlot, time: u32, position: Point<f64, Logical>) -> Self {
        Self { slot, time, position, ty, from_pointer: true }
    }
}

#[derive(Copy, Clone, Debug)]
pub(super) enum TouchEventType {
    Down,
    Up,
    Motion,
}


