Documentation
use std::fmt;
use std::hash::Hasher;
use std::io;

use byteorder::BigEndian;
use byteorder::ReadBytesExt;

use crate::config::CodeqConfig;

const CHECKSUM_BYTES: usize = size_of::<u64>();

/// A reader wrapper that calculates CRC32 checksum while reading data.
///
/// This reader wraps any type implementing `io::Read` and transparently calculates
/// a CRC32 checksum of all data read through it. The checksum can be either:
/// - Retrieved using [`finalize_checksum()`](Self::finalize_checksum)
/// - Verified against an expected value using [`verify_checksum()`](Self::verify_checksum)
/// The number of bytes read can be retrieved using [`read_bytes()`](Self::read_bytes).
///
/// Example:
#[cfg_attr(not(feature = "crc32fast"), doc = "```ignore")]
#[cfg_attr(feature = "crc32fast", doc = "```rust")]
/// # use std::io::Write;
/// # use std::io::Read;
/// # use codeq::config::CodeqConfig;
/// use codeq::ChecksumWriter;
/// use codeq::ChecksumReader;
/// use codeq::config::Crc32fast;
///
/// // Build a buffer with a checksum
/// let mut b = Vec::new();
/// let mut w = Crc32fast::new_writer(&mut b);
/// w.write(b"foo").unwrap();
/// w.write_checksum().unwrap();
///
/// // Read the buffer and verify the checksum
/// let mut r = Crc32fast::new_reader(&b[..]);
/// let mut read_buf = [0u8; 3];
/// r.read_exact(&mut read_buf).unwrap();
/// let crc = r.finalize_checksum();
/// assert_eq!(crc32fast::hash(b"foo") as u64, crc);
/// ```
/// 
/// Create a new reader with [`ChecksumReader::new`] or [`CodeqConfig::new_reader`], for example:
/// ```ignore
/// let reader = Crc32fast::new_reader(Vec::new());
/// ```
pub struct ChecksumReader<C, R>
where C: CodeqConfig
{
    hasher: C::Hasher,
    inner: R,
    read: usize,
}

impl<C, R> ChecksumReader<C, R>
where
    C: CodeqConfig,
    R: io::Read,
{
    /// Creates a new `ChecksumReader` wrapping the provided reader.
    pub fn new(inner: R) -> Self {
        Self {
            hasher: C::Hasher::default(),
            inner,
            read: 0,
        }
    }

    /// Returns the number of bytes read through this reader.
    pub fn read_bytes(&self) -> usize {
        self.read
    }

    /// Consumes the reader and returns the calculated CRC32 checksum.
    ///
    /// The returned value is the CRC32 checksum of all data read through this reader.
    #[allow(dead_code)]
    pub fn finalize_checksum(self) -> u64 {
        self.hasher.finish()
    }

    /// Verifies the calculated checksum against an expected value stored in the stream.
    ///
    /// Reads another 8-byte value from the underlying reader and compares
    /// its least significant 32 bits with the calculated checksum. The `context` closure
    /// is called to provide additional context in case of checksum mismatch.
    ///
    /// # Errors
    /// Returns [`io::Error`] with [`io::ErrorKind::InvalidData`] kind if checksums don't match.
    pub fn verify_checksum<D: fmt::Display>(self, context: impl Fn() -> D) -> io::Result<usize> {
        let mut r = self.inner;
        let actual = self.hasher.finish();

        let got = r.read_u64::<BigEndian>()?;
        if actual != got {
            Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "crc32 checksum mismatch: expected {:x}, got {:x}, while {}",
                    actual,
                    got,
                    context()
                ),
            ))
        } else {
            Ok(self.read + CHECKSUM_BYTES)
        }
    }
}

impl<C, R> io::Read for ChecksumReader<C, R>
where
    C: CodeqConfig,
    R: io::Read,
{
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let read = self.inner.read(buf)?;
        if read > 0 {
            self.hasher.write(&buf[..read]);
        }
        self.read += read;
        Ok(read)
    }
}

#[cfg(feature = "crc32fast")]
#[cfg(test)]
#[allow(clippy::redundant_clone)]
mod tests_crc32fast {
    use std::io;
    use std::io::Read;
    use std::io::Write;

    use crate::config::CodeqConfig;
    use crate::config::Crc32fast;

    #[test]
    fn test_checksum_reader() -> anyhow::Result<()> {
        let mut b = Vec::new();

        // write something
        {
            let mut n = 0;

            let mut w = Crc32fast::new_writer(&mut b);

            n += w.write(b"foo")?;
            n += w.write(b"bar")?;
            n += w.write_checksum()?;
            assert_eq!(n, 14);
        }

        // Finalize the checksum and verify it
        {
            let mut r = Crc32fast::new_reader(&b[..]);
            let mut read_buf = [0u8; 6];
            r.read_exact(&mut read_buf)?;
            assert_eq!(r.read_bytes(), 6);
            let crc = r.finalize_checksum();
            assert_eq!(crc32fast::hash(b"foobar") as u64, crc);
        }

        // Verify the checksum
        {
            let mut r = Crc32fast::new_reader(&b[..]);
            let mut read_buf = [0u8; 6];
            r.read_exact(&mut read_buf)?;
            assert_eq!(r.verify_checksum(|| "")?, 14);
        }

        // Verify against wrong checksum
        let last = b.len() - 1;
        b[last] = b[last].wrapping_add(1);

        {
            let mut r = Crc32fast::new_reader(&b[..]);
            let mut read_buf = [0; 6];
            r.read_exact(&mut read_buf)?;
            let res = r.verify_checksum(|| ""); // checksum error
            assert!(res.is_err());
        }

        Ok(())
    }

    struct OneByteReader<R>(R);

    impl<R: Read> Read for OneByteReader<R> {
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
            if buf.is_empty() {
                return Ok(0);
            }

            let mut one = [0; 1];
            let n = self.0.read(&mut one)?;
            if n == 1 {
                buf[0] = one[0];
            }
            Ok(n)
        }
    }

    #[test]
    fn test_checksum_reader_counts_only_accepted_bytes() -> anyhow::Result<()> {
        let mut r = Crc32fast::new_reader(OneByteReader(b"foobar".as_slice()));
        let mut buf = [0; 6];

        assert_eq!(r.read(&mut buf)?, 1);
        assert_eq!(r.read_bytes(), 1);
        assert_eq!(r.finalize_checksum(), crc32fast::hash(b"f") as u64);

        Ok(())
    }
}

#[cfg(feature = "crc64fast-nvme")]
#[cfg(test)]
#[allow(clippy::redundant_clone)]
mod tests_crc64fast_nvme {
    use std::io::Read;
    use std::io::Write;

    use crate::config::CodeqConfig;
    use crate::config::Crc64fastNvme;

    #[test]
    fn test_checksum_reader() -> anyhow::Result<()> {
        let mut b = Vec::new();

        // write something
        {
            let mut n = 0;

            let mut w = Crc64fastNvme::new_writer(&mut b);

            n += w.write(b"foo")?;
            n += w.write(b"bar")?;
            n += w.write_checksum()?;
            assert_eq!(n, 14);
        }

        // Finalize the checksum and verify it
        {
            let mut r = Crc64fastNvme::new_reader(&b[..]);
            let mut read_buf = [0u8; 6];
            r.read_exact(&mut read_buf)?;
            let crc = r.finalize_checksum();
            assert_eq!(Crc64fastNvme::hash(b"foobar") as u64, crc);
        }

        // Verify the checksum
        {
            let mut r = Crc64fastNvme::new_reader(&b[..]);
            let mut read_buf = [0u8; 6];
            r.read_exact(&mut read_buf)?;
            assert_eq!(r.verify_checksum(|| "")?, 14);
        }

        // Verify against wrong checksum
        let last = b.len() - 1;
        b[last] = b[last].wrapping_add(1);

        {
            let mut r = Crc64fastNvme::new_reader(&b[..]);
            let mut read_buf = [0; 6];
            r.read_exact(&mut read_buf)?;
            let res = r.verify_checksum(|| ""); // checksum error
            assert!(res.is_err());
        }

        Ok(())
    }
}