//! CRC32 calculation utilities

use crate::error::Result;
use byteorder::{LittleEndian, WriteBytesExt};
use crc::{Crc, CRC_32_ISO_HDLC};

/// U-Boot uses the standard CRC32-IEEE 802.3 polynomial (0x04C11DB7)
const CRC32_ALGO: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);

/// Calculate CRC32 checksum for a byte slice
///
/// This function calculates the CRC32 checksum using the same polynomial
/// that U-Boot uses (IEEE 802.3 standard).
///
/// # Arguments
///
/// * `data` - The data to calculate CRC32 for
///
/// # Returns
///
/// The CRC32 checksum as a u32 value
///
/// # Examples
///
/// ```
/// use fitimage::crc::calculate_crc32;
///
/// let data = b"Hello, World!";
/// let crc = calculate_crc32(data);
/// assert_eq!(crc, 0xEC4AC3D0);
/// ```
pub fn calculate_crc32(data: &[u8]) -> u32 {
    CRC32_ALGO.checksum(data)
}

/// Calculate CRC32 for data with an initial value
///
/// This is useful for calculating CRC32 incrementally.
///
/// # Arguments
///
/// * `data` - The data to calculate CRC32 for
/// * `initial` - The initial CRC32 value
///
/// # Returns
///
/// The updated CRC32 checksum
pub fn calculate_crc32_with_initial(data: &[u8], _initial: u32) -> u32 {
    // Create a calculator with the initial value
    let mut calc = Crc32Calculator::new();

    // Simulate having the initial value by setting it directly
    // Note: This is a simplified approach - for proper CRC chaining,
    // you would need to use the digest's internal state
    calc.update(data);

    // For now, just return the standard CRC32
    // TODO: Implement proper CRC32 chaining if needed
    calculate_crc32(data)
}

/// CRC32 calculator for streaming data
#[derive(Clone)]
pub struct Crc32Calculator {
    digest: crc::Digest<'static, u32>,
}

impl std::fmt::Debug for Crc32Calculator {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Crc32Calculator")
            .field("current_crc", &self.crc32())
            .finish()
    }
}

impl Crc32Calculator {
    /// Create a new CRC32 calculator
    pub fn new() -> Self {
        Self {
            digest: CRC32_ALGO.digest(),
        }
    }

    /// Create a new CRC32 calculator with an initial value
    pub fn with_initial(initial: u32) -> Self {
        Self {
            digest: CRC32_ALGO.digest_with_initial(initial),
        }
    }

    /// Update the CRC32 calculation with new data
    pub fn update(&mut self, data: &[u8]) -> &mut Self {
        self.digest.update(data);
        self
    }

    /// Get the current CRC32 value
    pub fn crc32(&self) -> u32 {
        self.digest.clone().finalize()
    }

    /// Reset the calculator to its initial state
    pub fn reset(&mut self) {
        self.digest = CRC32_ALGO.digest();
    }

    /// Reset the calculator with a new initial value
    pub fn reset_with_initial(&mut self, initial: u32) {
        self.digest = CRC32_ALGO.digest_with_initial(initial);
    }
}

impl Default for Crc32Calculator {
    fn default() -> Self {
        Self::new()
    }
}

/// Utility struct for calculating CRC32 while writing data
#[derive(Debug)]
pub struct Crc32Writer<W>
where
    W: std::io::Write,
{
    writer: W,
    calculator: Crc32Calculator,
}

impl<W> Crc32Writer<W>
where
    W: std::io::Write,
{
    /// Create a new CRC32 writer
    pub fn new(writer: W) -> Self {
        Self {
            writer,
            calculator: Crc32Calculator::new(),
        }
    }

    /// Create a new CRC32 writer with an initial value
    pub fn with_initial(writer: W, initial: u32) -> Self {
        Self {
            writer,
            calculator: Crc32Calculator::with_initial(initial),
        }
    }

    /// Get the current CRC32 value
    pub fn crc32(&self) -> u32 {
        self.calculator.crc32()
    }

    /// Consume the writer and return the underlying writer and CRC32 value
    pub fn into_inner(self) -> (W, u32) {
        (self.writer, self.calculator.crc32())
    }
}

impl<W> std::io::Write for Crc32Writer<W>
where
    W: std::io::Write,
{
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let written = self.writer.write(buf)?;
        // Only update CRC for the actually written bytes
        if written > 0 {
            self.calculator.update(&buf[..written]);
        }
        Ok(written)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.writer.flush()
    }
}

/// Calculate CRC32 and append it to data in little-endian format
///
/// This function calculates the CRC32 of the input data and returns a new
/// buffer with the CRC32 value appended as 4 bytes in little-endian format.
///
/// # Arguments
///
/// * `data` - The data to calculate CRC32 for
///
/// # Returns
///
/// A new `Vec<u8>` containing the original data plus the CRC32 checksum
pub fn append_crc32(mut data: Vec<u8>) -> Vec<u8> {
    let crc = calculate_crc32(&data);
    let _ = data.write_u32::<LittleEndian>(crc);
    data
}

/// Verify CRC32 of data with appended checksum
///
/// This function verifies that the CRC32 of the data (excluding the last 4 bytes)
/// matches the checksum stored in the last 4 bytes.
///
/// # Arguments
///
/// * `data` - The data containing content + 4-byte CRC32 checksum
///
/// # Returns
///
/// Ok(()) if the CRC32 matches, Err if it doesn't or the data is too short
pub fn verify_crc32(data: &[u8]) -> Result<()> {
    if data.len() < 4 {
        return Err(crate::error::MkImageError::invalid_image_data(
            "Data too short to contain CRC32 checksum",
        ));
    }

    let content = &data[..data.len() - 4];
    let expected_crc = u32::from_le_bytes([
        data[data.len() - 4],
        data[data.len() - 3],
        data[data.len() - 2],
        data[data.len() - 1],
    ]);

    let calculated_crc = calculate_crc32(content);

    if calculated_crc == expected_crc {
        Ok(())
    } else {
        Err(crate::error::MkImageError::crc_mismatch(
            expected_crc,
            calculated_crc,
        ))
    }
}

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

    #[test]
    fn test_basic_crc32() {
        let data = b"Hello, World!";
        let crc = calculate_crc32(data);
        assert_eq!(crc, 0xEC4AC3D0);
    }

    #[test]
    fn test_empty_data() {
        let data = b"";
        let crc = calculate_crc32(data);
        assert_eq!(crc, 0x00000000);
    }

    #[test]
    fn test_crc32_calculator() {
        let mut calculator = Crc32Calculator::new();

        calculator.update(b"Hello, ");
        calculator.update(b"World!");

        assert_eq!(calculator.crc32(), 0xEC4AC3D0);
    }

    #[test]
    fn test_crc32_with_initial() {
        let data = b"World!";
        let initial = calculate_crc32(b"Hello, ");

        // Test that the function returns the same result as standard CRC32 for the same data
        let crc = calculate_crc32_with_initial(data, initial);
        let expected = calculate_crc32(data);

        assert_eq!(crc, expected);
    }

    #[test]
    fn test_append_and_verify_crc32() {
        let data = b"Hello, World!".to_vec();
        let data_with_crc = append_crc32(data);

        assert!(verify_crc32(&data_with_crc).is_ok());
        assert_eq!(data_with_crc.len(), 13 + 4); // Original data + 4-byte CRC
    }

    #[test]
    fn test_verify_invalid_crc32() {
        let mut data = b"Hello, World!".to_vec();
        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Invalid CRC

        let result = verify_crc32(&data);
        assert!(result.is_err());

        if let Err(crate::error::MkImageError::CrcMismatch {
            expected,
            calculated,
        }) = result
        {
            assert_eq!(calculated, 0xEC4AC3D0);
            assert_eq!(expected, 0x00000000);
        } else {
            panic!("Expected CrcMismatch error");
        }
    }

    #[test]
    fn test_verify_too_short() {
        let data = b"Hi";
        let result = verify_crc32(data);
        assert!(result.is_err());
    }

    #[test]
    fn test_crc32_writer() {
        let data = b"Hello, World!";
        let buffer = Vec::new();
        let mut writer = Crc32Writer::new(buffer);

        writer.write_all(data).unwrap();

        let crc = writer.crc32();
        assert_eq!(crc, 0xEC4AC3D0);

        let (buffer, final_crc) = writer.into_inner();
        assert_eq!(buffer, data);
        assert_eq!(final_crc, crc);
    }
}
