use std::cell::RefCell;
use std::marker::PhantomData;
use thiserror::Error;

#[derive(Error, Debug, Copy, Clone)]
pub enum DataWriterError {
    #[error("Buffer Full")]
    BufferFull
}

pub trait WriteableDataType {
    const BYTES: usize;
    fn to_be_slice(&self, data: &mut [u8]);
    fn to_le_slice(&self, data: &mut [u8]);
}

macro_rules! primitive_writeable {
    ( $primitive:ty, $length:expr ) => {
        impl WriteableDataType for $primitive {
            const BYTES: usize = $length;
            fn to_be_slice(&self, data: &mut [u8]) {
                data.copy_from_slice(&self.to_be_bytes());
            }
            fn to_le_slice(&self, data: &mut [u8]) {
                data.copy_from_slice(&self.to_le_bytes());
            }
        }
    };
}

primitive_writeable!(u8, 1);
primitive_writeable!(u16, 2);
primitive_writeable!(u32, 4);
primitive_writeable!(u64, 8);
primitive_writeable!(u128, 16);
primitive_writeable!(usize, 8);
primitive_writeable!(i8, 1);
primitive_writeable!(i16, 2);
primitive_writeable!(i32, 4);
primitive_writeable!(i64, 8);
primitive_writeable!(i128, 16);
primitive_writeable!(isize, 8);
primitive_writeable!(f32, 4);
primitive_writeable!(f64, 8);

pub trait DataWriteAccess {
    fn get_data(&mut self) -> &mut [u8];
    fn get_position(&self) -> usize;
    fn set_position(&mut self, position: usize);
}

pub struct DeferredDataWrite<'a, T: WriteableDataType, U>
where
    RefCell<U>: DataWriter,
    <RefCell<U> as DataWriter>::WriterType: DataWriteAccess,
{
    writer: &'a RefCell<U>,
    position: usize,
    _phantom_data: PhantomData<T>,
}

pub trait DataWriter {
    type WriterType: DataWriteAccess;

    fn reset(&mut self);
    fn get_position(&self) -> usize;
    fn get_buffer(&mut self) -> &[u8];

    fn push_be<T: WriteableDataType>(&self, data: T) -> Result<(), DataWriterError>;
    fn push_le<T: WriteableDataType>(&self, data: T) -> Result<(), DataWriterError>;
    fn push(&self, data: &[u8]) -> Result<(), DataWriterError>;
    fn defer<T: WriteableDataType>(&self) -> Result<DeferredDataWrite<T, Self::WriterType>, DataWriterError>;
}

pub struct DataViewWriter<'a> {
    data: &'a mut [u8],
    position: usize,
}

impl<'a> DataViewWriter<'a> {
    pub fn new(data: &'a mut [u8]) -> RefCell<Self> {
        RefCell::new(Self {
            data,
            position: 0usize,
        })
    }
}

impl<'a> DataWriteAccess for DataViewWriter<'a> {
    fn get_data(&mut self) -> &mut [u8] {
        self.data
    }

    fn get_position(&self) -> usize {
        self.position
    }

    fn set_position(&mut self, position: usize) {
        self.position = position;
    }
}

pub struct DataBufferWriter<const LENGTH: usize> {
    data: [u8; LENGTH],
    position: usize,
}

impl<const LENGTH: usize> DataBufferWriter<LENGTH> {
    pub fn new() -> RefCell<Self> {
        RefCell::new(Self {
            data: [0u8; LENGTH],
            position: 0usize,
        })
    }
}

impl<const LENGTH: usize> DataWriteAccess for DataBufferWriter<LENGTH> {
    fn get_data(&mut self) -> &mut [u8] {
        &mut self.data
    }

    fn get_position(&self) -> usize {
        self.position
    }

    fn set_position(&mut self, position: usize) {
        self.position = position;
    }
}

impl<U: DataWriteAccess> DataWriter for RefCell<U> {
    type WriterType = U;

    fn reset(&mut self) {
        self.get_mut().set_position(0);
    }

    fn get_position(&self) -> usize {
        self.borrow().get_position()
    }

    fn get_buffer(&mut self) -> &[u8] {
        let position = self.get_position();
        &self.get_mut().get_data()[..position]
    }

    fn push_be<T: WriteableDataType>(&self, data: T) -> Result<(), DataWriterError> {
        let position = self.borrow().get_position();
        if self.borrow_mut().get_data().len() < position + T::BYTES {
            return Err(DataWriterError::BufferFull);
        }
        data.to_be_slice(&mut self.borrow_mut().get_data()[position..(position + T::BYTES)]);
        self.borrow_mut().set_position(position + T::BYTES);
        Ok(())
    }

    fn push_le<T: WriteableDataType>(&self, data: T) -> Result<(), DataWriterError> {
        let position = self.borrow().get_position();
        if self.borrow_mut().get_data().len() < position + T::BYTES {
            return Err(DataWriterError::BufferFull);
        }
        data.to_le_slice(&mut self.borrow_mut().get_data()[position..(position + T::BYTES)]);
        self.borrow_mut().set_position(position + T::BYTES);
        Ok(())
    }

    fn push(&self, data: &[u8]) -> Result<(), DataWriterError> {
        let position = self.borrow().get_position();
        if self.borrow_mut().get_data().len() < position + data.len() {
            return Err(DataWriterError::BufferFull);
        }
        self.borrow_mut().get_data()[position..(position + data.len())].copy_from_slice(data);
        self.borrow_mut().set_position(position + data.len());
        Ok(())
    }

    fn defer<T: WriteableDataType>(&self) -> Result<DeferredDataWrite<T, Self::WriterType>, DataWriterError> {
        let position = self.borrow().get_position();
        if self.borrow_mut().get_data().len() < position + T::BYTES {
            return Err(DataWriterError::BufferFull);
        }
        self.borrow_mut().set_position(position + T::BYTES);
        Ok(DeferredDataWrite {
            writer: self,
            position,
            _phantom_data: PhantomData,
        })
    }
}

impl<'a, T: WriteableDataType, U: DataWriteAccess> DeferredDataWrite<'a, T, U>
where
    RefCell<U>: DataWriter,
{
    pub fn set_be(self, data: T) {
        data.to_be_slice(&mut self.writer.borrow_mut().get_data()[self.position..(self.position + T::BYTES)]);
    }
    pub fn set_le(self, data: T) {
        data.to_le_slice(&mut self.writer.borrow_mut().get_data()[self.position..(self.position + T::BYTES)]);
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_buffer() -> Result<()> {
        let mut writer = DataBufferWriter::<128>::new();

        let deferred = writer.defer::<u16>()?;

        writer.push_be(1u8)?;
        assert_eq!(1, writer.borrow().data[2]);

        assert_eq!(0, writer.borrow().data[0]);
        assert_eq!(0, writer.borrow().data[1]);
        deferred.set_be(0x1234);
        assert_eq!(0x12, writer.borrow().data[0]);
        assert_eq!(0x34, writer.borrow().data[1]);

        Ok(())
    }
}

