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

/* VertexArray */

pub struct VertexArray {
    id: gl::GLuint,
    buffers: Vec<ArrayBuffer>,
}

impl VertexArray {
    pub fn builder() -> Builder {
        Builder::default()
    }

    pub fn buffers(&self) -> &Vec<ArrayBuffer> {
        &self.buffers
    }

    pub fn buffers_mut(&mut self) -> &mut Vec<ArrayBuffer> {
        &mut self.buffers
    }
}

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

impl Bindable for VertexArray {
    fn bind(&self) {
        gl::bind_vertex_array(self.id);
    }

    fn unbind(&self) {
        gl::bind_vertex_array(0);
    }
}

/* Builder */

#[derive(Default)]
pub struct Builder {
    bindings: Vec<Binding>,
}

impl Builder {
    pub fn bind_buffer(mut self, buffer: ArrayBuffer) -> BindingBuilder {
        let binding = Binding {
            buffer,
            pointers: Vec::new(),
        };

        self.bindings.push(binding);

        BindingBuilder { builder: self }
    }

    pub fn build(self) -> Result<VertexArray, Error> {
        let mut id = 0;

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

        let mut vertex_array = VertexArray {
            id,
            buffers: Vec::new(),
        };

        let guard = BindGuard::new(&vertex_array);

        let mut buffers = Vec::new();
        for binding in self.bindings {
            let guard = BindGuard::new(&binding.buffer);

            for pointer in binding.pointers {
                Error::checked(|| gl::enable_vertex_attrib_array(pointer.index))?;

                Error::checked(|| {
                    gl::vertex_attrib_pointer(
                        pointer.index,
                        pointer.size,
                        pointer.type_.as_enum(),
                        if pointer.normalize {
                            gl::TRUE
                        } else {
                            gl::FALSE
                        },
                        pointer.stride,
                        pointer.offset as *const _,
                    )
                })?;

                if let Some(divisor) = pointer.divisor {
                    Error::checked(|| gl::vertex_attrib_divisor(pointer.index, divisor))?;
                }
            }

            drop(guard);

            buffers.push(binding.buffer);
        }

        drop(guard);

        vertex_array.buffers = buffers;

        Ok(vertex_array)
    }
}

/* BindingBuilder */

pub struct BindingBuilder {
    builder: Builder,
}

impl BindingBuilder {
    pub fn bind_buffer(self, buffer: ArrayBuffer) -> Self {
        self.builder.bind_buffer(buffer)
    }

    pub fn vertex_attrib_pointer(
        mut self,
        index: gl::GLuint,
        size: gl::GLint,
        type_: DataType,
        normalize: bool,
        stride: gl::GLsizei,
        offset: gl::GLsizei,
    ) -> Result<Self, Error> {
        for binding in &self.builder.bindings {
            for pointer in &binding.pointers {
                if pointer.index == index {
                    return Err(Error::VertexArrayIndexAlreadyInUse(index));
                }
            }
        }

        let pointer = Pointer {
            index,
            size,
            type_,
            normalize,
            stride,
            offset,
            divisor: None,
        };

        self.builder
            .bindings
            .last_mut()
            .unwrap()
            .pointers
            .push(pointer);

        Ok(self)
    }

    pub fn vertex_attrib_divisor(mut self, divisor: gl::GLuint) -> Result<Self, Error> {
        let binding = self.builder.bindings.last_mut().unwrap();
        let pointer = binding
            .pointers
            .last_mut()
            .ok_or(Error::VertexArrayExpectedPointer)?;

        pointer.divisor = Some(divisor);

        Ok(self)
    }

    pub fn build(self) -> Result<VertexArray, Error> {
        self.builder.build()
    }
}

/* DataType */

#[allow(non_camel_case_types)]
pub enum DataType {
    Byte,
    UnsignedByte,
    Short,
    UnsignedShort,
    Int,
    UnsignedInt,
    HalfFloat,
    Float,
    Double,
    Fixed,
    Int_2_10_10_10_Rev,
    UnsignedInt_2_10_10_10_Rev,
    UnsignedInt_10f_11f_11f_Rev,
}

impl AsEnum for DataType {
    fn as_enum(&self) -> gl::GLenum {
        match self {
            Self::Byte => gl::BYTE,
            Self::UnsignedByte => gl::UNSIGNED_BYTE,
            Self::Short => gl::SHORT,
            Self::UnsignedShort => gl::UNSIGNED_SHORT,
            Self::Int => gl::INT,
            Self::UnsignedInt => gl::UNSIGNED_INT,
            Self::HalfFloat => gl::HALF_FLOAT,
            Self::Float => gl::FLOAT,
            Self::Double => gl::DOUBLE,
            Self::Fixed => gl::FIXED,
            Self::Int_2_10_10_10_Rev => gl::INT_2_10_10_10_REV,
            Self::UnsignedInt_2_10_10_10_Rev => gl::UNSIGNED_INT_2_10_10_10_REV,
            Self::UnsignedInt_10f_11f_11f_Rev => gl::UNSIGNED_INT_10F_11F_11F_REV,
        }
    }
}

/* Binding */

struct Binding {
    buffer: ArrayBuffer,
    pointers: Vec<Pointer>,
}

/* Pointer */

struct Pointer {
    index: gl::GLuint,
    size: gl::GLint,
    type_: DataType,
    normalize: bool,
    stride: gl::GLsizei,
    offset: gl::GLsizei,
    divisor: Option<gl::GLuint>,
}
