use u_fst::raw::Fst;

include!(concat!(env!("OUT_DIR"), "/class_map.rs"));

const TABLE: Fst<&'static [u8]> =
    Fst::new_unchecked(include_bytes!(concat!(env!("OUT_DIR"), "/table.fst")));

#[inline(always)]
pub fn lookup(ch: char) -> Entry {
    Entry::new(
        TABLE
            .get((ch as u32).to_ne_bytes())
            .map(|output| output.value())
            .unwrap_or(0),
    )
}

#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Entry(u64);

impl Entry {
    pub(crate) fn new(data: u64) -> Self {
        Self(data)
    }

    #[inline(always)]
    pub fn combining_class(&self) -> u8 {
        CLASS_MAP[(self.0 & 0x3F) as usize]
    }

    #[inline(always)]
    pub fn decomposition(&self) -> Option<Decompositions> {
        let data = self.0 >> 6;

        if data > 0 {
            Some(Decompositions(data))
        } else {
            None
        }
    }
}

#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Decompositions(u64);

impl Iterator for Decompositions {
    type Item = Decomposition;

    #[inline(always)]
    fn next(&mut self) -> Option<Self::Item> {
        let d = (self.0 & 0xFFFFFFF) as u32;

        if d > 0 {
            self.0 >>= 28;

            Some(Decomposition(d))
        } else {
            None
        }
    }
}

#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Decomposition(u32);

impl Decomposition {
    #[inline(always)]
    pub fn has_decompositions(&self) -> bool {
        (self.0 & 0x1) == 1
    }

    #[inline(always)]
    pub fn combining_class(&self) -> u8 {
        CLASS_MAP[((self.0 >> 1) & 0x3F) as usize]
    }

    #[inline(always)]
    pub fn char(&self) -> char {
        unsafe { char::from_u32_unchecked(((self.0 >> 7) & 0x1FFFFF) as u32) }
    }
}

#[cfg(test)]
mod tests {
    use proptest::prelude::*;

    use super::*;

    fn entry_strategy() -> impl Strategy<Value = (u64, (u8, u8, bool, u8, char, bool, u8, char))> {
        (
            (0u8..CLASS_MAP.len() as u8),
            (0u8..2),
            any::<bool>(),
            (0u8..CLASS_MAP.len() as u8),
            any::<char>().prop_filter("", |c| *c != '\u{0}'),
            any::<bool>(),
            (0u8..CLASS_MAP.len() as u8),
            any::<char>().prop_filter("", |c| *c != '\u{0}'),
        )
            .prop_map(
                |(
                    combining_class,
                    mapping_count,
                    d1_has_mapping,
                    d1_class,
                    d1_char,
                    d2_has_mapping,
                    d2_class,
                    d2_char,
                )| {
                    let mut entry = combining_class as u64;

                    if mapping_count > 0 {
                        entry |= (d1_char as u64) << ((6 + 1) + 6)
                            | (d1_class as u64) << ((1) + 6)
                            | (d1_has_mapping as u64) << (6);
                    }

                    if mapping_count > 1 {
                        entry |= (d1_char as u64) << ((6 + 1) + (21 + 6))
                            | (d1_class as u64) << ((1) + (21 + 6))
                            | (d1_has_mapping as u64) << (21 + 6);
                    }

                    (
                        entry,
                        (
                            combining_class,
                            mapping_count,
                            d1_has_mapping,
                            d1_class,
                            d1_char,
                            d2_has_mapping,
                            d2_class,
                            d2_char,
                        ),
                    )
                },
            )
    }

    proptest! {
        #[test]
        fn proptest_entry_serialize_and_deserialize(a in entry_strategy()) {
            let (data, (combining_class,
                mapping_count,
                d1_has_mapping,
                d1_class,
                d1_char,
                d2_has_mapping,
                d2_class,
                d2_char)) = a;

            let b = Entry::new(data);

            prop_assert_eq!(b.combining_class(), CLASS_MAP[combining_class as usize], "data = {:064b}", data);

            let c = b.decomposition().map(|i| i.map(|d| (d.has_decompositions(), d.combining_class(), d.char())).collect::<Vec<_>>());

            match mapping_count {
                0 => prop_assert_eq!(c, None, "data = {:064b}", data),
                1 => prop_assert_eq!(c, Some(vec![(d1_has_mapping, CLASS_MAP[d1_class as usize], d1_char)]), "data = {:064b}", data),
                2 => prop_assert_eq!(c, Some(vec![(d1_has_mapping, CLASS_MAP[d1_class as usize], d1_char), (d2_has_mapping, CLASS_MAP[d2_class as usize], d2_char)]), "data = {:064b}", data),
                _ => unreachable!(),
            }
        }
    }
}
