//! Block device trait and implementations.

use crate::{Error, Result};
use std::io::{Read, Seek, SeekFrom, Write};

/// Trait for block device operations.
///
/// Implement this trait to provide a custom block device backend.
/// The block device must support reading and writing at block granularity.
pub trait BlockDevice: Send {
    /// Returns the physical block size in bytes.
    fn block_size(&self) -> u32;

    /// Returns the total number of blocks.
    fn block_count(&self) -> u64;

    /// Read blocks from the device.
    ///
    /// # Arguments
    /// * `buf` - Buffer to read into (must be at least `block_count * block_size` bytes)
    /// * `block_id` - Starting block number
    /// * `block_count` - Number of blocks to read
    fn read_blocks(&mut self, buf: &mut [u8], block_id: u64, block_count: u32) -> Result<()>;

    /// Write blocks to the device.
    ///
    /// # Arguments
    /// * `buf` - Buffer to write from (must be at least `block_count * block_size` bytes)
    /// * `block_id` - Starting block number
    /// * `block_count` - Number of blocks to write
    fn write_blocks(&mut self, buf: &[u8], block_id: u64, block_count: u32) -> Result<()>;

    /// Open the device (optional).
    fn open(&mut self) -> Result<()> {
        Ok(())
    }

    /// Close the device (optional).
    fn close(&mut self) -> Result<()> {
        Ok(())
    }
}

/// A block device backed by any type implementing `Read + Write + Seek`.
///
/// This is useful for using files, memory buffers, or any seekable I/O as a block device.
pub struct IoBlockDevice<T> {
    inner: T,
    block_size: u32,
    block_count: u64,
}

impl<T> IoBlockDevice<T>
where
    T: Read + Write + Seek,
{
    /// Create a new IoBlockDevice wrapping the given I/O object.
    ///
    /// # Arguments
    /// * `inner` - The underlying I/O object
    /// * `block_size` - Block size in bytes (typically 512 or 4096)
    /// * `total_size` - Total size of the device in bytes
    pub fn new(inner: T, block_size: u32, total_size: u64) -> Self {
        let block_count = total_size / block_size as u64;
        Self {
            inner,
            block_size,
            block_count,
        }
    }

    /// Get a reference to the underlying I/O object.
    pub fn inner(&self) -> &T {
        &self.inner
    }

    /// Get a mutable reference to the underlying I/O object.
    pub fn inner_mut(&mut self) -> &mut T {
        &mut self.inner
    }

    /// Consume the wrapper and return the underlying I/O object.
    pub fn into_inner(self) -> T {
        self.inner
    }
}

impl<T> BlockDevice for IoBlockDevice<T>
where
    T: Read + Write + Seek + Send,
{
    fn block_size(&self) -> u32 {
        self.block_size
    }

    fn block_count(&self) -> u64 {
        self.block_count
    }

    fn read_blocks(&mut self, buf: &mut [u8], block_id: u64, block_count: u32) -> Result<()> {
        let offset = block_id * self.block_size as u64;
        let size = block_count as usize * self.block_size as usize;

        if buf.len() < size {
            return Err(Error::InvalidConfig(format!(
                "buffer too small: need {} bytes, got {}",
                size,
                buf.len()
            )));
        }

        self.inner.seek(SeekFrom::Start(offset))?;
        self.inner.read_exact(&mut buf[..size])?;
        Ok(())
    }

    fn write_blocks(&mut self, buf: &[u8], block_id: u64, block_count: u32) -> Result<()> {
        let offset = block_id * self.block_size as u64;
        let size = block_count as usize * self.block_size as usize;

        if buf.len() < size {
            return Err(Error::InvalidConfig(format!(
                "buffer too small: need {} bytes, got {}",
                size,
                buf.len()
            )));
        }

        self.inner.seek(SeekFrom::Start(offset))?;
        self.inner.write_all(&buf[..size])?;
        Ok(())
    }
}

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

    #[test]
    fn test_io_block_device_creation() {
        let data = vec![0u8; 4096];
        let cursor = Cursor::new(data);
        let device = IoBlockDevice::new(cursor, 512, 4096);

        assert_eq!(device.block_size(), 512);
        assert_eq!(device.block_count(), 8); // 4096 / 512 = 8
    }

    #[test]
    fn test_io_block_device_write_read() {
        let data = vec![0u8; 4096];
        let cursor = Cursor::new(data);
        let mut device = IoBlockDevice::new(cursor, 512, 4096);

        // Write test data
        let write_buf = vec![0xAB; 512];
        device.write_blocks(&write_buf, 0, 1).unwrap();

        // Read it back
        let mut read_buf = vec![0u8; 512];
        device.read_blocks(&mut read_buf, 0, 1).unwrap();

        assert_eq!(read_buf, write_buf);
    }

    #[test]
    fn test_io_block_device_write_read_multiple_blocks() {
        let data = vec![0u8; 4096];
        let cursor = Cursor::new(data);
        let mut device = IoBlockDevice::new(cursor, 512, 4096);

        // Write 2 blocks at block 2
        let write_buf = vec![0xCD; 1024];
        device.write_blocks(&write_buf, 2, 2).unwrap();

        // Read them back
        let mut read_buf = vec![0u8; 1024];
        device.read_blocks(&mut read_buf, 2, 2).unwrap();

        assert_eq!(read_buf, write_buf);

        // Verify block 0 is still zero
        let mut zero_buf = vec![0u8; 512];
        device.read_blocks(&mut zero_buf, 0, 1).unwrap();
        assert_eq!(zero_buf, vec![0u8; 512]);
    }

    #[test]
    fn test_io_block_device_buffer_too_small() {
        let data = vec![0u8; 4096];
        let cursor = Cursor::new(data);
        let mut device = IoBlockDevice::new(cursor, 512, 4096);

        // Try to read 2 blocks into a 512-byte buffer
        let mut small_buf = vec![0u8; 512];
        let result = device.read_blocks(&mut small_buf, 0, 2);
        assert!(result.is_err());

        // Try to write 2 blocks from a 512-byte buffer
        let small_write_buf = vec![0u8; 512];
        let result = device.write_blocks(&small_write_buf, 0, 2);
        assert!(result.is_err());
    }

    #[test]
    fn test_io_block_device_into_inner() {
        let data = vec![0u8; 1024];
        let cursor = Cursor::new(data);
        let device = IoBlockDevice::new(cursor, 512, 1024);

        let recovered = device.into_inner();
        assert_eq!(recovered.into_inner().len(), 1024);
    }

    #[test]
    fn test_block_device_open_close_default() {
        let data = vec![0u8; 1024];
        let cursor = Cursor::new(data);
        let mut device = IoBlockDevice::new(cursor, 512, 1024);

        // Default implementations should succeed
        assert!(device.open().is_ok());
        assert!(device.close().is_ok());
    }
}
