--[[
    shadertoy_atparty.lua

]]
shadertoy_scene = {}

local openGL = require("opengl")
local ffi = require("ffi")
local sf = require("util.shaderfunctions")
local bf = require("util.bmfont")
local mm = require("util.matrixmath")
local fsq = require("effect.fullscreen_quad")

-- Types from:
-- https://github.com/nanoant/glua/blob/master/init.lua
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 globalTime = 0
local vao = 0
local prog = 0
local vvbo
local vbos = {}
local win_w = 800
local win_h = 600

shadertoy_scene.fov_scale = 1
shadertoy_scene.cube_up = 0
shadertoy_scene.party_on = 0.3

local basic_vert = [[
#version 410 core

in vec4 vPosition;

void main()
{
    gl_Position = vPosition;
}
]]

local basic_frag = [[
#version 330

uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iGlobalTime; // shader playback time (in seconds)
//uniform vec4 iMouse;  // mouse pixel coords. xy: current (if MLB down), zw: click
vec4 iMouse = vec4(0.);

uniform float u_fovScale;
uniform float u_cubeUp;
uniform float u_partyOn;

out vec4 glFragColor;


float t = 3.*iGlobalTime;
float party = u_partyOn;

// math
const float PI = 3.14159265359;
const float DEG_TO_RAD = PI / 180.0;
mat3 rotationX(float t) {
    float ct=cos(t), st=sin(t);
    return mat3(1., 0.,  0.,  0., ct, -st,  0., st,  ct);}
mat3 rotationY(float t) {
    float ct=cos(t), st=sin(t);
    return mat3(ct, 0., st,  0., 1., 0.,  -st, 0., ct);}
mat3 rotationZ(float t) {
    float ct=cos(t), st=sin(t);
    return mat3(ct, -st, 0.,  st, ct, 0.,  0., 0., 1.);}
mat3 rotationXY(vec2 angle) {
    vec2 c = cos(angle);
    vec2 s = sin(angle);
    return mat3(
        c.y    ,  0.0, -s.y,
        s.y*s.x,  c.x,  c.y*s.x,
        s.y*c.x, -s.x,  c.y*c.x);
}

mat3 getMouseRotMtx()
{
    float f= .05;
    vec2 a = .5*vec2(.3,.2);
    vec2 o = vec2(.2,-.2);
    return rotationXY(-o+a*vec2(sin(f*t), cos(f*t)));
    
    // Use shadertoy mouse uniform
    vec4 m = iMouse;
    vec2 mm = m.xy - abs(m.zw);
    vec2 rv = 0.01*mm;
    mat3 rotmtx = rotationY(rv.x) * rotationX(-rv.y);
    return rotmtx;
}


// libiq

// exponential smooth min (k = 32);
float smine( float a, float b, float k )
{
    float res = exp( -k*a ) + exp( -k*b );
    return -log( res )/k;
}

// polynomial smooth min (k = 0.1);
float smin( float a, float b, float k )
{
    float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 );
    return mix( b, a, h ) - k*h*(1.0-h);
}

////////////// DISTANCE FUNCTIONS
//
// Primitives from http://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
//
float udBox( vec3 p, vec3 b )
{
  return length(max(abs(p)-b,0.0));
}
float sdBox( vec3 p, vec3 b )
{
  vec3 d = abs(p) - b;
  return min(max(d.x,max(d.y,d.z)),0.0) +
         length(max(d,0.0));
}
float udRoundBox( vec3 p, vec3 b, float r )
{
    return length(max(abs(p)-b,0.0))-r;
}
float sdPlane( vec3 p, vec4 n )
{
    // n must be normalized
    return dot(p,n.xyz) + n.w;
}

//
// Composites
//
float rollprism(vec3 pos)
{
    return max(
        udRoundBox(pos, vec3(2.,1.4,5.), 3.5),
        -sdPlane(pos, vec4(normalize(vec3(-1.,0.,0.)),3.))
        );
}

float rollprism2(vec3 pos)
{
    return max(
        udRoundBox(pos, vec3(2.,2.,5.), 7.5),
        -udRoundBox(pos, vec3(2.,2.,15.), 5.5)
        );
}

float atsign(vec3 pos)
{
    float d = min(
        rollprism(pos-vec3(1.,0.,0.)),
        rollprism2(pos)
        );
    d = smin(d,
             sdBox(pos-vec3(4.2,-3.5,0.), vec3(3.5,1.4,5.))
            ,1.);    
    d = max(d, -sdBox(pos-vec3(6.,-8.4,0.), vec3(6.,3.5,12.))); // chop horiz
    
    // chop off front and back
    float w = 0.;//1.+sin(t);
    d = max(d, -sdPlane(pos                 , vec4(vec3(0.,0.,-1.),0.)));
    d = max(d, -sdPlane(pos-vec3(0.,0.,-1.5-w), vec4(vec3(0.,0.,1.),0.)));
    return d;
}

float attail(vec3 pos)
{
    float s = -0.+0.5*pow((1.+0.1*pos.x),1.7);
    return sdBox(pos, vec3(5.,0.8+s,.6+s));
}

float at(vec3 pos)
{
    // dance
    float p = 0.02*party;
    pos = rotationY(p*pos.y*sin(3.*t)) * pos;
    pos = rotationX(p*pos.y*sin(5.*t)) * pos;
    
    return min(
        atsign(pos),
        attail(
            rotationY(-.03*(pos.x+7.)) *
            (pos-vec3(3.,-8.5,.0)))
        );
}

float DE_atlogo( vec3 pos )
{
    mat3 rotmtx = getMouseRotMtx();
    pos = rotmtx * pos;
    float d2 = 9999.;
    return min(d2, at(pos));
}


//
// lighting and shading
//
vec3 shading( vec3 v, vec3 n, vec3 eye ) {
    float shininess = 16.0;
    vec3 ev = normalize( v - eye );
    vec3 ref_ev = reflect( ev, n ); 
    vec3 final = vec3( 0.0 );
    // disco light
    {
        vec3 light_pos   = vec3( 1.0, 10.0, 30.0 );
        float p = party;
        vec3 light_color = vec3(p*sin(3.*t), p*sin(3.3*t), p*sin(4.*t));
        vec3 vl = normalize( light_pos - v );
    
        float diffuse  = 0.;//max( 0.0, dot( vl, n ) );
        float specular = max( 0.0, dot( vl, ref_ev ) );
        specular = pow( specular, shininess );
        
        final += light_color * ( diffuse + specular ); 
    }
    
    // white light
    {
        vec3 light_pos   = vec3( 10.0, 20.0, -10.0 );
        //vec3 light_pos   = vec3( -20.0, -20.0, -20.0 );
        vec3 light_color = vec3( 1.);//0.3, 0.7, 1.0 );
        vec3 vl = normalize( light_pos - v );
    
        shininess = 8.;
        float diffuse  = 0.;//max( 0.0, dot( vl, n ) );
        float specular = max( 0.0, dot( vl, ref_ev ) );
        specular = pow( specular, shininess );
        
        final += light_color * ( diffuse + specular ); 
    }

    return final;
}

// get gradient in the world
vec3 gradient( vec3 pos ) {
    const float grad_step = 0.31;
    const vec3 dx = vec3( grad_step, 0.0, 0.0 );
    const vec3 dy = vec3( 0.0, grad_step, 0.0 );
    const vec3 dz = vec3( 0.0, 0.0, grad_step );
    return normalize (
        vec3(
            DE_atlogo( pos + dx ) - DE_atlogo( pos - dx ),
            DE_atlogo( pos + dy ) - DE_atlogo( pos - dy ),
            DE_atlogo( pos + dz ) - DE_atlogo( pos - dz )   
        )
    );
}

// ray marching
float ray_marching( vec3 origin, vec3 dir, float start, float end ) {
    const int max_iterations = 255;
    const float stop_threshold = 0.001;
    float depth = start;
    for ( int i = 0; i < max_iterations; i++ ) {
        float dist = DE_atlogo( origin + dir * depth );
        if ( dist < stop_threshold ) {
            return depth;
        }
        depth += dist;
        if ( depth >= end) {
            return end;
        }
    }
    return end;
}

// get ray direction from pixel position
vec3 ray_dir( float fov, vec2 size, vec2 pos ) {
    vec2 xy = pos - size * 0.5;

    float cot_half_fov = tan( ( 90.0 - fov * 0.5 ) * DEG_TO_RAD );  
    float z = size.y * 0.5 * cot_half_fov;
    
    return normalize( vec3( xy, -z ) );
}


vec3 getSceneColor_atlogo( in vec3 ro, in vec3 rd )
{
    const float clip_far = 100.0;
    float depth = ray_marching( ro, rd, -10.0, clip_far );
    if ( depth >= clip_far ) {
        return vec3(1.);
    }
    vec3 pos = ro + rd * depth;
    vec3 n = gradient( pos );
    return shading( pos, n, ro );
}



//
// The cube
//
mat3 getCubeMtx()
{
    return rotationY(.8+party*pow(abs(sin(PI*2.*iGlobalTime)),5.)) * rotationX(.3);
}

float DE_cube( vec3 pos )
{
    pos = getCubeMtx() * pos;
    pos.y -= u_cubeUp;
    return udRoundBox(pos, vec3(1.5), .15);
}

vec3 shade_cube(vec3 v, vec3 n, vec3 ntx, vec3 eye) {
    vec3 ev = normalize(v - eye);
    vec3 final = vec3(0.);
    vec3 light_pos = vec3(-10.,20.,40.);
    vec3 vl = normalize(light_pos - v);
    float diffuse = max(0.0, dot( vl, n ));
    final += 1.3 * diffuse; 
    // transform normals with the cube to find flat faces/edges
    float px = abs(dot(ntx,vec3(1.,0.,0.)));
    float py = abs(dot(ntx,vec3(0.,1.,0.)));
    float pz = abs(dot(ntx,vec3(0.,0.,1.)));
    float p = max(px,max(py,pz));
    final *= smoothstep(0.9,1.,length(p));
    return final;
}

vec3 grad_cube(vec3 pos) {
    const float gs = 0.02;
    const vec3 dx = vec3(gs, 0., 0.);
    const vec3 dy = vec3(0., gs, 0.);
    const vec3 dz = vec3(0., 0., gs);
    return normalize( vec3(
            DE_cube(pos + dx) - DE_cube(pos - dx),
            DE_cube(pos + dy) - DE_cube(pos - dy),
            DE_cube(pos + dz) - DE_cube(pos - dz)   
        ));
}

float raymarch_cube(vec3 origin, vec3 dir, float start, float end) {
    const int max_iterations = 64;
    const float stop_threshold = 0.01;
    float depth = start;
    for (int i=0; i<max_iterations; i++) {
        float dist = DE_cube(origin + dir*depth);
        if (dist < stop_threshold) return depth;
        depth += dist;
        if (depth >= end) return end;
    }
    return end;
}

vec3 getCubeColor(in vec3 ro, in vec3 rd) {
    const float clip_far = 100.0;
    float depth = raymarch_cube(ro, rd, 0., clip_far);
    if ( depth >= clip_far )
        return getSceneColor_atlogo(ro,rd);
    vec3 pos = ro + rd * depth;
    vec3 ne = grad_cube(pos);
    vec3 ntx = getCubeMtx() * ne;
    return shade_cube(pos, ne, ntx, ro);
}

vec3 getSceneColor(in vec3 ro, in vec3 rd)
{
    return getCubeColor(ro,rd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // default ray dir/origin
    vec3 dir = ray_dir( u_fovScale*45.0, iResolution.xy, fragCoord.xy );
    vec3 eye = vec3( 0.0, 0.0, 30.0 );  
    fragColor = vec4( getSceneColor( eye, dir ), 1.0 );
}


void main()
{
    vec4 fragcol = vec4(0.);
    mainImage(fragcol, gl_FragCoord.xy);
    if (length(fragcol.xyz) < .01)
        discard;
    glFragColor = fragcol;
}
]]

function shadertoy_scene.initGL()
    local vaoId = ffi.new("int[1]")
    gl.GenVertexArrays(1, vaoId)
    vao = vaoId[0]
    gl.BindVertexArray(vao)

    sf.CHECK_GL_ERROR()
    prog = sf.make_shader_from_source({
        vsrc = basic_vert,
        fsrc = basic_frag,
        })
    vbos = {}

    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)

    sf.CHECK_GL_ERROR()
    gl.EnableVertexAttribArray(vpos_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()

    gl.BindVertexArray(0)
end

function shadertoy_scene.exitGL()
    for k,v in pairs(vbos) do
        print(k,v[0])
        gl.DeleteBuffers(1,v)
    end
    vbos = nil
    gl.DeleteProgram(prog)

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

function shadertoy_scene.render_for_one_eye(mview, proj)
    gl.UseProgram(prog)
    local ures_loc = gl.GetUniformLocation(prog, "iResolution")
    gl.Uniform3f(ures_loc, win_w, win_h, 0)
    local utim_loc = gl.GetUniformLocation(prog, "iGlobalTime")
    gl.Uniform1f(utim_loc, globalTime)

    -- Curtain effect: Zoom in as we go, slide cube up to reveal black field
    local fovs = shadertoy_scene.fov_scale
    local cup = shadertoy_scene.cube_up
    local par = shadertoy_scene.party_on

    local ufov_loc = gl.GetUniformLocation(prog, "u_fovScale")
    gl.Uniform1f(ufov_loc, fovs)
    local ucup_loc = gl.GetUniformLocation(prog, "u_cubeUp")
    gl.Uniform1f(ucup_loc, cup)
    local upon_loc = gl.GetUniformLocation(prog, "u_partyOn")
    gl.Uniform1f(upon_loc, par)

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

    sf.CHECK_GL_ERROR()

    gl.UseProgram(0)
end

function shadertoy_scene.timestep(absTime, dt)
    globalTime = absTime
end

function shadertoy_scene.resize_window(w, h)
    win_w = w
    win_h = h
end

return shadertoy_scene
