use core::time::Duration;

use serde::{Deserialize, Serialize};

use crate::layout::{LayoutOffset, LayoutPos, LayoutPx};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum PointerButton {
    Primary,
    Secondary,
    Middle,
}

impl PointerButton {
    pub const ALL: [Self; 3] = [Self::Primary, Self::Secondary, Self::Middle];

    #[must_use]
    pub const fn bit(self) -> u8 {
        match self {
            Self::Primary => 1 << 0,
            Self::Secondary => 1 << 1,
            Self::Middle => 1 << 2,
        }
    }
}

#[derive(
    Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct PointerButtonMask(u8);

impl PointerButtonMask {
    pub const EMPTY: Self = Self(0);

    #[must_use]
    pub const fn just(button: PointerButton) -> Self {
        Self(button.bit())
    }

    #[must_use]
    pub const fn contains(self, button: PointerButton) -> bool {
        (self.0 & button.bit()) != 0
    }

    #[must_use]
    pub const fn is_empty(self) -> bool {
        self.0 == 0
    }

    #[must_use]
    pub const fn with(self, button: PointerButton) -> Self {
        Self(self.0 | button.bit())
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FrameInstant(Duration);

impl FrameInstant {
    pub const ZERO: Self = Self(Duration::ZERO);

    #[must_use]
    pub const fn from_duration(d: Duration) -> Self {
        Self(d)
    }

    #[must_use]
    pub const fn duration(self) -> Duration {
        self.0
    }

    #[must_use]
    pub fn since(self, earlier: Self) -> Duration {
        self.0.saturating_sub(earlier.0)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DragThreshold(LayoutPx);

impl DragThreshold {
    pub const DEFAULT: Self = Self(LayoutPx::new(4.0));

    #[must_use]
    pub const fn new(px: LayoutPx) -> Self {
        assert!(px.value() >= 0.0, "DragThreshold must be non-negative");
        Self(px)
    }

    #[must_use]
    pub const fn px(self) -> LayoutPx {
        self.0
    }

    #[must_use]
    pub fn exceeded_by(self, offset: LayoutOffset) -> bool {
        let dx = offset.dx.value();
        let dy = offset.dy.value();
        let t = self.0.value();
        dx * dx + dy * dy > t * t
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DoubleClickWindow(Duration);

impl DoubleClickWindow {
    pub const DEFAULT: Self = Self(Duration::from_millis(400));

    #[must_use]
    pub const fn new(window: Duration) -> Self {
        Self(window)
    }

    #[must_use]
    pub const fn duration(self) -> Duration {
        self.0
    }

    #[must_use]
    pub fn contains(self, gap: Duration) -> bool {
        gap <= self.0
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ClickCount(u8);

impl ClickCount {
    pub const SINGLE: Self = Self(1);
    pub const DOUBLE: Self = Self(2);

    #[must_use]
    pub const fn new(count: u8) -> Self {
        Self(count)
    }

    #[must_use]
    pub const fn get(self) -> u8 {
        self.0
    }

    #[must_use]
    pub const fn next(self) -> Self {
        Self(self.0.saturating_add(1))
    }

    #[must_use]
    pub const fn is_double(self) -> bool {
        self.0 >= 2
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PointerSample {
    pub position: LayoutPos,
}

impl PointerSample {
    #[must_use]
    pub const fn new(position: LayoutPos) -> Self {
        Self { position }
    }
}

#[cfg(test)]
mod tests {
    use core::time::Duration;

    use super::{
        ClickCount, DoubleClickWindow, DragThreshold, FrameInstant, PointerButton,
        PointerButtonMask,
    };
    use crate::layout::{LayoutOffset, LayoutPx};

    #[test]
    fn button_mask_with_just() {
        let mask = PointerButtonMask::just(PointerButton::Primary).with(PointerButton::Middle);
        assert!(mask.contains(PointerButton::Primary));
        assert!(mask.contains(PointerButton::Middle));
        assert!(!mask.contains(PointerButton::Secondary));
    }

    #[test]
    fn drag_threshold_uses_squared_distance() {
        let t = DragThreshold::new(LayoutPx::new(5.0));
        let inside = LayoutOffset::new(LayoutPx::new(3.0), LayoutPx::new(3.0));
        let outside = LayoutOffset::new(LayoutPx::new(4.0), LayoutPx::new(4.0));
        assert!(!t.exceeded_by(inside));
        assert!(t.exceeded_by(outside));
    }

    #[test]
    fn double_click_window_contains_short_gap() {
        let w = DoubleClickWindow::DEFAULT;
        assert!(w.contains(Duration::from_millis(200)));
        assert!(!w.contains(Duration::from_millis(800)));
    }

    #[test]
    fn click_count_progresses() {
        let one = ClickCount::SINGLE;
        let two = one.next();
        assert!(!one.is_double());
        assert!(two.is_double());
    }

    #[test]
    fn click_count_triple_remains_double_flag() {
        let three = ClickCount::SINGLE.next().next();
        assert_eq!(three.get(), 3);
        assert!(three.is_double());
    }

    #[test]
    fn frame_instant_since_clamps() {
        let later = FrameInstant::from_duration(Duration::from_millis(100));
        let earlier = FrameInstant::from_duration(Duration::from_millis(40));
        assert_eq!(later.since(earlier), Duration::from_millis(60));
        assert_eq!(earlier.since(later), Duration::ZERO);
    }
}
