# this function is used to get the 
# suffix of the glUniform function
# for the correct type
_type_suffix(::Type{<:Bool}) = "i"
_type_suffix(::Type{<:Signed}) = "i"
_type_suffix(::Type{<:Unsigned}) = "ui"
_type_suffix(::Type{<:AbstractFloat}) = "f"

"""
        uniform!(location, values...) -> Nothing

Set the uniform `vec` variable at `location`.

# Examples

```julia
myuniform = getuniform(program, "someUniform")
# set float vec3
uniform!(myuniform, 1.0, 1.0, 0.5)
# set float vec4
uniform!(myuniform, Cfloat[1, 2, 3, 4])
# set a 4x4 matrix
uniform!(myuniform, rand(Cfloat, 4, 4))
```
"""
function uniform! end

@generated function uniform!(location::GLint, values::Vararg{T, N}) where {N, T <: Real}
    suffix = _type_suffix(T)
    glFunc = Symbol("glUniform$(N)$suffix")
    return :( $(glFunc)(location, values...) )
end

@generated function uniform!(location::GLint, vector::SVector{N, T}) where {N, T <: Real}
    suffix = _type_suffix(T)
    glFunc = Symbol("glUniform$(N)$(suffix)v")
    return :( $(glFunc)(location, 1, vector) )
end

@generated function uniform!(location::GLint, matrix::SMatrix{N, M, T}, transposed::Bool = false) where {N, M, T <: Real}
    glFunc = Symbol(
        if N == M
            "glUniformMatrix$(N)fv"
        else
            "glUniformMatrix$(N)x$(M)fv"
        end
    )
    return :( $(glFunc)(location, 1, transposed, Cfloat[matrix...]) )
end

function uniform!(location::GLint, vector::AbstractVector{T}) where {T <: Real}
    N = length(vector)
    uniform!(location, SVector{N, T}(vector...))
end

function uniform!(location::GLint, matrix::AbstractMatrix{T}, transposed::Bool = false) where {T <: Real}
    N, M = size(matrix)
    uniform!(location, SMatrix{N, M, T}(matrix), transposed)
end
"""
        getuniform(program, name) -> GLint

Gets the location of the uniform variable
identified by `name`.
"""
getuniform(program::GLuint, name::SymString) = glGetUniformLocation(program, string(name))
"""
        @uniforms foo, bar, ... = program

Get the location of `foo`, `bar` and `etc` from `program`,
and assign each uniform to the corresponding variable.

This macro transform the following expression:

```julia
@uniforms foo, bar, qux: baz = program
```

Into:
```julia
foo = getuniform(program, :foo)
bar = getuniform(program, :bar)
qux = getuniform(program, :baz)
```

You can also rename the variables as following:

```julia
@uniforms a: foo, b: bar = program
```

The variable `a` contains the location of `foo`,
and `b` contains the loctation of `bar`.
"""
macro uniforms(ex)
    @assert ex.head == :(=)
    program = esc(ex.args[2])
    if !Meta.isexpr(ex.args[1], :tuple)
        args = [ex.args[1]]
    else
        args = ex.args[1].args
    end
    return Expr(:block,
        map(args) do arg
            if arg isa Symbol
                alias, name = arg, arg
            elseif Meta.isexpr(arg, :(=))
                alias, name = arg.args
            elseif Meta.isexpr(arg, :call)
                alias, name = arg.args[2:3]
            end
            return Expr(:(=), esc(alias), Expr(:call, :getuniform, program, QuoteNode(name)))
        end...
    )
end

macro uniforms!(ex)
    args = ex.args
    for i in eachindex(args)
        if Meta.isexpr(args[i], :(:=))
            name, value = args[i].args
            args[i] = Expr(:call, :uniform!, name, value)
        end
    end
    return esc(Expr(:block, args...))
end