pub use bone_text::{FontFace, FontWeight};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use uom::si::f64::Length;
use uom::si::length::{meter, millimeter};

const PX_TO_MM: f64 = 25.4 / 96.0;
const MM_TO_PX: f64 = 96.0 / 25.4;

fn px_to_length(px: f64) -> Length {
    Length::new::<millimeter>(px * PX_TO_MM)
}

fn length_to_px(length: Length) -> f64 {
    length.get::<millimeter>() * MM_TO_PX
}

fn length_to_meters(length: Length) -> f64 {
    length.get::<meter>()
}

fn length_from_meters(meters: f64) -> Length {
    Length::new::<meter>(meters)
}

macro_rules! length_newtype {
    ($name:ident) => {
        #[derive(Copy, Clone, Debug, PartialEq)]
        pub struct $name(Length);

        impl $name {
            #[must_use]
            pub fn from_px(px: f64) -> Self {
                Self(px_to_length(px))
            }

            #[must_use]
            pub fn as_px(self) -> f64 {
                length_to_px(self.0)
            }

            #[must_use]
            #[allow(
                clippy::cast_possible_truncation,
                reason = "px sizes fit f32 mantissa at typography scales"
            )]
            pub fn as_px_f32(self) -> f32 {
                length_to_px(self.0) as f32
            }

            #[must_use]
            pub fn length(self) -> Length {
                self.0
            }
        }

        impl core::fmt::Display for $name {
            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
                write!(f, "{}px", self.as_px())
            }
        }

        impl Serialize for $name {
            fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
                s.serialize_f64(length_to_meters(self.0))
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
                f64::deserialize(d).map(|m| Self(length_from_meters(m)))
            }
        }
    };
}

length_newtype!(FontSize);
length_newtype!(LineHeight);
length_newtype!(LetterSpacing);

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TypographyRole {
    pub face: FontFace,
    pub size: FontSize,
    pub line_height: LineHeight,
    pub weight: FontWeight,
    pub letter_spacing: LetterSpacing,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Typography {
    pub caption: TypographyRole,
    pub body: TypographyRole,
    pub label: TypographyRole,
    pub title: TypographyRole,
    pub heading: TypographyRole,
    pub mono: TypographyRole,
}

impl Typography {
    #[must_use]
    pub fn standard() -> Self {
        Self {
            caption: role(FontFace::Sans, 11.0, 14.0, FontWeight::Regular),
            body: role(FontFace::Sans, 13.0, 18.0, FontWeight::Regular),
            label: role(FontFace::Sans, 12.0, 16.0, FontWeight::Medium),
            title: role(FontFace::Sans, 14.0, 20.0, FontWeight::Semibold),
            heading: role(FontFace::Sans, 18.0, 24.0, FontWeight::Semibold),
            mono: role(FontFace::Mono, 12.0, 16.0, FontWeight::Regular),
        }
    }
}

fn role(face: FontFace, size_px: f64, line_height_px: f64, weight: FontWeight) -> TypographyRole {
    TypographyRole {
        face,
        size: FontSize::from_px(size_px),
        line_height: LineHeight::from_px(line_height_px),
        weight,
        letter_spacing: LetterSpacing::from_px(0.0),
    }
}
