use super::*;
use super::errors::*;

use std;
use std::os::raw::c_void;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct BufferId(u32);

impl From<BufferId> for u32 {
    fn from(buffer_id: BufferId) -> Self {
        buffer_id.0
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct BlockIndex(pub u32);

impl From<BlockIndex> for u32 {
    fn from(block_index: BlockIndex) -> Self {
        block_index.0
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BufferMutability {
    Mutable,
    Immutable,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BufferMap {
    Read,
    Write,
    ReadWrite,
}

#[derive(Debug, Copy, Clone)]
pub enum BufferData<'a, T: 'a> {
    Empty(usize),
    Data(&'a [T]),
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BindBufferTarget {
    DispatchIndirectBuffer,
    DrawIndirectBuffer,
    PixelPackBuffer,
    PixelUnpackBuffer,
}

impl From<BindBufferTarget> for u32 {
    fn from(target: BindBufferTarget) -> Self {
        match target {
            BindBufferTarget::DispatchIndirectBuffer => gl::DISPATCH_INDIRECT_BUFFER,
            BindBufferTarget::DrawIndirectBuffer => gl::DRAW_INDIRECT_BUFFER,
            BindBufferTarget::PixelPackBuffer => gl::PIXEL_PACK_BUFFER,
            BindBufferTarget::PixelUnpackBuffer => gl::PIXEL_UNPACK_BUFFER,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum BufferBaseTarget {
    AtomicCounter,
    TransformFeedback,
    Uniform,
    ShaderStorage,
}

impl From<BufferBaseTarget> for u32 {
    fn from(target: BufferBaseTarget) -> Self {
        match target {
            BufferBaseTarget::AtomicCounter => gl::ATOMIC_COUNTER_BUFFER,
            BufferBaseTarget::TransformFeedback => gl::TRANSFORM_FEEDBACK_BUFFER,
            BufferBaseTarget::Uniform => gl::UNIFORM_BUFFER,
            BufferBaseTarget::ShaderStorage => gl::SHADER_STORAGE_BUFFER,
        }
    }
}


/// Returns a new buffer
///
/// # Example
///
/// ```
/// let vertices_buffer = create_buffer();
/// ```
///
pub fn create_buffer() -> BufferId {
    let mut buffer_id: u32 = 0;
    let buffer_id_ptr: *mut u32 = &mut buffer_id;

    // Safe without errors because:
    // - GL_INVALID_VALUE happens when *n* is negative, 1 is not negative
    // - pointer to memory is valid
    unsafe {
        gl::CreateBuffers(1, buffer_id_ptr);
    }

    BufferId(buffer_id)
}

/// Initializes buffer
///
/// # Arguments
/// - `id` - id of the buffer
/// - `data` - enumeration, Empty(size) will allocate the size for the buffer,
/// Data(&[T]) will initialize size of &[T] and copy data from slice to GPU memory
/// - `mutability` - sets the mutability for the buffer. Beware, in OpenGL terminology
/// immutable buffer means not resizable but all buffers created with `glNamedBufferStorage`
/// are non-resizable. This simply says whether you can **modify** data in the buffer
/// - `map` - whether to map the buffer for reading, writing, both or none
///
/// # Example
///
/// ```
/// let vertices: Vec<f32> = vec![-1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 1.0, 0.0];
/// let vertices_buffer = create_buffer();
/// named_buffer_storage(
///     buffer,
///     BufferData::Data(&vertices),
///     BufferMutability:Immutable,
///     None
/// );
/// ```
///
pub unsafe fn named_buffer_storage<T, M>(
    id: BufferId,
    data: BufferData<T>,
    mutability: BufferMutability,
    map: M,
) -> Result<()>
where
    M: Into<Option<BufferMap>>,
{
    let mut flags: u32 = match mutability {
        BufferMutability::Mutable => gl::DYNAMIC_STORAGE_BIT,
        _ => 0,
    };

    flags |= match map.into() {
        None => 0,
        Some(BufferMap::Read) => gl::MAP_READ_BIT | gl::MAP_COHERENT_BIT | gl::MAP_PERSISTENT_BIT,
        Some(BufferMap::Write) => gl::MAP_WRITE_BIT | gl::MAP_COHERENT_BIT | gl::MAP_PERSISTENT_BIT,
        Some(BufferMap::ReadWrite) => {
            gl::MAP_READ_BIT | gl::MAP_WRITE_BIT | gl::MAP_COHERENT_BIT | gl::MAP_PERSISTENT_BIT
        }
    };

    match data {
        BufferData::Data(data) => {
            let size = (std::mem::size_of::<T>() * data.len()) as isize;
            let buffer_ptr = data.as_ptr() as *const c_void;

            gl::NamedBufferStorage(id.into(), size, buffer_ptr, flags);
        }
        BufferData::Empty(size) => {
            gl::NamedBufferStorage(id.into(), size as isize, std::ptr::null(), flags);
        }
    }

    get_error(())
}

/// Copies data from slice to GPU memory pointer by buffer
///
/// # Arguments
/// - `id` - id of the buffer
/// - `offset` - offset into buffer where to copy
/// - `data` - slice of data to copy
///
pub unsafe fn named_buffer_sub_data<T>(
    buffer_id: BufferId,
    offset: isize,
    data: &[T],
) -> Result<()> {
    let size = (std::mem::size_of::<T>() * data.len()) as isize;
    let buffer_ptr = data.as_ptr() as *const c_void;

    gl::NamedBufferSubData(buffer_id.into(), offset, size, buffer_ptr);

    get_error(())
}

/// Returns pointer to buffer memory
///
/// Every mapping is always COHERENT and PERSISTENT, this follows AZDO advices
/// and examples
pub unsafe fn map_named_buffer_range(
    id: BufferId,
    offset: usize,
    length: usize,
    access: BufferMap,
) -> Result<*mut c_void> {
    let flags = match access {
        BufferMap::Read => gl::MAP_READ_BIT | gl::MAP_COHERENT_BIT | gl::MAP_PERSISTENT_BIT,
        BufferMap::Write => gl::MAP_WRITE_BIT | gl::MAP_COHERENT_BIT | gl::MAP_PERSISTENT_BIT,
        BufferMap::ReadWrite => {
            gl::MAP_WRITE_BIT | gl::MAP_READ_BIT | gl::MAP_COHERENT_BIT | gl::MAP_PERSISTENT_BIT
        }
    };

    let pointer: *mut c_void =
        gl::MapBufferRange(id.into(), offset as isize, length as isize, flags);

    get_error(pointer)
}

pub unsafe fn bind_buffer(target: BindBufferTarget, id: BufferId) -> Result<()> {
    gl::BindBuffer(target.into(), id.into());

    get_error(())
}

pub unsafe fn bind_buffer_base(
    target: BufferBaseTarget,
    block_index: BlockIndex,
    buffer_id: BufferId,
) -> Result<()> {
    gl::BindBufferBase(target.into(), block_index.into(), buffer_id.into());

    get_error(())
}

pub unsafe fn bind_buffers_base(
    target: BufferBaseTarget,
    first_ubo_index: BlockIndex,
    buffers_id: &[BufferId],
) -> Result<()> {
    gl::BindBuffersBase(
        target.into(),
        first_ubo_index.into(),
        buffers_id.len() as i32,
        buffers_id
            .iter()
            .map(|id| id.0)
            .collect::<Vec<u32>>()
            .as_ptr(),
    );

    get_error(())
}


/// Deletes buffer
pub unsafe fn delete_buffer(id: BufferId) {
    let buffer_id_ptr: *const u32 = &id.into();

    gl::DeleteBuffers(1, buffer_id_ptr);
}
