//! The FIT CRC-16.
//!
//! The FIT specification defines a nibble-table CRC. It is bit-for-bit
//! CRC-16/ARC (reflected polynomial `0xA001`, init `0x0000`, no final XOR),
//! which gives us published test vectors to check against — see the tests.
//!
//! Two CRCs appear in every FIT file and both must be right or the file is
//! silently rejected on upload:
//!
//! * the *header CRC* — bytes 0..12 of a 14-byte header, stored at bytes 12..14;
//! * the *file CRC* — every byte from the start of the header through the end
//!   of the data records, appended as the last two bytes of the file.

/// Nibble lookup table from the FIT SDK.
const CRC_TABLE: [u16; 16] = [
    0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401,
    0x5000, 0x9C01, 0x8801, 0x4400,
];

/// Running FIT CRC-16 state.
///
/// Lets the encoder checksum bytes as they are produced rather than buffering
/// the whole file twice.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Crc16(u16);

impl Crc16 {
    /// A fresh CRC with the FIT initial value (zero).
    pub const fn new() -> Self {
        Self(0)
    }

    /// Fold `data` into the running CRC.
    pub fn update(&mut self, data: &[u8]) {
        let mut crc = self.0;
        for &byte in data {
            // Low nibble, then high nibble.
            let mut tmp = CRC_TABLE[(crc & 0xF) as usize];
            crc = (crc >> 4) & 0x0FFF;
            crc = crc ^ tmp ^ CRC_TABLE[(byte & 0xF) as usize];

            tmp = CRC_TABLE[(crc & 0xF) as usize];
            crc = (crc >> 4) & 0x0FFF;
            crc = crc ^ tmp ^ CRC_TABLE[((byte >> 4) & 0xF) as usize];
        }
        self.0 = crc;
    }

    /// The current checksum.
    pub const fn value(self) -> u16 {
        self.0
    }
}

/// One-shot FIT CRC-16 over `data`.
pub fn crc16(data: &[u8]) -> u16 {
    let mut crc = Crc16::new();
    crc.update(data);
    crc.value()
}

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

    /// The canonical CRC-16/ARC check value: `crc("123456789") == 0xBB3D`.
    /// If this fails, every FIT file we produce is rejected.
    #[test]
    fn known_vector_check_string() {
        assert_eq!(crc16(b"123456789"), 0xBB3D);
    }

    #[test]
    fn known_vector_empty_and_zero() {
        assert_eq!(crc16(b""), 0x0000);
        // CRC-16/ARC of a single zero byte is 0.
        assert_eq!(crc16(&[0x00]), 0x0000);
        // Published CRC-16/ARC vectors.
        assert_eq!(crc16(b"A"), 0x30C0);
        assert_eq!(crc16(&[0x00, 0x00, 0x00, 0x00]), 0x0000);
    }

    #[test]
    fn matches_reference_bitwise_implementation() {
        // Independent, deliberately naive reflected-CRC implementation.
        fn reference(data: &[u8]) -> u16 {
            let mut crc: u16 = 0;
            for &b in data {
                crc ^= b as u16;
                for _ in 0..8 {
                    if crc & 1 != 0 {
                        crc = (crc >> 1) ^ 0xA001;
                    } else {
                        crc >>= 1;
                    }
                }
            }
            crc
        }

        // A deterministic pseudo-random corpus.
        let mut data = Vec::new();
        let mut x: u32 = 0x1234_5678;
        for _ in 0..1000 {
            x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
            data.push((x >> 16) as u8);
            assert_eq!(crc16(&data), reference(&data), "mismatch at len {}", data.len());
        }
    }

    #[test]
    fn incremental_equals_one_shot() {
        let data: Vec<u8> = (0u8..=255).cycle().take(777).collect();
        let mut running = Crc16::new();
        for chunk in data.chunks(13) {
            running.update(chunk);
        }
        assert_eq!(running.value(), crc16(&data));
    }
}
