-- fs.lua
-- A shadertoy clone: takes the body of a fragment shader as input
-- to buildShader. Draws a fullscreen quad with a the given shader program.
-- Most functionality is offloaded to the fullscreen_shader class.

require "util.fullscreen_shader"
local ffi = require "ffi"
local fbf = require "util.fbofunctions"
local sf = require("util.shaderfunctions2")

shadertoy = {}
shadertoy.__index = shadertoy

function shadertoy.new(...)
    local self = setmetatable({}, shadertoy)
    if self.init ~= nil and type(self.init) == "function" then
        self:init(...)
    end 
    return self
end

-- Input: uv  vec2  screen position in [0,1]
-- Output: fragColor vec4  pixel color 
local frag_body = [[
uniform float time;
void main()
{
    vec2 col = uv;
    col.y = 1.-col.y;
    //col.x *= 3.*sin(100.*col.x);
    col.x *= 2.*tan(30.*col.x);
    fragColor = vec4(col, .5*(sin(7.*time) + 1.), 1.);
}
]]


local basic_vert = [[
#version 300 es

in vec2 vPosition;
out vec2 uv;

void main()
{
    uv = .5*vPosition + vec2(.5);
    gl_Position = vec4(vPosition, 0., 1.);
}
]]

local present_frag = [[
#version 300 es

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

in vec2 uv;
out vec4 fragColor;

uniform sampler2D sTex;

void main()
{
    vec4 tc = texture(sTex, uv);
    fragColor = tc;
}
]]

function shadertoy:resizeFbos(w,h)
    if self.fbo then fbf.deallocate_fbo(self.fbo) end
    self.fbo = fbf.allocate_fbo(w, h, true)
    gl.glBindTexture(GL.GL_TEXTURE_2D, self.fbo.tex)
    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
    gl.glBindTexture(GL.GL_TEXTURE_2D, 0)
end

function shadertoy:initFbos()
    self:resizeFbos(self.w, self.h)
end

function shadertoy:init()
    self.shader = nil
    self.time = 0
    self.src = frag_body
    self.descr = "Shadertoy clone"
    self.shadertype = "fragment"
    self.w = 1024
    self.h = 1024
end

function shadertoy:stateString()
    return self.w..'x'..self.h..' px'
end

function shadertoy:getNewShader()
    return frag_body
end

-- TODO: pass src in as param?
function shadertoy:buildShader(src)
    return self.shader:buildShader(src)
end

function shadertoy:initGL()
    self.shader = FullscreenShader.new(frag_body)
    self.shader:initGL()
    self:initFbos()

    --gl.glDeleteProgram(self.prog_present)
    local err = nil
    self.prog_present, err = sf.make_shader_from_source({
        vsrc = basic_vert,
        fsrc = present_frag,
        })
end

function shadertoy:exitGL()
    self.shader:exitGL()
    if self.fbo then fbf.deallocate_fbo(self.fbo) end
    gl.glDeleteProgram(self.prog_present)
end

function shadertoy:renderEye(model, view, proj)
    -- Save viewport dimensions
    -- TODO: this is very inefficient; store them manually, restore at caller
    local vp = ffi.new("GLuint[4]", 0,0,0,0)
    gl.glGetIntegerv(GL.GL_VIEWPORT, vp)
    -- Save bound FBO
    local boundfbo = ffi.new("int[1]")
    gl.glGetIntegerv(GL.GL_FRAMEBUFFER_BINDING, boundfbo)

        -- Draw quad scene to this scene's first internal fbo
        fbf.bind_fbo(self.fbo)
        gl.glViewport(0,0, self.w, self.h)
        gl.glDisable(GL.GL_DEPTH_TEST)

        local function set_variables(prog)
            local glFloatv = ffi.typeof('GLfloat[?]')
            local um_loc = gl.glGetUniformLocation(prog, "modelMtx")
            local uv_loc = gl.glGetUniformLocation(prog, "viewMtx")
            local up_loc = gl.glGetUniformLocation(prog, "projMtx")
            local uan_loc = gl.glGetUniformLocation(prog, "time")
            gl.glUniformMatrix4fv(um_loc, 1, GL.GL_FALSE, glFloatv(16, model))
            gl.glUniformMatrix4fv(uv_loc, 1, GL.GL_FALSE, glFloatv(16, view))
            gl.glUniformMatrix4fv(up_loc, 1, GL.GL_FALSE, glFloatv(16, proj))
            gl.glUniform1f(uan_loc, self.time)
        end
        self.shader:render(view, proj, set_variables)

        fbf.unbind_fbo()
        gl.glEnable(GL.GL_DEPTH_TEST)

    -- Restore viewport
    gl.glViewport(vp[0],vp[1],vp[2],vp[3])
    -- Restore FBO binding
    if boundfbo[0] ~= 0 then
        gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, boundfbo[0])
    end

    -- Present FBO
    gl.glUseProgram(self.prog_present)

    gl.glActiveTexture(GL.GL_TEXTURE0)
    gl.glBindTexture(GL.GL_TEXTURE_2D, self.fbo.tex)
    local samp_loc = gl.glGetUniformLocation(self.prog_present, "sTex")
    gl.glUniform1i(samp_loc, 0)

    gl.glBindVertexArray(self.shader.vao)
    gl.glDrawArrays(GL.GL_TRIANGLES, 0, 3)
    gl.glBindVertexArray(0)
    gl.glUseProgram(0)
end

function shadertoy:render_for_one_eye(view, proj)
    -- Save viewport dimensions
    -- TODO: this is very inefficient; store them manually, restore at caller
    local vp = ffi.new("GLuint[4]", 0,0,0,0)
    gl.glGetIntegerv(GL.GL_VIEWPORT, vp)
    -- Save bound FBO
    local boundfbo = ffi.new("int[1]")
    gl.glGetIntegerv(GL.GL_FRAMEBUFFER_BINDING, boundfbo)

        -- Draw quad scene to this scene's first internal fbo
        fbf.bind_fbo(self.fbo)
        gl.glViewport(0,0, self.w, self.h)
        gl.glDisable(GL.GL_DEPTH_TEST)

        local function set_variables(prog)
            local glFloatv = ffi.typeof('GLfloat[?]')
            local umv_loc = gl.glGetUniformLocation(prog, "mvmtx")
            local upr_loc = gl.glGetUniformLocation(prog, "prmtx")
            gl.glUniformMatrix4fv(upr_loc, 1, GL.GL_FALSE, glFloatv(16, proj))
            gl.glUniformMatrix4fv(umv_loc, 1, GL.GL_FALSE, glFloatv(16, view))

            local uan_loc = gl.glGetUniformLocation(prog, "time")
            gl.glUniform1f(uan_loc, self.time)
        end
        self.shader:render(view, proj, set_variables)


        fbf.unbind_fbo()
        gl.glEnable(GL.GL_DEPTH_TEST)

    -- Restore viewport
    gl.glViewport(vp[0],vp[1],vp[2],vp[3])
    -- Restore FBO binding
    if boundfbo[0] ~= 0 then
        gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, boundfbo[0])
    end

    -- Present FBO
    gl.glUseProgram(self.prog_present)

    gl.glActiveTexture(GL.GL_TEXTURE0)
    gl.glBindTexture(GL.GL_TEXTURE_2D, self.fbo.tex)
    local samp_loc = gl.glGetUniformLocation(self.prog_present, "sTex")
    gl.glUniform1i(samp_loc, 0)

    gl.glBindVertexArray(self.shader.vao)
    gl.glDrawArrays(GL.GL_TRIANGLES, 0, 3)
    gl.glBindVertexArray(0)
    gl.glUseProgram(0)
end

function shadertoy:timestep(absTime, dt)
    self.time = absTime
end

function shadertoy:scaleBufferSize(k)
    self.w = self.w * k
    self.h = self.h * k
    self:resizeFbos(self.w, self.h)
end

function shadertoy:keypressed(key, scancode, action, mods)
    local func_table = {
        [298] = function (x) self:scaleBufferSize(.5*.5) end, -- F9
        [299] = function (x) self:scaleBufferSize(.5) end, -- F10
        [300] = function (x) self:scaleBufferSize(2) end, -- F11
        [301] = function (x) self:scaleBufferSize(2*2) end, -- F12
    }
    local f = func_table[key]
    if f then f() handled = true end
end

return shadertoy
