use serde::{Deserialize, Serialize};

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum LayoutPxError {
    #[error("LayoutPx must be finite")]
    NotFinite,
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "f32", into = "f32")]
pub struct LayoutPx(f32);

impl LayoutPx {
    pub const ZERO: Self = Self(0.0);

    #[must_use]
    pub const fn new(value: f32) -> Self {
        assert!(value.is_finite(), "LayoutPx must be finite");
        Self(value)
    }

    #[must_use]
    pub fn saturating(value: f32) -> Self {
        if value.is_finite() {
            Self(value)
        } else {
            Self::ZERO
        }
    }

    #[must_use]
    pub fn saturating_nonneg(value: f32) -> Self {
        if value.is_finite() {
            Self(value.max(0.0))
        } else {
            Self::ZERO
        }
    }

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

    #[must_use]
    pub fn min(self, other: Self) -> Self {
        if self.0 <= other.0 { self } else { other }
    }

    #[must_use]
    pub fn max(self, other: Self) -> Self {
        if self.0 >= other.0 { self } else { other }
    }
}

impl Default for LayoutPx {
    fn default() -> Self {
        Self::ZERO
    }
}

impl TryFrom<f32> for LayoutPx {
    type Error = LayoutPxError;

    fn try_from(value: f32) -> Result<Self, Self::Error> {
        if value.is_finite() {
            Ok(Self(value))
        } else {
            Err(LayoutPxError::NotFinite)
        }
    }
}

impl From<LayoutPx> for f32 {
    fn from(value: LayoutPx) -> Self {
        value.0
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LayoutPos {
    pub x: LayoutPx,
    pub y: LayoutPx,
}

impl LayoutPos {
    pub const ORIGIN: Self = Self {
        x: LayoutPx::ZERO,
        y: LayoutPx::ZERO,
    };

    #[must_use]
    pub const fn new(x: LayoutPx, y: LayoutPx) -> Self {
        Self { x, y }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LayoutSize {
    pub width: LayoutPx,
    pub height: LayoutPx,
}

impl LayoutSize {
    pub const ZERO: Self = Self {
        width: LayoutPx::ZERO,
        height: LayoutPx::ZERO,
    };

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

#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct LayoutOffset {
    pub dx: LayoutPx,
    pub dy: LayoutPx,
}

impl LayoutOffset {
    pub const ZERO: Self = Self {
        dx: LayoutPx::ZERO,
        dy: LayoutPx::ZERO,
    };

    #[must_use]
    pub const fn new(dx: LayoutPx, dy: LayoutPx) -> Self {
        Self { dx, dy }
    }

    #[must_use]
    pub fn between(from: LayoutPos, to: LayoutPos) -> Self {
        Self {
            dx: LayoutPx::saturating(to.x.value() - from.x.value()),
            dy: LayoutPx::saturating(to.y.value() - from.y.value()),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LayoutRect {
    pub origin: LayoutPos,
    pub size: LayoutSize,
}

impl LayoutRect {
    #[must_use]
    pub const fn new(origin: LayoutPos, size: LayoutSize) -> Self {
        Self { origin, size }
    }

    #[must_use]
    pub fn min_x(self) -> LayoutPx {
        self.origin.x
    }

    #[must_use]
    pub fn min_y(self) -> LayoutPx {
        self.origin.y
    }

    #[must_use]
    pub fn max_x(self) -> LayoutPx {
        LayoutPx::saturating(self.origin.x.value() + self.size.width.value())
    }

    #[must_use]
    pub fn max_y(self) -> LayoutPx {
        LayoutPx::saturating(self.origin.y.value() + self.size.height.value())
    }

    #[must_use]
    pub fn contains(self, pos: LayoutPos) -> bool {
        let x = pos.x.value();
        let y = pos.y.value();
        x >= self.min_x().value()
            && x < self.max_x().value()
            && y >= self.min_y().value()
            && y < self.max_y().value()
    }

    #[must_use]
    pub fn union(self, other: Self) -> Self {
        let min_x = self.min_x().min(other.min_x());
        let min_y = self.min_y().min(other.min_y());
        let max_x = self.max_x().max(other.max_x());
        let max_y = self.max_y().max(other.max_y());
        Self::new(
            LayoutPos::new(min_x, min_y),
            LayoutSize::new(
                LayoutPx::saturating_nonneg(max_x.value() - min_x.value()),
                LayoutPx::saturating_nonneg(max_y.value() - min_y.value()),
            ),
        )
    }

    #[must_use]
    pub fn mirror_horizontally_within(
        self,
        container: Self,
        direction: super::axis::LayoutDirection,
    ) -> Self {
        match direction {
            super::axis::LayoutDirection::Ltr => self,
            super::axis::LayoutDirection::Rtl => {
                let container_left = container.origin.x.value();
                let container_right = container_left + container.size.width.value();
                let mirrored_x = container_right
                    - (self.origin.x.value() - container_left)
                    - self.size.width.value();
                Self::new(
                    LayoutPos::new(LayoutPx::new(mirrored_x), self.origin.y),
                    self.size,
                )
            }
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EdgeInsets {
    pub start: crate::theme::Spacing,
    pub end: crate::theme::Spacing,
    pub top: crate::theme::Spacing,
    pub bottom: crate::theme::Spacing,
}

impl EdgeInsets {
    #[must_use]
    pub const fn all(value: crate::theme::Spacing) -> Self {
        Self {
            start: value,
            end: value,
            top: value,
            bottom: value,
        }
    }

    #[must_use]
    pub fn physical(
        self,
        direction: super::axis::LayoutDirection,
    ) -> (crate::theme::Spacing, crate::theme::Spacing) {
        match direction {
            super::axis::LayoutDirection::Ltr => (self.start, self.end),
            super::axis::LayoutDirection::Rtl => (self.end, self.start),
        }
    }
}
