use std::mem::size_of;
use std::ops::{Deref, DerefMut};
use std::ptr::null;
use std::slice::from_raw_parts_mut;

use crate::{
    error::Error,
    misc::{AsEnum, Bindable},
};

/* ArrayBuffer */

pub struct ArrayBuffer {
    id: gl::GLuint,
    target: Target,
    size: usize,
}

impl ArrayBuffer {
    pub fn new(target: Target) -> Result<Self, Error> {
        let mut id = 0;

        Error::checked(|| gl::create_buffers(1, &mut id))?;

        Ok(Self {
            id,
            target,
            size: 0,
        })
    }

    pub fn id(&self) -> gl::GLuint {
        self.id
    }

    pub fn buffer_data<T>(&mut self, usage: Usage, data: &[T]) -> Result<(), Error> {
        let size = data.len() * size_of::<T>();

        Error::checked(|| {
            gl::named_buffer_data(
                self.id,
                size as gl::GLsizeiptr,
                data.as_ptr() as *const _,
                usage.as_enum(),
            )
        })?;

        self.size = size;

        Ok(())
    }

    pub fn buffer_size(&mut self, usage: Usage, size: usize) -> Result<(), Error> {
        Error::checked(|| {
            gl::named_buffer_data(self.id, size as gl::GLsizeiptr, null(), usage.as_enum())
        })?;

        self.size = size;

        Ok(())
    }

    pub fn map<T>(&self) -> Result<Map<'_, T>, Error> {
        Ok(Map {
            data: self.inner_map(gl::READ_ONLY)?,
            buf: self,
        })
    }

    pub fn map_mut<T>(&mut self, write_only: bool) -> Result<MapMut<'_, T>, Error> {
        Ok(MapMut {
            data: self.inner_map(if write_only {
                gl::WRITE_ONLY
            } else {
                gl::READ_WRITE
            })?,
            buf: self,
        })
    }

    pub fn map_range<T>(
        &self,
        byte_offset: usize,
        value_count: Option<usize>,
    ) -> Result<Map<'_, T>, Error> {
        Ok(Map {
            data: self.inner_map_range(gl::READ_ONLY, byte_offset, value_count)?,
            buf: self,
        })
    }

    pub fn map_range_mut<T>(
        &mut self,
        write_only: bool,
        byte_offset: usize,
        value_count: Option<usize>,
    ) -> Result<MapMut<'_, T>, Error> {
        let access = if write_only {
            gl::WRITE_ONLY
        } else {
            gl::READ_WRITE
        };

        Ok(MapMut {
            data: self.inner_map_range(access, byte_offset, value_count)?,
            buf: self,
        })
    }

    pub fn bind_buffer_base(&self, index: gl::GLuint) {
        gl::bind_buffer_base(self.target.as_enum(), index, self.id);
    }

    pub fn bind_buffer_range(&self, index: gl::GLuint, offset: gl::GLintptr, size: gl::GLsizeiptr) {
        gl::bind_buffer_range(self.target.as_enum(), index, self.id, offset, size);
    }

    fn inner_map<T>(&self, access: gl::GLenum) -> Result<&mut [T], Error> {
        let ptr = Error::err_if(&null(), gl::map_named_buffer(self.id, access))?;
        let count = self.size / size_of::<T>();

        Ok(unsafe { from_raw_parts_mut(ptr as *mut T, count) })
    }

    pub fn inner_map_range<T>(
        &self,
        access: gl::GLenum,
        byte_offset: usize,
        value_count: Option<usize>,
    ) -> Result<&mut [T], Error> {
        let count = value_count.unwrap_or_else(|| {
            if self.size > byte_offset {
                (self.size - byte_offset) / size_of::<T>()
            } else {
                0
            }
        });

        if byte_offset + count * size_of::<T>() > self.size {
            return Err(Error::InvalidParameter);
        }

        let size = count * size_of::<T>();
        let ptr = Error::err_if(
            &null(),
            gl::map_named_buffer_range(self.id, byte_offset as isize, size as isize, access),
        )?;

        Ok(unsafe { from_raw_parts_mut(ptr as *mut T, count) })
    }
}

impl Drop for ArrayBuffer {
    fn drop(&mut self) {
        gl::delete_buffers(1, &self.id);
    }
}

impl Bindable for ArrayBuffer {
    fn bind(&self) {
        gl::bind_buffer(self.target.as_enum(), self.id);
    }

    fn unbind(&self) {
        gl::bind_buffer(self.target.as_enum(), 0);
    }
}

/* Map */

pub struct Map<'a, T> {
    buf: &'a ArrayBuffer,
    data: &'a [T],
}

impl<T> Drop for Map<'_, T> {
    fn drop(&mut self) {
        gl::unmap_named_buffer(self.buf.id);
    }
}

impl<T> Deref for Map<'_, T> {
    type Target = [T];

    fn deref(&self) -> &Self::Target {
        self.data
    }
}

/* MapMut */

pub struct MapMut<'a, T> {
    buf: &'a ArrayBuffer,
    data: &'a mut [T],
}

impl<T> Drop for MapMut<'_, T> {
    fn drop(&mut self) {
        gl::unmap_named_buffer(self.buf.id);
    }
}

impl<T> Deref for MapMut<'_, T> {
    type Target = [T];

    fn deref(&self) -> &Self::Target {
        self.data
    }
}

impl<T> DerefMut for MapMut<'_, T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.data
    }
}

/* Target */

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Target {
    ArrayBuffer,
    AtomicCounterBuffer,
    CopyReadBuffer,
    CopyWriteBuffer,
    DispatchIndirectBuffer,
    DrawIndirectBuffer,
    ElementArrayBuffer,
    PixelPackBuffer,
    PixelUnpackBuffer,
    QueryBuffer,
    ShaderStorageBuffer,
    TextureBuffer,
    TransformFeedbackBuffer,
    UniformBuffer,
}

impl AsEnum for Target {
    fn as_enum(&self) -> gl::GLenum {
        match self {
            Self::ArrayBuffer => gl::ARRAY_BUFFER,
            Self::AtomicCounterBuffer => gl::ATOMIC_COUNTER_BUFFER,
            Self::CopyReadBuffer => gl::COPY_READ_BUFFER,
            Self::CopyWriteBuffer => gl::COPY_WRITE_BUFFER,
            Self::DispatchIndirectBuffer => gl::DISPATCH_INDIRECT_BUFFER,
            Self::DrawIndirectBuffer => gl::DRAW_INDIRECT_BUFFER,
            Self::ElementArrayBuffer => gl::ELEMENT_ARRAY_BUFFER,
            Self::PixelPackBuffer => gl::PIXEL_PACK_BUFFER,
            Self::PixelUnpackBuffer => gl::PIXEL_UNPACK_BUFFER,
            Self::QueryBuffer => gl::QUERY_BUFFER,
            Self::ShaderStorageBuffer => gl::SHADER_STORAGE_BUFFER,
            Self::TextureBuffer => gl::TEXTURE_BUFFER,
            Self::TransformFeedbackBuffer => gl::TRANSFORM_FEEDBACK_BUFFER,
            Self::UniformBuffer => gl::UNIFORM_BUFFER,
        }
    }
}

/* Usage */

pub enum Usage {
    StreamDraw,
    StreamRead,
    StreamCopy,
    StaticDraw,
    StaticRead,
    StatidCopy,
    DynamicDraw,
    DynamicRead,
    DynamicCopy,
}

impl AsEnum for Usage {
    fn as_enum(&self) -> gl::GLenum {
        match self {
            Self::StreamDraw => gl::STREAM_DRAW,
            Self::StreamRead => gl::STREAM_READ,
            Self::StreamCopy => gl::STREAM_COPY,
            Self::StaticDraw => gl::STATIC_DRAW,
            Self::StaticRead => gl::STATIC_READ,
            Self::StatidCopy => gl::STATIC_COPY,
            Self::DynamicDraw => gl::DYNAMIC_DRAW,
            Self::DynamicRead => gl::DYNAMIC_READ,
            Self::DynamicCopy => gl::DYNAMIC_COPY,
        }
    }
}
