#include "raylib.h"
#include "raymath.h"
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"
#include "rlgl.h"

#include <assert.h>

//----------------------------------------------------------------------------------
// Camera
//----------------------------------------------------------------------------------

// Basic Orbit Camera with simple controls
typedef struct {

    Camera3D cam3d;
    float azimuth;
    float altitude;
    float distance;
    Vector3 offset;

} OrbitCamera;

static inline void OrbitCameraInit(OrbitCamera* camera)
{
    memset(&camera->cam3d, 0, sizeof(Camera3D));
    camera->cam3d.position = (Vector3){ 2.0f, 3.0f, 5.0f };
    camera->cam3d.target = (Vector3){ -0.5f, 1.0f, 0.0f };
    camera->cam3d.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera->cam3d.fovy = 45.0f;
    camera->cam3d.projection = CAMERA_PERSPECTIVE;

    camera->azimuth = 0.0f;
    camera->altitude = 0.4f;
    camera->distance = 4.0f;
    camera->offset = Vector3Zero();
}

static inline void OrbitCameraUpdate(
    OrbitCamera* camera,
    Vector3 target,
    float azimuthDelta,
    float altitudeDelta,
    float offsetDeltaX,
    float offsetDeltaY,
    float mouseWheel,
    float dt)
{
    camera->azimuth = camera->azimuth + 1.0f * dt * -azimuthDelta;
    camera->altitude = Clamp(camera->altitude + 1.0f * dt * altitudeDelta, 0.0, 0.4f * PI);
    camera->distance = Clamp(camera->distance +  20.0f * dt * -mouseWheel, 0.1f, 100.0f);
    
    Quaternion rotationAzimuth = QuaternionFromAxisAngle((Vector3){0, 1, 0}, camera->azimuth);
    Vector3 position = Vector3RotateByQuaternion((Vector3){0, 0, camera->distance}, rotationAzimuth);
    Vector3 axis = Vector3Normalize(Vector3CrossProduct(position, (Vector3){0, 1, 0}));

    Quaternion rotationAltitude = QuaternionFromAxisAngle(axis, camera->altitude);

    Vector3 localOffset = (Vector3){ dt * offsetDeltaX, dt * -offsetDeltaY, 0.0f };
    localOffset = Vector3RotateByQuaternion(localOffset, rotationAzimuth);

    camera->offset = Vector3Add(camera->offset, Vector3RotateByQuaternion(localOffset, rotationAltitude));

    Vector3 cameraTarget = Vector3Add(camera->offset, target);
    Vector3 eye = Vector3Add(cameraTarget, Vector3RotateByQuaternion(position, rotationAltitude));

    camera->cam3d.target = cameraTarget;
    camera->cam3d.position = eye;
}

//----------------------------------------------------------------------------------
// Shadow Maps
//----------------------------------------------------------------------------------

typedef struct
{
    Vector3 target;
    Vector3 position;
    Vector3 up;
    double width;
    double height;
    double near;
    double far;
    
} ShadowLight;

RenderTexture2D LoadShadowMap(int width, int height)
{
    RenderTexture2D target = { 0 };
    target.id = rlLoadFramebuffer();
    target.texture.width = width;
    target.texture.height = height;
    assert(target.id);
    
    rlEnableFramebuffer(target.id);

    target.depth.id = rlLoadTextureDepth(width, height, false);
    target.depth.width = width;
    target.depth.height = height;
    target.depth.format = 19;       //DEPTH_COMPONENT_24BIT?
    target.depth.mipmaps = 1;
    rlFramebufferAttach(target.id, target.depth.id, RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_TEXTURE2D, 0);
    assert(rlFramebufferComplete(target.id));

    rlDisableFramebuffer();

    return target;
}

void UnloadShadowMap(RenderTexture2D target)
{
    if (target.id > 0)
    {
        rlUnloadFramebuffer(target.id);
    }
}

void BeginShadowMap(RenderTexture2D target, ShadowLight shadowLight)
{
    BeginTextureMode(target);
    ClearBackground(WHITE);
    
    rlDrawRenderBatchActive();      // Update and draw internal render batch

    rlMatrixMode(RL_PROJECTION);    // Switch to projection matrix
    rlPushMatrix();                 // Save previous matrix, which contains the settings for the 2d ortho projection
    rlLoadIdentity();               // Reset current matrix (projection)

    rlOrtho(
        -shadowLight.width/2, shadowLight.width/2, 
        -shadowLight.height/2, shadowLight.height/2, 
        shadowLight.near, shadowLight.far);

    rlMatrixMode(RL_MODELVIEW);     // Switch back to modelview matrix
    rlLoadIdentity();               // Reset current matrix (modelview)

    // Setup Camera view
    Matrix matView = MatrixLookAt(shadowLight.position, shadowLight.target, shadowLight.up);
    rlMultMatrixf(MatrixToFloat(matView));      // Multiply modelview matrix by view matrix (camera)

    rlEnableDepthTest();            // Enable DEPTH_TEST for 3D    
}

void EndShadowMap()
{
    rlDrawRenderBatchActive();      // Update and draw internal render batch

    rlMatrixMode(RL_PROJECTION);    // Switch to projection matrix
    rlPopMatrix();                  // Restore previous matrix (projection) from matrix stack

    rlMatrixMode(RL_MODELVIEW);     // Switch back to modelview matrix
    rlLoadIdentity();               // Reset current matrix (modelview)

    rlDisableDepthTest();           // Disable DEPTH_TEST for 2D

    EndTextureMode();
}

void SetShaderValueShadowMap(Shader shader, int locIndex, RenderTexture2D target)
{
    if (locIndex > -1)
    {
        rlEnableShader(shader.id);
        int slot = 10; // Can be anything 0 to 15, but 0 will probably be taken up
        rlActiveTextureSlot(slot);
        rlEnableTexture(target.depth.id);
        rlSetUniform(locIndex, &slot, SHADER_UNIFORM_INT, 1);
    }
}

//----------------------------------------------------------------------------------
// GBuffer
//----------------------------------------------------------------------------------

typedef struct
{
    unsigned int id;        // OpenGL framebuffer object id
    Texture color;          // Color buffer attachment texture
    Texture normal;         // Normal buffer attachment texture
    Texture depth;          // Depth buffer attachment texture
    
} GBuffer;

GBuffer LoadGBuffer(int width, int height)
{
    GBuffer target = { 0 };
    target.id = rlLoadFramebuffer();
    assert(target.id);
    
    rlEnableFramebuffer(target.id);

    target.color.id = rlLoadTexture(NULL, width, height, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 1);
    target.color.width = width;
    target.color.height = height;
    target.color.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
    target.color.mipmaps = 1;
    rlFramebufferAttach(target.id, target.color.id, RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_TEXTURE2D, 0);
    
    target.normal.id = rlLoadTexture(NULL, width, height, PIXELFORMAT_UNCOMPRESSED_R16G16B16A16, 1);
    target.normal.width = width;
    target.normal.height = height;
    target.normal.format = PIXELFORMAT_UNCOMPRESSED_R16G16B16A16;
    target.normal.mipmaps = 1;
    rlFramebufferAttach(target.id, target.normal.id, RL_ATTACHMENT_COLOR_CHANNEL1, RL_ATTACHMENT_TEXTURE2D, 0);
    
    target.depth.id = rlLoadTextureDepth(width, height, false);
    target.depth.width = width;
    target.depth.height = height;
    target.depth.format = 19;       //DEPTH_COMPONENT_24BIT?
    target.depth.mipmaps = 1;
    rlFramebufferAttach(target.id, target.depth.id, RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_TEXTURE2D, 0);

    assert(rlFramebufferComplete(target.id));

    rlDisableFramebuffer();

    return target;
}

void UnloadGBuffer(GBuffer target)
{
    if (target.id > 0)
    {
        rlUnloadFramebuffer(target.id);
    }
}

void BeginGBuffer(GBuffer target, Camera3D camera)
{
    rlDrawRenderBatchActive();      // Update and draw internal render batch

    rlEnableFramebuffer(target.id); // Enable render target
    rlActiveDrawBuffers(2);

    // Set viewport and RLGL internal framebuffer size
    rlViewport(0, 0, target.color.width, target.color.height);
    rlSetFramebufferWidth(target.color.width);
    rlSetFramebufferHeight(target.color.height);

    ClearBackground(BLACK);

    rlMatrixMode(RL_PROJECTION);    // Switch to projection matrix
    rlPushMatrix();                 // Save previous matrix, which contains the settings for the 2d ortho projection
    rlLoadIdentity();               // Reset current matrix (projection)

    float aspect = (float)target.color.width/(float)target.color.height;

    // NOTE: zNear and zFar values are important when computing depth buffer values
    if (camera.projection == CAMERA_PERSPECTIVE)
    {
        // Setup perspective projection
        double top = rlGetCullDistanceNear()*tan(camera.fovy*0.5*DEG2RAD);
        double right = top*aspect;

        rlFrustum(-right, right, -top, top, rlGetCullDistanceNear(), rlGetCullDistanceFar());
    }
    else if (camera.projection == CAMERA_ORTHOGRAPHIC)
    {
        // Setup orthographic projection
        double top = camera.fovy/2.0;
        double right = top*aspect;

        rlOrtho(-right, right, -top,top, rlGetCullDistanceNear(), rlGetCullDistanceFar());
    }

    rlMatrixMode(RL_MODELVIEW);     // Switch back to modelview matrix
    rlLoadIdentity();               // Reset current matrix (modelview)

    // Setup Camera view
    Matrix matView = MatrixLookAt(camera.position, camera.target, camera.up);
    rlMultMatrixf(MatrixToFloat(matView));      // Multiply modelview matrix by view matrix (camera)

    rlEnableDepthTest();            // Enable DEPTH_TEST for 3D
}

void EndGBuffer(int windowWidth, int windowHeight)
{
    rlDrawRenderBatchActive();      // Update and draw internal render batch
    
    rlDisableDepthTest();           // Disable DEPTH_TEST for 2D
    rlActiveDrawBuffers(1);
    rlDisableFramebuffer();         // Disable render target (fbo)

    rlMatrixMode(RL_PROJECTION);        // Switch to projection matrix
    rlPopMatrix();                  // Restore previous matrix (projection) from matrix stack
    rlLoadIdentity();                   // Reset current matrix (projection)
    rlOrtho(0, windowWidth, windowHeight, 0, 0.0f, 1.0f);

    rlMatrixMode(RL_MODELVIEW);         // Switch back to modelview matrix
    rlLoadIdentity();                   // Reset current matrix (modelview)
}

//----------------------------------------------------------------------------------
// Geno Character and Animation
//----------------------------------------------------------------------------------

Model LoadGenoModel(const char* fileName)
{
    Model model = { 0 };
    model.transform = MatrixIdentity();
  
    FILE* f = fopen(fileName, "rb");
    if (f == NULL)
    {
        TRACELOG(LOG_ERROR, "MODEL Unable to read skinned model file %s", fileName);
        return model;
    }
    
    model.materialCount = 1;
    model.materials = RL_CALLOC(model.materialCount, sizeof(Mesh));
    model.materials[0] = LoadMaterialDefault();

    model.meshCount = 1;
    model.meshes = RL_CALLOC(model.meshCount, sizeof(Mesh));
    model.meshMaterial = RL_CALLOC(model.meshCount, sizeof(Mesh));
    model.meshMaterial[0] = 0;

    fread(&model.meshes[0].vertexCount, sizeof(int), 1, f);
    fread(&model.meshes[0].triangleCount, sizeof(int), 1, f);
    fread(&model.boneCount, sizeof(int), 1, f);

    model.meshes[0].boneCount = model.boneCount;
    model.meshes[0].vertices = RL_CALLOC(model.meshes[0].vertexCount * 3, sizeof(float));
    model.meshes[0].texcoords = RL_CALLOC(model.meshes[0].vertexCount * 2, sizeof(float));
    model.meshes[0].normals = RL_CALLOC(model.meshes[0].vertexCount * 3, sizeof(float));
    model.meshes[0].boneIds = RL_CALLOC(model.meshes[0].vertexCount * 4, sizeof(unsigned char));
    model.meshes[0].boneWeights = RL_CALLOC(model.meshes[0].vertexCount * 4, sizeof(float));
    model.meshes[0].indices = RL_CALLOC(model.meshes[0].triangleCount * 3, sizeof(unsigned short));
    model.meshes[0].animVertices = RL_CALLOC(model.meshes[0].vertexCount * 3, sizeof(float));
    model.meshes[0].animNormals = RL_CALLOC(model.meshes[0].vertexCount * 3, sizeof(float));
    model.bones =  RL_CALLOC(model.boneCount, sizeof(BoneInfo));
    model.bindPose =  RL_CALLOC(model.boneCount, sizeof(Transform));
    
    fread(model.meshes[0].vertices, sizeof(float), model.meshes[0].vertexCount * 3, f);
    fread(model.meshes[0].texcoords, sizeof(float), model.meshes[0].vertexCount * 2, f);
    fread(model.meshes[0].normals, sizeof(float), model.meshes[0].vertexCount * 3, f);
    fread(model.meshes[0].boneIds, sizeof(unsigned char), model.meshes[0].vertexCount * 4, f);
    fread(model.meshes[0].boneWeights, sizeof(float), model.meshes[0].vertexCount * 4, f);
    fread(model.meshes[0].indices, sizeof(unsigned short), model.meshes[0].triangleCount * 3, f);
    memcpy(model.meshes[0].animVertices, model.meshes[0].vertices, sizeof(float) * model.meshes[0].vertexCount * 3);
    memcpy(model.meshes[0].animNormals, model.meshes[0].normals, sizeof(float) * model.meshes[0].vertexCount * 3);
    fread(model.bones, sizeof(BoneInfo), model.boneCount, f);
    fread(model.bindPose, sizeof(Transform), model.boneCount, f);
    fclose(f);
    
    model.meshes[0].boneMatrices = RL_CALLOC(model.boneCount, sizeof(Matrix));
    for (int i = 0; i < model.boneCount; i++)
    {
        model.meshes[0].boneMatrices[i] = MatrixIdentity();
    }
    
    UploadMesh(&model.meshes[0], true);
    
    return model;
}

ModelAnimation LoadGenoModelAnimation(const char* fileName)
{
    ModelAnimation animation = { 0 };
    
    FILE* f = fopen(fileName, "rb");
    if (f == NULL)
    {
        TRACELOG(LOG_ERROR, "MODEL ANIMATION Unable to read animation file %s", fileName);
        return animation;
    }
    
    fread(&animation.frameCount, sizeof(int), 1, f);
    fread(&animation.boneCount, sizeof(int), 1, f);
    
    animation.bones = RL_CALLOC(animation.boneCount, sizeof(BoneInfo));
    fread(animation.bones, sizeof(BoneInfo), animation.boneCount, f);        
    
    animation.framePoses = RL_CALLOC(animation.frameCount, sizeof(Transform*));
    for (int i = 0; i < animation.frameCount; i++)
    {
        animation.framePoses[i] = RL_CALLOC(animation.boneCount, sizeof(Transform));
        fread(animation.framePoses[i], sizeof(Transform), animation.boneCount, f);        
    }

    fclose(f);
    
    return animation;
}

ModelAnimation LoadEmptyModelAnimation(Model model)
{
    ModelAnimation animation = { 0 };
    animation.frameCount = 1;
    animation.boneCount = model.boneCount;
    
    animation.bones = RL_CALLOC(animation.boneCount, sizeof(BoneInfo));
    memcpy(animation.bones, model.bones, animation.boneCount * sizeof(BoneInfo));
    
    animation.framePoses = RL_CALLOC(animation.frameCount, sizeof(Transform*));
    for (int i = 0; i < animation.frameCount; i++)
    {
        animation.framePoses[i] = RL_CALLOC(animation.boneCount, sizeof(Transform));
        memcpy(animation.framePoses[i], model.bindPose, animation.boneCount * sizeof(Transform));
    }

    return animation;
}

//----------------------------------------------------------------------------------
// Debug Draw
//----------------------------------------------------------------------------------

static inline void DrawTransform(Transform t, float scale)
{
    Matrix rotMatrix = QuaternionToMatrix(t.rotation);
  
    DrawLine3D(
        t.translation,
        Vector3Add(t.translation, (Vector3){ scale * rotMatrix.m0, scale * rotMatrix.m1, scale * rotMatrix.m2 }),
        RED);
        
    DrawLine3D(
        t.translation,
        Vector3Add(t.translation, (Vector3){ scale * rotMatrix.m4, scale * rotMatrix.m5, scale * rotMatrix.m6 }),
        GREEN);
        
    DrawLine3D(
        t.translation,
        Vector3Add(t.translation, (Vector3){ scale * rotMatrix.m8, scale * rotMatrix.m9, scale * rotMatrix.m10 }),
        BLUE);
}

static inline void DrawModelBindPose(Model model, Color color)
{
    for (int i = 0; i < model.boneCount; i++)
    {
        DrawSphereWires(
            model.bindPose[i].translation,
            0.01f,
            4,
            6,
            color);
            
        DrawTransform(model.bindPose[i], 0.1f);

        if (model.bones[i].parent != -1)
        {
            DrawLine3D(
                model.bindPose[i].translation,
                model.bindPose[model.bones[i].parent].translation,
                color);
        }
    }
}

static inline void DrawModelAnimationFrameSkeleton(ModelAnimation animation, int frame, Color color)
{
    for (int i = 0; i < animation.boneCount; i++)
    {
        DrawSphereWires(
            animation.framePoses[frame][i].translation,
            0.01f,
            4,
            6,
            color);

        DrawTransform(animation.framePoses[frame][i], 0.1f);

        if (animation.bones[i].parent != -1)
        {
            DrawLine3D(
                animation.framePoses[frame][i].translation,
                animation.framePoses[frame][animation.bones[i].parent].translation,
                color);
        }
    }
}

//----------------------------------------------------------------------------------
// App
//----------------------------------------------------------------------------------

int main(int argc, char **argv)
{
    // Init Window
    
    const int screenWidth = 1280;
    const int screenHeight = 720;
    
    SetConfigFlags(FLAG_VSYNC_HINT);
    InitWindow(screenWidth, screenHeight, "GenoView");
    SetTargetFPS(60);

    // Shaders
    
    Shader shadowShader = LoadShader("./resources/shadow.vs", "./resources/shadow.fs");
    int shadowShaderLightClipNear = GetShaderLocation(shadowShader, "lightClipNear");
    int shadowShaderLightClipFar = GetShaderLocation(shadowShader, "lightClipFar");
    
    Shader skinnedShadowShader = LoadShader("./resources/skinnedShadow.vs", "./resources/shadow.fs");
    int skinnedShadowShaderLightClipNear = GetShaderLocation(skinnedShadowShader, "lightClipNear");
    int skinnedShadowShaderLightClipFar = GetShaderLocation(skinnedShadowShader, "lightClipFar");
    
    Shader skinnedBasicShader = LoadShader("./resources/skinnedBasic.vs", "./resources/basic.fs");
    int skinnedBasicShaderSpecularity = GetShaderLocation(skinnedBasicShader, "specularity");
    int skinnedBasicShaderGlossiness = GetShaderLocation(skinnedBasicShader, "glossiness");
    int skinnedBasicShaderCamClipNear = GetShaderLocation(skinnedBasicShader, "camClipNear");
    int skinnedBasicShaderCamClipFar = GetShaderLocation(skinnedBasicShader, "camClipFar");

    Shader basicShader = LoadShader("./resources/basic.vs", "./resources/basic.fs");
    int basicShaderSpecularity = GetShaderLocation(basicShader, "specularity");
    int basicShaderGlossiness = GetShaderLocation(basicShader, "glossiness");
    int basicShaderCamClipNear = GetShaderLocation(basicShader, "camClipNear");
    int basicShaderCamClipFar = GetShaderLocation(basicShader, "camClipFar");
    
    Shader lightingShader = LoadShader("./resources/post.vs", "./resources/lighting.fs");
    int lightingShaderGBufferColor = GetShaderLocation(lightingShader, "gbufferColor");
    int lightingShaderGBufferNormal = GetShaderLocation(lightingShader, "gbufferNormal");
    int lightingShaderGBufferDepth = GetShaderLocation(lightingShader, "gbufferDepth");
    int lightingShaderSSAO = GetShaderLocation(lightingShader, "ssao");
    int lightingShaderCamPos = GetShaderLocation(lightingShader, "camPos");
    int lightingShaderCamInvViewProj = GetShaderLocation(lightingShader, "camInvViewProj");
    int lightingShaderLightDir = GetShaderLocation(lightingShader, "lightDir");
    int lightingShaderSunColor = GetShaderLocation(lightingShader, "sunColor");
    int lightingShaderSunStrength = GetShaderLocation(lightingShader, "sunStrength");
    int lightingShaderSkyColor = GetShaderLocation(lightingShader, "skyColor");
    int lightingShaderSkyStrength = GetShaderLocation(lightingShader, "skyStrength");
    int lightingShaderGroundStrength = GetShaderLocation(lightingShader, "groundStrength");
    int lightingShaderAmbientStrength = GetShaderLocation(lightingShader, "ambientStrength");
    int lightingShaderExposure = GetShaderLocation(lightingShader, "exposure");
    int lightingShaderCamClipNear = GetShaderLocation(lightingShader, "camClipNear");
    int lightingShaderCamClipFar = GetShaderLocation(lightingShader, "camClipFar");
    
    Shader ssaoShader = LoadShader("./resources/post.vs", "./resources/ssao.fs");
    int ssaoShaderGBufferNormal = GetShaderLocation(ssaoShader, "gbufferNormal");
    int ssaoShaderGBufferDepth = GetShaderLocation(ssaoShader, "gbufferDepth");
    int ssaoShaderCamView = GetShaderLocation(ssaoShader, "camView");
    int ssaoShaderCamProj = GetShaderLocation(ssaoShader, "camProj");
    int ssaoShaderCamInvProj = GetShaderLocation(ssaoShader, "camInvProj");
    int ssaoShaderCamInvViewProj = GetShaderLocation(ssaoShader, "camInvViewProj");
    int ssaoShaderLightViewProj = GetShaderLocation(ssaoShader, "lightViewProj");
    int ssaoShaderShadowMap = GetShaderLocation(ssaoShader, "shadowMap");
    int ssaoShaderShadowInvResolution = GetShaderLocation(ssaoShader, "shadowInvResolution");
    int ssaoShaderCamClipNear = GetShaderLocation(ssaoShader, "camClipNear");
    int ssaoShaderCamClipFar = GetShaderLocation(ssaoShader, "camClipFar");
    int ssaoShaderLightClipNear = GetShaderLocation(ssaoShader, "lightClipNear");
    int ssaoShaderLightClipFar = GetShaderLocation(ssaoShader, "lightClipFar");
    int ssaoShaderLightDir = GetShaderLocation(ssaoShader, "lightDir");
    
    Shader blurShader = LoadShader("./resources/post.vs", "./resources/blur.fs");
    int blurShaderGBufferNormal = GetShaderLocation(blurShader, "gbufferNormal");
    int blurShaderGBufferDepth = GetShaderLocation(blurShader, "gbufferDepth");
    int blurShaderInputTexture = GetShaderLocation(blurShader, "inputTexture");
    int blurShaderCamInvProj = GetShaderLocation(blurShader, "camInvProj");
    int blurShaderCamClipNear = GetShaderLocation(blurShader, "camClipNear");
    int blurShaderCamClipFar = GetShaderLocation(blurShader, "camClipFar");
    int blurShaderInvTextureResolution = GetShaderLocation(blurShader, "invTextureResolution");
    int blurShaderBlurDirection = GetShaderLocation(blurShader, "blurDirection");

    Shader fxaaShader = LoadShader("./resources/post.vs", "./resources/fxaa.fs");
    int fxaaShaderInputTexture = GetShaderLocation(fxaaShader, "inputTexture");
    int fxaaShaderInvTextureResolution = GetShaderLocation(fxaaShader, "invTextureResolution");
    
    // Objects
    
    Mesh groundMesh = GenMeshPlane(20.0f, 20.0f, 10, 10);
    Model groundModel = LoadModelFromMesh(groundMesh);
    Vector3 groundPosition = (Vector3){ 0.0f, -0.01f, 0.0f };
    
    Model genoModel = LoadGenoModel("./resources/Geno.bin");
    Vector3 genoPosition = (Vector3){ 0.0f, 0.0f, 0.0f };
    
    // Animation
    
    // ModelAnimation testAnimation = LoadGenoModelAnimation("./resources/ground1_subject1.bin");
    //ModelAnimation testAnimation = LoadGenoModelAnimation("./resources/kthstreet_gPO_sFM_cAll_d02_mPO_ch01_atombounce_001.bin");
    ModelAnimation testAnimation = LoadEmptyModelAnimation(genoModel);
    int animationFrame = 0;
    
    assert(testAnimation.boneCount == genoModel.boneCount);
    
    // Camera
    
    OrbitCamera camera;
    OrbitCameraInit(&camera);
    
    // Shadows
    
    Vector3 lightDir = Vector3Normalize((Vector3){ 0.35f, -1.0f, -0.35f });
    
    ShadowLight shadowLight = (ShadowLight){ 0 };
    shadowLight.target = Vector3Zero();
    shadowLight.position = Vector3Scale(lightDir, -5.0f);
    shadowLight.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    shadowLight.width = 5.0f;
    shadowLight.height = 5.0f;
    shadowLight.near = 0.01f;
    shadowLight.far = 10.0f;
    
    int shadowWidth = 1024;
    int shadowHeight = 1024;
    Vector2 shadowInvResolution = (Vector2){ 1.0f / shadowWidth, 1.0f / shadowHeight };
    RenderTexture2D shadowMap = LoadShadowMap(shadowWidth, shadowHeight);    
    
    // GBuffer and Render Textures
    
    GBuffer gbuffer = LoadGBuffer(screenWidth, screenHeight);
    RenderTexture2D lighted = LoadRenderTexture(screenWidth, screenHeight);
    RenderTexture2D ssaoFront = LoadRenderTexture(screenWidth, screenHeight);
    RenderTexture2D ssaoBack = LoadRenderTexture(screenWidth, screenHeight);
    
    // UI
    
    bool drawBoneTransforms = false;
    
    // Go
    
    while (!WindowShouldClose())
    {
        // Animation
        
        animationFrame = (animationFrame + 1) % testAnimation.frameCount;
        UpdateModelAnimationBoneMatrices(genoModel, testAnimation, animationFrame);

        // Shadow Light Tracks Character
        
        Vector3 hipPosition = testAnimation.framePoses[animationFrame][0].translation;
        
        shadowLight.target = (Vector3){ hipPosition.x, 0.0f, hipPosition.z };
        shadowLight.position = Vector3Add(shadowLight.target, Vector3Scale(lightDir, -5.0f));

        // Update Camera
        
        OrbitCameraUpdate(
            &camera,
            (Vector3){ hipPosition.x, 0.75f, hipPosition.z },
            (IsKeyDown(KEY_LEFT_CONTROL) && IsMouseButtonDown(0)) ? GetMouseDelta().x : 0.0f,
            (IsKeyDown(KEY_LEFT_CONTROL) && IsMouseButtonDown(0)) ? GetMouseDelta().y : 0.0f,
            (IsKeyDown(KEY_LEFT_CONTROL) && IsMouseButtonDown(1)) ? GetMouseDelta().x : 0.0f,
            (IsKeyDown(KEY_LEFT_CONTROL) && IsMouseButtonDown(1)) ? GetMouseDelta().y : 0.0f,
            GetMouseWheelMove(),
            GetFrameTime());
        
        // Render
        
        rlDisableColorBlend();
        
        BeginDrawing();
        
        // Render Shadow Maps
        
        BeginShadowMap(shadowMap, shadowLight);  
        
        Matrix lightViewProj = MatrixMultiply(rlGetMatrixModelview(), rlGetMatrixProjection());
        float lightClipNear = rlGetCullDistanceNear();
        float lightClipFar = rlGetCullDistanceFar();
        
        SetShaderValue(shadowShader, shadowShaderLightClipNear, &lightClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(shadowShader, shadowShaderLightClipFar, &lightClipFar, SHADER_UNIFORM_FLOAT);
        SetShaderValue(skinnedShadowShader, skinnedShadowShaderLightClipNear, &lightClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(skinnedShadowShader, skinnedShadowShaderLightClipFar, &lightClipFar, SHADER_UNIFORM_FLOAT);
        
        groundModel.materials[0].shader = shadowShader;
        DrawModel(groundModel, groundPosition, 1.0f, WHITE);
        
        genoModel.materials[0].shader = skinnedShadowShader;
        DrawModel(genoModel, genoPosition, 1.0f, WHITE);
        
        EndShadowMap();
        
        // Render GBuffer
        
        BeginGBuffer(gbuffer, camera.cam3d);
        
        Matrix camView = rlGetMatrixModelview();
        Matrix camProj = rlGetMatrixProjection();
        Matrix camInvProj = MatrixInvert(camProj);
        Matrix camInvViewProj = MatrixInvert(MatrixMultiply(camView, camProj));
        float camClipNear = rlGetCullDistanceNear();
        float camClipFar = rlGetCullDistanceFar();

        float specularity = 0.5f;
        float glossiness = 10.0f;        
        
        SetShaderValue(basicShader, basicShaderSpecularity, &specularity, SHADER_UNIFORM_FLOAT);
        SetShaderValue(basicShader, basicShaderGlossiness, &glossiness, SHADER_UNIFORM_FLOAT);
        SetShaderValue(basicShader, basicShaderCamClipNear, &camClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(basicShader, basicShaderCamClipFar, &camClipFar, SHADER_UNIFORM_FLOAT);
        
        SetShaderValue(skinnedBasicShader, skinnedBasicShaderSpecularity, &specularity, SHADER_UNIFORM_FLOAT);
        SetShaderValue(skinnedBasicShader, skinnedBasicShaderGlossiness, &glossiness, SHADER_UNIFORM_FLOAT);
        SetShaderValue(skinnedBasicShader, skinnedBasicShaderCamClipNear, &camClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(skinnedBasicShader, skinnedBasicShaderCamClipFar, &camClipFar, SHADER_UNIFORM_FLOAT);        
        
        groundModel.materials[0].shader = basicShader;
        DrawModel(groundModel, groundPosition, 1.0f, WHITE);
        
        genoModel.materials[0].shader = skinnedBasicShader;
        DrawModel(genoModel, genoPosition, 1.0f, ORANGE);       
        
        EndGBuffer(screenWidth, screenHeight);
        
        // Render SSAO and Shadows
        
        BeginTextureMode(ssaoFront);
        
        BeginShaderMode(ssaoShader);
        
        SetShaderValueTexture(ssaoShader, ssaoShaderGBufferNormal, gbuffer.normal);
        SetShaderValueTexture(ssaoShader, ssaoShaderGBufferDepth, gbuffer.depth);
        SetShaderValueMatrix(ssaoShader, ssaoShaderCamView, camView);
        SetShaderValueMatrix(ssaoShader, ssaoShaderCamProj, camProj);
        SetShaderValueMatrix(ssaoShader, ssaoShaderCamInvProj, camInvProj);
        SetShaderValueMatrix(ssaoShader, ssaoShaderCamInvViewProj, camInvViewProj);
        SetShaderValueMatrix(ssaoShader, ssaoShaderLightViewProj, lightViewProj);
        SetShaderValueShadowMap(ssaoShader, ssaoShaderShadowMap, shadowMap);
        SetShaderValue(ssaoShader, ssaoShaderShadowInvResolution, &shadowInvResolution, SHADER_UNIFORM_VEC2);
        SetShaderValue(ssaoShader, ssaoShaderCamClipNear, &camClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(ssaoShader, ssaoShaderCamClipFar, &camClipFar, SHADER_UNIFORM_FLOAT);
        SetShaderValue(ssaoShader, ssaoShaderLightClipNear, &lightClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(ssaoShader, ssaoShaderLightClipFar, &lightClipFar, SHADER_UNIFORM_FLOAT);
        SetShaderValue(ssaoShader, ssaoShaderLightDir, &lightDir, SHADER_UNIFORM_VEC3);
        
        ClearBackground(WHITE);
        
        DrawTextureRec(
            ssaoFront.texture,
            (Rectangle){ 0, 0, ssaoFront.texture.width, -ssaoFront.texture.height },
            (Vector2){ 0, 0 },
            WHITE);

        EndShaderMode();

        EndTextureMode();
        
        // Blur Horizontal
        
        BeginTextureMode(ssaoBack);
        
        BeginShaderMode(blurShader);
        
        Vector2 blurDirection = (Vector2){ 1.0f, 0.0f };
        Vector2 blurInvTextureResolution = (Vector2){ 1.0f / ssaoFront.texture.width, 1.0f / ssaoFront.texture.height };
        
        SetShaderValueTexture(blurShader, blurShaderGBufferNormal, gbuffer.normal);
        SetShaderValueTexture(blurShader, blurShaderGBufferDepth, gbuffer.depth);
        SetShaderValueTexture(blurShader, blurShaderInputTexture, ssaoFront.texture);
        SetShaderValueMatrix(blurShader, blurShaderCamInvProj, camInvProj);
        SetShaderValue(blurShader, blurShaderCamClipNear, &camClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(blurShader, blurShaderCamClipFar, &camClipFar, SHADER_UNIFORM_FLOAT);
        SetShaderValue(blurShader, blurShaderInvTextureResolution, &blurInvTextureResolution, SHADER_UNIFORM_VEC2);
        SetShaderValue(blurShader, blurShaderBlurDirection, &blurDirection, SHADER_UNIFORM_VEC2);

        DrawTextureRec(
            ssaoBack.texture,
            (Rectangle){ 0, 0, ssaoBack.texture.width, -ssaoBack.texture.height },
            (Vector2){ 0, 0 },
            WHITE);

        EndShaderMode();

        EndTextureMode();
      
        // Blur Vertical
        
        BeginTextureMode(ssaoFront);
        
        BeginShaderMode(blurShader);
        
        blurDirection = (Vector2){ 0.0f, 1.0f };
        
        SetShaderValueTexture(blurShader, blurShaderInputTexture, ssaoBack.texture);
        SetShaderValue(blurShader, blurShaderBlurDirection, &blurDirection, SHADER_UNIFORM_VEC2);

        DrawTextureRec(
            ssaoFront.texture,
            (Rectangle){ 0, 0, ssaoFront.texture.width, -ssaoFront.texture.height },
            (Vector2){ 0, 0 },
            WHITE);

        EndShaderMode();

        EndTextureMode();
      
        // Light GBuffer
        
        BeginTextureMode(lighted);
        
        BeginShaderMode(lightingShader);
        
        Vector3 sunColor = (Vector3){ 253.0f / 255.0f, 255.0f / 255.0f, 232.0f / 255.0f };
        float sunStrength = 0.25f;
        Vector3 skyColor = (Vector3){ 174.0f / 255.0f, 183.0f / 255.0f, 190.0f / 255.0f };
        float skyStrength = 0.2f;
        float groundStrength = 0.1f;
        float ambientStrength = 1.0f;
        float exposure = 0.9f;
        
        SetShaderValueTexture(lightingShader, lightingShaderGBufferColor, gbuffer.color);
        SetShaderValueTexture(lightingShader, lightingShaderGBufferNormal, gbuffer.normal);
        SetShaderValueTexture(lightingShader, lightingShaderGBufferDepth, gbuffer.depth);
        SetShaderValueTexture(lightingShader, lightingShaderSSAO, ssaoFront.texture);
        SetShaderValue(lightingShader, lightingShaderCamPos, &camera.cam3d.position, SHADER_UNIFORM_VEC3);
        SetShaderValueMatrix(lightingShader, lightingShaderCamInvViewProj, camInvViewProj);
        SetShaderValue(lightingShader, lightingShaderLightDir, &lightDir, SHADER_UNIFORM_VEC3);
        SetShaderValue(lightingShader, lightingShaderSunColor, &sunColor, SHADER_UNIFORM_VEC3);
        SetShaderValue(lightingShader, lightingShaderSunStrength, &sunStrength, SHADER_UNIFORM_FLOAT);
        SetShaderValue(lightingShader, lightingShaderSkyColor, &skyColor, SHADER_UNIFORM_VEC3);
        SetShaderValue(lightingShader, lightingShaderSkyStrength, &skyStrength, SHADER_UNIFORM_FLOAT);
        SetShaderValue(lightingShader, lightingShaderGroundStrength, &groundStrength, SHADER_UNIFORM_FLOAT);
        SetShaderValue(lightingShader, lightingShaderAmbientStrength, &ambientStrength, SHADER_UNIFORM_FLOAT);
        SetShaderValue(lightingShader, lightingShaderExposure, &exposure, SHADER_UNIFORM_FLOAT);
        SetShaderValue(lightingShader, lightingShaderCamClipNear, &camClipNear, SHADER_UNIFORM_FLOAT);
        SetShaderValue(lightingShader, lightingShaderCamClipFar, &camClipFar, SHADER_UNIFORM_FLOAT);
        
        ClearBackground(RAYWHITE);
        
        DrawTextureRec(
            gbuffer.color,
            (Rectangle){ 0, 0, gbuffer.color.width, -gbuffer.color.height },
            (Vector2){ 0, 0 },
            WHITE);
        
        EndShaderMode();        
        
        // Debug Draw
        
        BeginMode3D(camera.cam3d);
        
        if (drawBoneTransforms)
        {
            DrawModelAnimationFrameSkeleton(testAnimation, animationFrame, GRAY);
        }
  
        EndMode3D();

        EndTextureMode();
        
        // Render Final with FXAA
        
        BeginShaderMode(fxaaShader);

        Vector2 fxaaInvTextureResolution = (Vector2){ 1.0f / lighted.texture.width, 1.0f / lighted.texture.height };
        
        SetShaderValueTexture(fxaaShader, fxaaShaderInputTexture, lighted.texture);
        SetShaderValue(fxaaShader, fxaaShaderInvTextureResolution, &fxaaInvTextureResolution, SHADER_UNIFORM_VEC2);
        
        DrawTextureRec(
            lighted.texture,
            (Rectangle){ 0, 0, lighted.texture.width, -lighted.texture.height },
            (Vector2){ 0, 0 },
            WHITE);
        
        EndShaderMode();
  
        // UI
  
        rlEnableColorBlend();
  
        GuiGroupBox((Rectangle){ 20, 10, 190, 180 }, "Camera");

        GuiLabel((Rectangle){ 30, 20, 150, 20 }, "Ctrl + Left Click - Rotate");
        GuiLabel((Rectangle){ 30, 40, 150, 20 }, "Ctrl + Right Click - Pan");
        GuiLabel((Rectangle){ 30, 60, 150, 20 }, "Mouse Scroll - Zoom");
        GuiLabel((Rectangle){ 30, 80, 150, 20 }, TextFormat("Target: [% 5.3f % 5.3f % 5.3f]", camera.cam3d.target.x, camera.cam3d.target.y, camera.cam3d.target.z));
        GuiLabel((Rectangle){ 30, 100, 150, 20 }, TextFormat("Offset: [% 5.3f % 5.3f % 5.3f]", camera.offset.x, camera.offset.y, camera.offset.z));
        GuiLabel((Rectangle){ 30, 120, 150, 20 }, TextFormat("Azimuth: %5.3f", camera.azimuth));
        GuiLabel((Rectangle){ 30, 140, 150, 20 }, TextFormat("Altitude: %5.3f", camera.altitude));
        GuiLabel((Rectangle){ 30, 160, 150, 20 }, TextFormat("Distance: %5.3f", camera.distance));
  
        GuiGroupBox((Rectangle){ screenWidth - 260, 10, 240, 40 }, "Rendering");

        GuiCheckBox((Rectangle){ screenWidth - 250, 20, 20, 20 }, "Draw Transfoms", &drawBoneTransforms);

  
        EndDrawing();
    }

    UnloadRenderTexture(lighted);
    UnloadRenderTexture(ssaoBack);
    UnloadRenderTexture(ssaoFront);
    UnloadRenderTexture(lighted);
    UnloadGBuffer(gbuffer);

    UnloadShadowMap(shadowMap);
    
    UnloadModelAnimation(testAnimation);
    
    UnloadModel(genoModel);
    UnloadModel(groundModel);
    
    UnloadShader(fxaaShader);    
    UnloadShader(blurShader);    
    UnloadShader(ssaoShader);    
    UnloadShader(lightingShader);    
    UnloadShader(basicShader);    
    UnloadShader(skinnedBasicShader);
    UnloadShader(skinnedShadowShader);
    UnloadShader(shadowShader);
        
    CloseWindow();

    return 0;
}