-- iir_effect.lua
iir_effect = {}

local openGL = require("opengl")
local ffi = require("ffi")
local sf = require("util.shaderfunctions")
local fbf = require("util.fbofunctions")

local glIntv     = ffi.typeof('GLint[?]')
local glUintv    = ffi.typeof('GLuint[?]')
local glCharv    = ffi.typeof('GLchar[?]')
local glSizeiv   = ffi.typeof('GLsizei[?]')
local glFloatv   = ffi.typeof('GLfloat[?]')
local glConstCharpp = ffi.typeof('const GLchar *[1]')

local vao = 0
local prog = 0
local vbos = {}
iir_effect.fbos = {}
iir_effect.pingpong = 1
iir_effect.mix_coeff = 0.95

local basic_vert = [[
#version 410 core

in vec4 vPosition;
in vec4 vColor;
out vec3 vfTexCoord;

void main()
{
    vfTexCoord = vec3(.5*(vColor.xy+1.),0.);
    gl_Position = vec4(vPosition.xy, 0., 1.);
}
]]

local basic_frag = [[
#version 330

in vec3 vfTexCoord;
out vec4 fragColor;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform float u_coeff;

void main()
{
    vec4 col1 = texture(tex1, vfTexCoord.xy);
    vec4 col2 = texture(tex2, vfTexCoord.xy);
    fragColor = mix(col1, col2, u_coeff);
}
]]

local pres_vert = [[
#version 410 core

in vec4 vPosition;
in vec4 vColor;
out vec3 vfTexCoord;

void main()
{
    vfTexCoord = vec3(.5*(vColor.xy+1.),0.);
    gl_Position = vec4(vPosition.xy, 0., 1.);
}
]]

local pres_frag = [[
#version 330

in vec3 vfTexCoord;
out vec4 fragColor;
uniform sampler2D tex1;

void main()
{
    vec4 col1 = texture(tex1, vfTexCoord.xy);
    fragColor = col1;
}
]]

local function init_quad_attributes()
    local verts = glFloatv(4*2, {
        -1,-1,
        1,-1,
        1,1,
        -1,1,
        })

    local vpos_loc = gl.GetAttribLocation(prog, "vPosition")
    local vcol_loc = gl.GetAttribLocation(prog, "vColor")

    local vvbo = glIntv(0)
    gl.GenBuffers(1, vvbo)
    gl.BindBuffer(GL.ARRAY_BUFFER, vvbo[0])
    gl.BufferData(GL.ARRAY_BUFFER, ffi.sizeof(verts), verts, GL.STATIC_DRAW)
    gl.VertexAttribPointer(vpos_loc, 2, GL.FLOAT, GL.FALSE, 0, nil)
    table.insert(vbos, vvbo)

    local cvbo = glIntv(0)
    gl.GenBuffers(1, cvbo)
    gl.BindBuffer(GL.ARRAY_BUFFER, cvbo[0])
    gl.BufferData(GL.ARRAY_BUFFER, ffi.sizeof(verts), verts, GL.DYNAMIC_DRAW)
    sf.CHECK_GL_ERROR()
    gl.VertexAttribPointer(vcol_loc, 2, GL.FLOAT, GL.FALSE, 0, nil)
    table.insert(vbos, cvbo)

    sf.CHECK_GL_ERROR()
    gl.EnableVertexAttribArray(vpos_loc)
    gl.EnableVertexAttribArray(vcol_loc)

    local quads = glUintv(3*2, {
        1,0,2,
        2,0,3,
    })
    local qvbo = glIntv(0)
    gl.GenBuffers(1, qvbo)
    gl.BindBuffer(GL.ELEMENT_ARRAY_BUFFER, qvbo[0])
    gl.BufferData(GL.ELEMENT_ARRAY_BUFFER, ffi.sizeof(quads), quads, GL.STATIC_DRAW)
    table.insert(vbos, qvbo)

    sf.CHECK_GL_ERROR()
end

function iir_effect.initGL()
    vbos = {}
    texs = {}
    local vaoId = ffi.new("int[1]")
    gl.GenVertexArrays(1, vaoId)
    vao = vaoId[0]
    gl.BindVertexArray(vao)

    prog = sf.make_shader_from_source({
        vsrc = basic_vert,
        fsrc = basic_frag,
        })
    pres = sf.make_shader_from_source({
        vsrc = pres_vert,
        fsrc = pres_frag,
        })

    init_quad_attributes()
    gl.BindVertexArray(0)
end

function iir_effect.exitGL()
    for _,v in pairs(vbos) do
        gl.DeleteBuffers(1,v)
    end
    vbos = {}
    gl.DeleteProgram(prog)
    gl.DeleteProgram(pres)

    local vaoId = ffi.new("GLuint[1]", vao)
    gl.DeleteVertexArrays(1, vaoId)

    fbf.deallocate_fbo(iir_effect.fbos[1])
    fbf.deallocate_fbo(iir_effect.fbos[2])
    fbf.deallocate_fbo(iir_effect.fbos[3])
end

function iir_effect.resize_fbo(w,h)
    if iir_effect.fbos[1] then fbf.deallocate_fbo(iir_effect.fbos[1]) end
    if iir_effect.fbos[2] then fbf.deallocate_fbo(iir_effect.fbos[2]) end
    if iir_effect.fbos[3] then fbf.deallocate_fbo(iir_effect.fbos[3]) end
    iir_effect.fbos[1] = fbf.allocate_fbo(w,h)
    iir_effect.fbos[2] = fbf.allocate_fbo(w,h)
    iir_effect.fbos[3] = fbf.allocate_fbo(w,h)

    iir_effect.clear_fbos()
end

local function draw_fullscreen_quad()
    gl.BindVertexArray(vao)
    gl.DrawElements(GL.TRIANGLES, 3*2, GL.UNSIGNED_INT, nil)
    gl.BindVertexArray(0)
end

-- Mix the two given textures into the front buffer
function iir_effect.mix_textures(texId1, texId2)
    local fron = iir_effect.getfrontfbo()
    fbf.bind_fbo(fron)
    do
        gl.UseProgram(prog)

        gl.ActiveTexture(GL.TEXTURE0)
        gl.BindTexture(GL.TEXTURE_2D, texId1)
        local tx_loc1 = gl.GetUniformLocation(prog, "tex1")
        gl.Uniform1i(tx_loc1, 0)
        
        gl.ActiveTexture(GL.TEXTURE1)
        gl.BindTexture(GL.TEXTURE_2D, texId2)
        local tx_loc2 = gl.GetUniformLocation(prog, "tex2")
        gl.Uniform1i(tx_loc2, 1)

        local mc_loc = gl.GetUniformLocation(prog, "u_coeff")
        gl.Uniform1f(mc_loc, iir_effect.mix_coeff)

        draw_fullscreen_quad()
        sf.CHECK_GL_ERROR()

        gl.UseProgram(0)
    end
    fbf.unbind_fbo()
end

function iir_effect.bind_fbo()
    local f = iir_effect.fbos[3]
    if f then fbf.bind_fbo(f) end
end

function iir_effect.unbind_fbo()
    fbf.unbind_fbo()
end

function iir_effect.clear_fbos()
    for _,v in pairs(iir_effect.fbos) do
        fbf.bind_fbo(v)
        gl.ClearColor(0,0,0,0)
        gl.Clear(GL.COLOR_BUFFER_BIT + GL.DEPTH_BUFFER_BIT)
        fbf.unbind_fbo()
    end
end

function iir_effect.present()
    -- First, mix new and old images into front buffer
    local f = iir_effect.fbos[3]
    local f2 = iir_effect.getbackfbo()
    iir_effect.mix_textures(f.tex, f2.tex)
    
    -- Then, Present front buffer
    local fron = iir_effect.getfrontfbo()
    gl.UseProgram(pres)

    gl.ActiveTexture(GL.TEXTURE0)
    gl.BindTexture(GL.TEXTURE_2D, fron.tex)
    local tx_loc1 = gl.GetUniformLocation(pres, "tex1")
    gl.Uniform1i(tx_loc1, 0)

    draw_fullscreen_quad()
    sf.CHECK_GL_ERROR()

    gl.UseProgram(0)
end

function iir_effect.getfrontfbo()
    return iir_effect.fbos[iir_effect.pingpong]
end

function iir_effect.getbackfbo()
    local other = 1 + (1-(iir_effect.pingpong-1))
    return iir_effect.fbos[other]
end

-- Swap buffers
function iir_effect.swap()
    iir_effect.pingpong = 1 + iir_effect.pingpong
    if iir_effect.pingpong > 2 then
        iir_effect.pingpong = 1
    end
end

return iir_effect
