use std::fmt::Display;

const L1_BITS: u32 = 16;
const L2_BITS: u32 = 9;
const L3_BITS: u32 = 5;
const VARIABLE_BITS: u32 = 1;

#[derive(Clone, Copy, Default)]
pub(crate) struct Elements {
    pub(crate) buf: &'static [u64],
    pub(crate) cur: u64,
}

impl Elements {
    pub(crate) fn is_empty(&self) -> bool {
        self.cur == 0 && self.buf.is_empty()
    }
}

impl Iterator for Elements {
    type Item = Element;

    #[inline(always)]
    fn next(&mut self) -> Option<Self::Item> {
        if self.cur == 0 {
            if self.buf.is_empty() {
                return None;
            }

            self.cur = self.buf[0];
            self.buf = &self.buf[1..];
        }

        let element = Element(((self.cur >> 1) & 0x7FFFFFFF) as u32);

        self.cur >>= 32;

        Some(element)
    }
}

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

impl Element {
    // l1       = 16 bits
    // l2       =  9 bits
    // l3       =  5 bits
    // l4       =  0 bits
    // variable =  1 bit
    //
    // total    = 31 bits
    pub(crate) fn new(l1: u16, l2: u16, l3: u8, l4: u16, variable: bool) -> Self {
        debug_assert!(u16::BITS - l1.leading_zeros() <= L1_BITS);
        debug_assert!(u16::BITS - l2.leading_zeros() <= L2_BITS);
        debug_assert!(u8::BITS - l3.leading_zeros() <= L3_BITS);
        debug_assert!(l4 == 0);

        Self(
            variable as u32
                | (l1 as u32) << VARIABLE_BITS
                | (l2 as u32) << (VARIABLE_BITS + L1_BITS)
                | (l3 as u32) << (VARIABLE_BITS + L1_BITS + L2_BITS),
        )
    }

    #[inline(always)]
    pub(crate) fn l1(&self) -> u16 {
        ((self.0 >> VARIABLE_BITS) & 0xFFFF) as _
    }

    #[inline(always)]
    pub(crate) fn l2(&self) -> u16 {
        ((self.0 >> (VARIABLE_BITS + L1_BITS)) & 0x1FF) as _
    }

    #[inline(always)]
    pub(crate) fn l3(&self) -> u8 {
        ((self.0 >> (VARIABLE_BITS + L1_BITS + L2_BITS)) & 0x1F) as _
    }

    #[inline(always)]
    pub(crate) fn l4(&self) -> u16 {
        0
    }

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

    pub(crate) fn as_u32(&self) -> u32 {
        self.0
    }
}

impl Display for Element {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}{:04X}.{:04X}.{:04X}.{:04X}",
            if self.is_variable() { "*" } else { "." },
            self.l1(),
            self.l2(),
            self.l3(),
            self.l4()
        )
    }
}

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

impl Range {
    pub(crate) fn new(start: u32, len: u8) -> Self {
        Self((len as u64) | (start as u64) << 8)
    }

    #[inline(always)]
    pub(crate) fn start(&self) -> usize {
        ((self.0 >> 8) & 0xFFFFFFFF) as usize
    }

    #[inline(always)]
    pub(crate) fn end(&self) -> usize {
        self.start() + (self.0 & 0xFF) as usize
    }

    pub(crate) fn as_u64(&self) -> u64 {
        self.0
    }
}

pub(crate) fn pack(elements: &[Element]) -> u64 {
    assert!(elements.len() <= 2);

    let mut data = 0;

    for e in elements.iter().rev() {
        if data != 0 {
            data <<= 32;
        }

        data |= (e.as_u32() as u64) << 1 | 1;
    }

    data
}

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

    use super::*;

    #[test]
    fn test_elements() {
        let a = Element::new(0x4593, 0x0020, 0x0016, 0x0000, false);
        let b = Element::new(0x0000, 0x0119, 0x0016, 0x0000, false);

        let e = Elements {
            buf: &[],
            cur: pack(&[a, b]),
        };

        assert_eq!(e.collect::<Vec<_>>(), vec![a, b]);
    }

    proptest! {
        #[test]
        fn proptest_element(l1 in any::<u16>(), l2 in (0u16..256), l3 in (0u8..32), variable in any::<bool>()) {
            let r = Element::new(l1, l2, l3, 0, variable);

            prop_assert_eq!(r.l1(), l1);
            prop_assert_eq!(r.l2(), l2);
            prop_assert_eq!(r.l3(), l3);
            prop_assert_eq!(r.l4(), 0);
            prop_assert_eq!(r.is_variable(), variable);
        }

        #[test]
        fn proptest_range(start in any::<u32>(), len in any::<u8>()) {
            let r = Range::new(start, len);

            prop_assert_eq!(r.start(), start as usize);
            prop_assert_eq!(r.end(), start as usize + len as usize);
        }
    }
}
