#ifdef __APPLE__
#include <OpenCL/OpenCL.h>
#include <OpenGL/OpenGL.h>
#elif WIN32
#define NOMINMAX
#include <Windows.h>
#include "GL/glew.h"
#include "GLUT/GLUT.h"
#else
#include <CL/cl.h>
#include <GL/glew.h>
#include <GL/glut.h>
#endif

#include <memory>
#include <chrono>
#include <cassert>
#include <vector>
#include <stdexcept>
#include <sstream>
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <numeric>

#define _USE_MATH_DEFINES
#include <math.h>

#ifdef FR_EMBED_KERNELS
#include "./CL/cache/kernels.h"
#endif

#include "CLW.h"

#include "math/mathutils.h"

#include "tiny_obj_loader.h"
#include "perspective_camera.h"
#include "shader_manager.h"
#include "scene.h"
#include "frrenderer.h"

using namespace FireRays;

// Help message
char const* kHelpMessage =
"App [-p path_to_models][-f model_name][-em emissive multiplier][-ao ao_radius][-w window_width][-h window_height][-nb number_of_indirect_bounces]";
char const* g_path =
"../Resources/CornellBox";
char const* g_modelname = "cornell.obj";

std::unique_ptr<ShaderManager>	g_shader_manager;

GLuint g_vertex_buffer;
GLuint g_index_buffer;
GLuint g_texture;

int g_window_width = 512;
int g_window_height = 512;
int g_ao_enabled = false;
int g_num_bounces = 1;
int g_num_samples = -1;
int g_samplecount = 0;
float g_ao_radius = 1.f;
float g_envmapmul = 1.f;
float3 g_camera_pos = float3(0, 1, 5);
float3 g_camera_at = float3(0, 1, 0);
int g_frame_count = 0;
bool g_benchmark = false;

using namespace tinyobj;

std::unique_ptr<FrRenderer> g_renderer;
Output* g_output = nullptr;
std::unique_ptr<Scene> g_scene;

std::vector<float3> g_data;
std::vector<char> g_imgdata;

#define CAMERA_POSITION float3(0, 0.3f, 0.f)
#define CAMERA_AT float3(1.f, 0.3f, 0.f)
#define CAMERA_UP float3(0, 1, 0)
#define CAMERA_ZCAP float2 (0.00001f, 100000.f)
#define CAMERA_FOVY ((float)M_PI_4 * 1.5f)

static bool     g_is_left_pressed = false;
static bool     g_is_right_pressed = false;
static bool     g_is_fwd_pressed = false;
static bool     g_is_back_pressed = false;
static bool     g_is_mouse_tracking = false;
static float2   g_mouse_pos = float2(0, 0);
static float2   g_mouse_delta = float2(0, 0);


// CLW stuff
CLWContext g_cl_context;

char* GetCmdOption(char ** begin, char ** end, const std::string & option);
bool CmdOptionExists(char** begin, char** end, const std::string& option);
void ShowHelpAndDie();
void SaveFrameBuffer(int framecnt, float3 const* data);

void Display()
{
    try
    {
        {
            glDisable(GL_DEPTH_TEST);
            glViewport(0, 0, g_window_width, g_window_height);

            glClear(GL_COLOR_BUFFER_BIT);

            glBindBuffer(GL_ARRAY_BUFFER, g_vertex_buffer);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_index_buffer);

            GLuint program = g_shader_manager->GetProgram("../App/simple");
            glUseProgram(program);

            GLuint texloc = glGetUniformLocation(program, "g_Texture");
            assert(texloc >= 0);

            glUniform1i(texloc, 0);

            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, g_texture);

            GLuint position_attr = glGetAttribLocation(program, "inPosition");
            GLuint texcoord_attr = glGetAttribLocation(program, "inTexcoord");

            glVertexAttribPointer(position_attr, 3, GL_FLOAT, GL_FALSE, sizeof(float)* 5, 0);
            glVertexAttribPointer(texcoord_attr, 2, GL_FLOAT, GL_FALSE, sizeof(float)* 5, (void*)(sizeof(float)* 3));

            glEnableVertexAttribArray(position_attr);
            glEnableVertexAttribArray(texcoord_attr);

            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, nullptr);

            glDisableVertexAttribArray(texcoord_attr);
            glBindTexture(GL_TEXTURE_2D, 0);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
            glUseProgram(0);
        }

        glutSwapBuffers();
    }
    catch (std::runtime_error& e)
    {
        std::cout << e.what();
        exit(-1);
    }
}


void ResizeBuffers()
{
    if (g_output)
    {
        g_renderer->DeleteOutput(g_output);
    }

    g_output = g_renderer->CreateOutput(g_window_width, g_window_height);

    g_renderer->SetOutput(g_output);

    g_data.resize(g_output->width() * g_output->height());
    g_imgdata.resize(g_output->width() * g_output->height() * 3);
}


void InitGraphics()
{
    g_shader_manager.reset(new ShaderManager());

    glClearColor(0.0, 0.0, 0.0, 0.0);
    glCullFace(GL_NONE);
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);

    glGenBuffers(1, &g_vertex_buffer);
    glGenBuffers(1, &g_index_buffer);

    // create Vertex buffer
    glBindBuffer(GL_ARRAY_BUFFER, g_vertex_buffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_index_buffer);

    float quad_vdata[] =
    {
        -1, -1, 0.5, 0, 0,
        1, -1, 0.5, 1, 0,
        1, 1, 0.5, 1, 1,
        -1, 1, 0.5, 0, 1
    };

    GLshort quad_idata[] =
    {
        0, 1, 3,
        3, 1, 2
    };

    // fill data
    glBufferData(GL_ARRAY_BUFFER, sizeof(quad_vdata), quad_vdata, GL_STATIC_DRAW);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(quad_idata), quad_idata, GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    glGenTextures(1, &g_texture);
    glBindTexture(GL_TEXTURE_2D, g_texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, g_window_width, g_window_height, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);

    glBindTexture(GL_TEXTURE_2D, 0);
}

void InitCl()
{
    std::vector<CLWPlatform> platforms;

    CLWPlatform::CreateAllPlatforms(platforms);

    bool       bAmdContext = false;
    for (int i = 0; i < platforms.size(); ++i)
    {
        if (platforms[i].GetVendor().find("AMD") != std::string::npos ||
            platforms[i].GetVendor().find("Advanced Micro Devices") != std::string::npos)
        {
            g_cl_context = CLWContext::Create(platforms[i].GetDevice(0));
            bAmdContext = true;
        }
    }

    if (!bAmdContext)
    {
        g_cl_context = CLWContext::Create(platforms[0].GetDevice(0));
    }

    g_renderer.reset(new FrRenderer(g_cl_context));
}

void InitData()
{

    rand_init();

    // Load obj file
    std::string basepath = g_path;
    basepath += "/";
    std::string filename = basepath + g_modelname;

    g_scene.reset(Scene::LoadFromObj(filename, basepath));

    g_scene->camera_.reset(new PerspectiveCamera(
        g_camera_pos
        , g_camera_at
        , CAMERA_UP
        , CAMERA_ZCAP
        , CAMERA_FOVY
        , (float)g_window_width / g_window_height
        ));

    g_scene->SetEnvironment("../Resources/Textures/HDR_1.hdr", "", g_envmapmul);

    g_renderer->SetNumBounces(g_num_bounces);

    ResizeBuffers();

    g_renderer->Preprocess(*g_scene);
}

void Reshape(GLint w, GLint h)
{
    // Disable window resize
    glutReshapeWindow(g_window_width, g_window_height);
}

void OnMouseMove(int x, int y)
{
    if (g_is_mouse_tracking)
    {
        g_mouse_delta = float2((float)x, (float)y) - g_mouse_pos;
        g_mouse_pos = float2((float)x, (float)y);
    }
}

void OnMouseButton(int btn, int state, int x, int y)
{
    if (btn == GLUT_LEFT_BUTTON)
    {
        if (state == GLUT_DOWN)
        {
            g_is_mouse_tracking = true;
            g_mouse_pos = float2((float)x, (float)y);
            g_mouse_delta = float2(0, 0);
        }
        else if (state == GLUT_UP && g_is_mouse_tracking)
        {
            g_is_mouse_tracking = true;
            g_mouse_delta = float2(0, 0);
        }
    }
}

void OnKey(int key, int x, int y)
{
    switch (key)
    {
    case GLUT_KEY_UP:
        g_is_fwd_pressed = true;
        break;
    case GLUT_KEY_DOWN:
        g_is_back_pressed = true;
        break;
    case GLUT_KEY_LEFT:
        g_is_left_pressed = true;
        break;
    case GLUT_KEY_RIGHT:
        g_is_right_pressed = true;
    case GLUT_KEY_F1:
        g_mouse_delta = float2(0, 0);
        break;
    case GLUT_KEY_F3:
        g_benchmark = true;
        break;
    default:
        break;
    }
}

void OnKeyUp(int key, int x, int y)
{
    switch (key)
    {
    case GLUT_KEY_UP:
        g_is_fwd_pressed = false;
        break;
    case GLUT_KEY_DOWN:
        g_is_back_pressed = false;
        break;
    case GLUT_KEY_LEFT:
        g_is_left_pressed = false;
        break;
    case GLUT_KEY_RIGHT:
        g_is_right_pressed = false;
        break;
    case GLUT_KEY_PAGE_DOWN:
    {
                               ++g_num_bounces;
                               g_renderer->SetNumBounces(g_num_bounces);
                               g_renderer->Clear(float3(0, 0, 0), *g_output);
                               g_samplecount = 0;
                               break;
    }
    case GLUT_KEY_PAGE_UP:
    {
                             if (g_num_bounces > 1)
                             {
                                 --g_num_bounces;
                                 g_renderer->SetNumBounces(g_num_bounces);
                                 g_renderer->Clear(float3(0, 0, 0), *g_output);
                                 g_samplecount = 0;
                             }
                             break;
    }
    default:
        break;
    }
}

void Update()
{
    static auto prevtime = std::chrono::high_resolution_clock::now();
    auto time = std::chrono::high_resolution_clock::now();
    auto dt = std::chrono::duration_cast<std::chrono::duration<double>>(time - prevtime);
    prevtime = time;

    bool update = false;
    float camrotx = 0.f;
    float camroty = 0.f;

    if (g_benchmark)
    {
        g_renderer->RunBenchmark(*g_scene);
        g_benchmark = false;
        update = true;
    }

    const float kMouseSensitivity = 0.001125f;
    float2 delta = g_mouse_delta * float2(kMouseSensitivity, kMouseSensitivity);
    camrotx = -delta.y;
    camroty = -delta.x;

    if (std::abs(camroty) > 0.001f)
    {
        g_scene->camera_->Rotate(camroty);
        update = true;
    }

    if (std::abs(camrotx) > 0.001f)
    {
        g_scene->camera_->Tilt(camrotx);
        update = true;
    }

    const float kMovementSpeed = 100.25f;
    if (g_is_fwd_pressed)
    {
        g_scene->camera_->MoveForward((float)dt.count() * kMovementSpeed);
        update = true;
    }

    if (g_is_back_pressed)
    {
        g_scene->camera_->MoveForward(-(float)dt.count() * kMovementSpeed);
        update = true;
    }

    if (update)
    {
        if (g_num_samples > -1)
        {
            g_samplecount = 0;
        }

        g_renderer->Clear(float3(0.f, 0.f, 0.f), *g_output);
    }

    if (g_num_samples == -1 || g_samplecount++ < g_num_samples)
    {
        if (g_ao_enabled)
        {
            g_renderer->RenderAmbientOcclusion(*g_scene.get(), g_ao_radius);
        }
        else
        {
            g_renderer->Render(*g_scene.get());
        }

        g_output->GetData(&g_data[0]);

        float gamma = 2.2f;
        for (int i = 0; i < (int)g_data.size(); ++i)
        {
            g_imgdata[3 * i] = (char)clamp(clamp(pow(g_data[i].x / g_data[i].w, 1.f / gamma), 0.f, 1.f) * 255, 0, 255);
            g_imgdata[3 * i + 1] = (char)clamp(clamp(pow(g_data[i].y / g_data[i].w, 1.f / gamma), 0.f, 1.f) * 255, 0, 255);
            g_imgdata[3 * i + 2] = (char)clamp(clamp(pow(g_data[i].z / g_data[i].w, 1.f / gamma), 0.f, 1.f) * 255, 0, 255);
        }
    }

    glActiveTexture(GL_TEXTURE0);

    glBindTexture(GL_TEXTURE_2D, g_texture);

    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, g_output->width(), g_output->height(), GL_RGB, GL_UNSIGNED_BYTE, &g_imgdata[0]);

    glBindTexture(GL_TEXTURE_2D, 0);

    glutPostRedisplay();
}


int main(int argc, char * argv[])
{
    // Command line parsing
    char* path = GetCmdOption(argv, argv + argc, "-p");
    g_path = path ? path : g_path;

    char* modelname = GetCmdOption(argv, argv + argc, "-f");
    g_modelname = modelname ? modelname : g_modelname;

    char* width = GetCmdOption(argv, argv + argc, "-w");
    g_window_width = width ? atoi(width) : g_window_width;

    char* height = GetCmdOption(argv, argv + argc, "-h");
    g_window_height = width ? atoi(height) : g_window_height;

    char* aorays = GetCmdOption(argv, argv + argc, "-ao");
    g_ao_radius = aorays ? (float)atof(aorays) : g_ao_radius;

    char* bounces = GetCmdOption(argv, argv + argc, "-nb");
    g_num_bounces = bounces ? atoi(bounces) : g_num_bounces;

    char* camposx = GetCmdOption(argv, argv + argc, "-cpx");
    g_camera_pos.x = camposx ? (float)atof(camposx) : g_camera_pos.x;

    char* camposy = GetCmdOption(argv, argv + argc, "-cpy");
    g_camera_pos.y = camposy ? (float)atof(camposy) : g_camera_pos.y;

    char* camposz = GetCmdOption(argv, argv + argc, "-cpz");
    g_camera_pos.z = camposz ? (float)atof(camposz) : g_camera_pos.z;

    char* camatx = GetCmdOption(argv, argv + argc, "-tpx");
    g_camera_at.x = camatx ? (float)atof(camatx) : g_camera_at.x;

    char* camaty = GetCmdOption(argv, argv + argc, "-tpy");
    g_camera_at.y = camaty ? (float)atof(camaty) : g_camera_at.y;

    char* camatz = GetCmdOption(argv, argv + argc, "-tpz");
    g_camera_at.z = camatz ? (float)atof(camatz) : g_camera_at.z;

    char* envmapmul = GetCmdOption(argv, argv + argc, "-em");
    g_envmapmul = envmapmul ? (float)atof(envmapmul) : g_envmapmul;

    char* numsamples = GetCmdOption(argv, argv + argc, "-ns");
    g_num_samples = numsamples ? atoi(numsamples) : -1;

    if (aorays)
    {
        g_ao_enabled = true;
    }

    // GLUT Window Initialization:
    glutInit(&argc, (char**)argv);
    glutInitWindowSize(g_window_width, g_window_height);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
    glutCreateWindow("App");

#ifndef __APPLE__
    GLenum err = glewInit();
    if (err != GLEW_OK)
    {
        std::cout << "GLEW initialization failed\n";
        return -1;
    }
#endif

    try
    {
        InitCl();
        InitGraphics();
        InitData();

        // Register callbacks:
        glutDisplayFunc(Display);
        glutReshapeFunc(Reshape);

        glutSpecialFunc(OnKey);
        glutSpecialUpFunc(OnKeyUp);
        glutMouseFunc(OnMouseButton);
        glutMotionFunc(OnMouseMove);
        glutIdleFunc(Update);

        glutMainLoop ();
    }
    catch (std::runtime_error& err)
    {
        std::cout << err.what();
        return -1;
    }

    return 0;
}

char* GetCmdOption(char ** begin, char ** end, const std::string & option)
{
    char ** itr = std::find(begin, end, option);
    if (itr != end && ++itr != end)
    {
        return *itr;
    }
    return 0;
}

bool CmdOptionExists(char** begin, char** end, const std::string& option)
{
    return std::find(begin, end, option) != end;
}

void ShowHelpAndDie()
{
    std::cout << kHelpMessage << "\n";
}


