use super::braid::CRC32_LSB_POLY;

pub const fn crc32_combine(crc1: u32, crc2: u32, len2: u64) -> u32 {
    crc32_combine_op(crc1, crc2, crc32_combine_gen(len2))
}

#[inline(always)]
const fn crc32_combine_gen(len2: u64) -> u32 {
    x2nmodp(len2, 3)
}

#[inline(always)]
const fn crc32_combine_op(crc1: u32, crc2: u32, op: u32) -> u32 {
    multmodp(op, crc1) ^ crc2
}

const X2N_TABLE: [u32; 32] = [
    0x40000000, 0x20000000, 0x08000000, 0x00800000, 0x00008000, 0xedb88320, 0xb1e6b092, 0xa06a2517,
    0xed627dae, 0x88d14467, 0xd7bbfe6a, 0xec447f11, 0x8e7ea170, 0x6427800e, 0x4d47bae0, 0x09fe548f,
    0x83852d0f, 0x30362f1a, 0x7b5a9cc3, 0x31fec169, 0x9fec022a, 0x6c8dedc4, 0x15d6874d, 0x5fde7a4e,
    0xbad90e37, 0x2e4e5eef, 0x4eaba214, 0xa8a472c0, 0x429a969e, 0x148d302a, 0xc40ba6d0, 0xc4e22c3c,
];

// Return a(x) multiplied by b(x) modulo p(x), where p(x) is the CRC polynomial,
// reflected. For speed, this requires that a not be zero.
const fn multmodp(a: u32, mut b: u32) -> u32 {
    let mut m = 1 << 31;
    let mut p = 0;

    loop {
        if (a & m) != 0 {
            p ^= b;
            if (a & (m - 1)) == 0 {
                break;
            }
        }
        m >>= 1;
        b = if (b & 1) != 0 {
            (b >> 1) ^ CRC32_LSB_POLY as u32
        } else {
            b >> 1
        };
    }

    p
}

// Return x^(n * 2^k) modulo p(x).
const fn x2nmodp(mut n: u64, mut k: u32) -> u32 {
    let mut p: u32 = 1 << 31; /* x^0 == 1 */

    while n > 0 {
        if (n & 1) != 0 {
            p = multmodp(X2N_TABLE[k as usize & 31], p);
        }
        n >>= 1;
        k += 1;
    }

    p
}

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

    use crate::crc32;

    #[test]
    fn test_crc32_combine() {
        ::quickcheck::quickcheck(test as fn(_) -> _);

        fn test(data: Vec<u8>) -> bool {
            let Some(buf_len) = data.first().copied() else {
                return true;
            };

            let buf_size = Ord::max(buf_len, 1) as usize;

            let crc0 = 0;
            let mut crc1 = crc0;
            let mut crc2 = crc0;

            /* CRC32 */
            for chunk in data.chunks(buf_size) {
                let crc3 = crc32(crc0, chunk);
                let op = crc32_combine_gen(chunk.len() as _);
                let crc4 = crc32_combine_op(crc1, crc3, op);
                crc1 = crc32(crc1, chunk);

                assert_eq!(crc1, crc4);
            }

            crc2 = crc32(crc2, &data);

            assert_eq!(crc1, crc2);

            let combine1 = crc32_combine(crc1, crc2, data.len() as _);
            let combine2 = crc32_combine(crc1, crc1, data.len() as _);
            assert_eq!(combine1, combine2);

            // Fast CRC32 combine.
            let op = crc32_combine_gen(data.len() as _);
            let combine1 = crc32_combine_op(crc1, crc2, op);
            let combine2 = crc32_combine_op(crc2, crc1, op);
            assert_eq!(combine1, combine2);

            let combine1 = crc32_combine(crc1, crc2, data.len() as _);
            let combine2 = crc32_combine_op(crc2, crc1, op);
            assert_eq!(combine1, combine2);

            true
        }
    }
}
