use core::num::NonZeroU64;

use serde::{Deserialize, Serialize};

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

const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;

impl WidgetId {
    pub const ROOT: Self = match NonZeroU64::new(0xB0FE_B0FE_B0FE_B0FE) {
        Some(n) => Self(n),
        None => panic!("ROOT seed must be non-zero"),
    };

    #[must_use]
    pub const fn from_raw(raw: NonZeroU64) -> Self {
        Self(raw)
    }

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

    #[must_use]
    pub fn child(self, key: WidgetKey) -> Self {
        self.mix(key.as_str(), 0)
    }

    #[must_use]
    pub fn child_indexed(self, key: WidgetKey, index: u64) -> Self {
        self.mix(key.as_str(), index)
    }

    fn mix(self, key: &str, index: u64) -> Self {
        let parent = self.0.get().to_le_bytes();
        let suffix = index.to_le_bytes();
        let raw = parent
            .iter()
            .chain(key.as_bytes().iter())
            .chain(suffix.iter())
            .fold(FNV_OFFSET, |h, b| {
                (h ^ u64::from(*b)).wrapping_mul(FNV_PRIME)
            })
            | 1;
        match NonZeroU64::new(raw) {
            Some(n) => Self(n),
            None => panic!("`| 1` forces non-zero"),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WidgetKey(&'static str);

impl WidgetKey {
    #[must_use]
    pub const fn new(s: &'static str) -> Self {
        Self(s)
    }

    #[must_use]
    pub const fn as_str(self) -> &'static str {
        self.0
    }
}

#[cfg(test)]
mod tests {
    use super::{WidgetId, WidgetKey};

    const PANEL: WidgetKey = WidgetKey::new("panel");
    const ITEM: WidgetKey = WidgetKey::new("item");

    #[test]
    fn root_is_stable() {
        assert_eq!(WidgetId::ROOT, WidgetId::ROOT);
    }

    #[test]
    fn child_is_deterministic() {
        let a = WidgetId::ROOT.child(PANEL);
        let b = WidgetId::ROOT.child(PANEL);
        assert_eq!(a, b);
    }

    #[test]
    fn child_distinguishes_keys() {
        let a = WidgetId::ROOT.child(PANEL);
        let b = WidgetId::ROOT.child(ITEM);
        assert_ne!(a, b);
    }

    #[test]
    fn child_indexed_distinguishes_indices() {
        let a = WidgetId::ROOT.child_indexed(ITEM, 0);
        let b = WidgetId::ROOT.child_indexed(ITEM, 1);
        assert_ne!(a, b);
    }

    #[test]
    fn child_path_depends_on_parent() {
        let a = WidgetId::ROOT.child(PANEL).child(ITEM);
        let b = WidgetId::ROOT.child(ITEM);
        assert_ne!(a, b);
    }

    #[test]
    fn child_zero_indexed_matches_child_unindexed() {
        let a = WidgetId::ROOT.child(PANEL);
        let b = WidgetId::ROOT.child_indexed(PANEL, 0);
        assert_eq!(a, b);
    }

    #[test]
    fn child_value_is_pinned_to_fnv() {
        let id = WidgetId::ROOT.child(PANEL);
        let raw = id.raw().get();
        assert_eq!(raw & 1, 1, "low bit must be forced on");
        assert_eq!(raw, 0x185e_529f_9def_b5c5, "FNV-1a hash must stay frozen");
        let other = WidgetId::ROOT.child(ITEM).raw().get();
        assert_ne!(raw, other);
    }
}
