const BUFFER_TYPE = @map {Symbol, GLenum} {
    Array       = GL_ARRAY_BUFFER,
    Element     = GL_ELEMENT_ARRAY_BUFFER,
    Texture     = GL_TEXTURE_BUFFER,
    Uniform     = GL_UNIFORM_BUFFER,
    Atomic      = GL_ATOMIC_COUNTER_BUFFER,
    CopyRead    = GL_COPY_READ_BUFFER,
    CopyWrite   = GL_COPY_WRITE_BUFFER,
    Dispatch    = GL_DISPATCH_INDIRECT_BUFFER,
    Draw        = GL_DRAW_INDIRECT_BUFFER,
    PixelPack   = GL_PIXEL_PACK_BUFFER,
    PixelUnpack = GL_PIXEL_UNPACK_BUFFER,
}
"""
        genbuffer() --> GLuint

Creates a OpenGL `buffer` object.
"""
function genbuffer()
    buffer = GLuint'[]
    glGenBuffers(1, buffer)
    return buffer'
end
"""
        bindbuffer(buffer, type::Symbol) -> Nothing

Bind the `buffer` object.

The `type` parameter can be:

- `:Array`       for `GL_ARRAY_BUFFER`
- `:Element`     for `GL_ELEMENT_ARRAY_BUFFER`
- `:Texture`     for `GL_TEXTURE_BUFFER`
- `:Uniform`     for `GL_UNIFORM_BUFFER`
- `:Atomic`      for `GL_ATOMIC_COUNTER_BUFFER`
- `:CopyRead`    for `GL_COPY_READ_BUFFER`
- `:CopyWrite`   for `GL_COPY_WRITE_BUFFER`
- `:Dispatch`    for `GL_DISPATCH_INDIRECT_BUFFER`
- `:Draw`        for `GL_DRAW_INDIRECT_BUFFER`
- `:PixelPack`   for `GL_PIXEL_PACK_BUFFER`
- `:PixelUnpack` for `GL_PIXEL_UNPACK_BUFFER`
"""
function bindbuffer(type::Symbol, buffer::GLuint)
    @assert type in keys(BUFFER_TYPE) "Unknown buffer type: $type"
    glBindBuffer(BUFFER_TYPE[type], buffer)
end
"""
        bufferdata(type, data, usage) -> Nothing

Creates and initializes a `buffer` object's data store.

The `type` parameter can be:

- `:Array`       for `GL_ARRAY_BUFFER`
- `:Element`     for `GL_ELEMENT_ARRAY_BUFFER`
- `:Texture`     for `GL_TEXTURE_BUFFER`
- `:Uniform`     for `GL_UNIFORM_BUFFER`
- `:Atomic`      for `GL_ATOMIC_COUNTER_BUFFER`
- `:CopyRead`    for `GL_COPY_READ_BUFFER`
- `:CopyWrite`   for `GL_COPY_WRITE_BUFFER`
- `:Dispatch`    for `GL_DISPATCH_INDIRECT_BUFFER`
- `:Draw`        for `GL_DRAW_INDIRECT_BUFFER`
- `:PixelPack`   for `GL_PIXEL_PACK_BUFFER`
- `:PixelUnpack` for `GL_PIXEL_UNPACK_BUFFER`
"""
function bufferdata(type::Symbol, data::AbstractArray, usage::GLenum)
    target = BUFFER_TYPE[type]
    glBufferData(target, sizeof(data), data, usage)
end
"""
        arraybuffer(data; usage::Symbol) -> GLuint

Creates an `Array` buffer object and initializes with
the specified `usage` and `data`.
"""
function arraybuffer(data::AbstractArray, usage::GLenum)
    buffer = genbuffer()
    bindbuffer(:Array, buffer)
    bufferdata(:Array, data, usage)
    return buffer
end 
"""
        indexbuffer(data; usage::Symbol) -> GLuint

Creates an `ElementArray` buffer object and initializes with
the specified `usage` and `data`.
"""
function indexbuffer(data::AbstractArray, usage::GLenum)
    buffer = genbuffer()
    bindbuffer(:Element, buffer)
    bufferdata(:Element, data, usage)
    return buffer
end

function vertexarray()
    id = GLuint'[0]
    glGenVertexArrays(1, id)
    glBindVertexArray(id')
    return id'
end
"""
        vertexattrib(type, stride, attributes) -> Nothing

Define an array of vertex attribute data.

- *@param* `type` - the vertex data type.
- *@param* `stride` - the size of each vertex.
- *@param* `attributes` - a map (Dict) containg the index, size and offset of each attribute.

The `attributes` parameter is a `Dict{Int, NamedTuple}`:

```julia
attributes = Dict(
    index₁ => ( size = size₁, offset = offset₁ [, normalized = false] ),
    index₂ => ( size = size₂, offset = offset₂ [, normalized = false] ),
        ⋮
    indexₙ  => ( size = sizeₙ, offset = offsetₙ  [, normalized = false] ),
) 
```
        
# Usage

```julia
vertexattrib(Cfloat, 4, @map {
    0 := (size = 2, offset = 0),
    1 := (size = 2, offset = 2),
})
```
"""
function vertexattrib(::Type{T}, stride::Int, attributes::Dict{Int, <:NamedTuple}) where {T}
    for (index, attr) in attributes
        size = attr.size
        offset = attr.offset
        norm = hasproperty(attr, :normalized) ? attr.normalized : false
        glEnableVertexAttribArray(index)
        glVertexAttribPointer(
            index,
            size,
            genumof(T),
            gbool(norm),
            stride * sizeof(T),
            offset * sizeof(T) |> Ptr{Cvoid}
        )
    end
end

function vertexattrib(::Type{T}, stride::Int, attributes::NamedTuple...) where {T}
    indices = 0:length(attributes) - 1
    vertexattrib(T, stride, Dict(zip(indices, attributes)))
end