/// Includes

#include <ctime>
#include <sstream>
#include <fstream>
#include <iostream>
#include <iomanip>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

/// Constants

constexpr auto width  = 640;
constexpr auto height = 480;
constexpr auto title  = "";

/// Helpers

//// Debug
void GLAPIENTRY debug_message_callback(
    GLenum          /* source   */,
    GLenum          /* type     */,
    GLuint          /* id       */,
    GLenum          /* severity */,
    GLsizei         /* length   */,
    GLchar  const *    message,
    void    const * /* param    */
)
{
    std::cerr << message << std::endl;
};

//// Uniform
void uniform(GLint location, int            value    ) { glUniform1i (location,    value); }
void uniform(GLint location, bool           value    ) { glUniform1i (location,    value); }
void uniform(GLint location, float          value    ) { glUniform1f (location,    value); }
void uniform(GLint location, float const (& value)[1]) { glUniform1fv(location, 1, value); }
void uniform(GLint location, float const (& value)[2]) { glUniform2fv(location, 1, value); }
void uniform(GLint location, float const (& value)[3]) { glUniform3fv(location, 1, value); }
void uniform(GLint location, float const (& value)[4]) { glUniform4fv(location, 1, value); }
template<typename T>
void uniform(char const * name, T const & value)
{
    GLuint program;   glGetIntegerv(GL_CURRENT_PROGRAM, (GLint *)&program);
    GLint  location = glGetUniformLocation(program, name);
    uniform(location, value);
}

//// Write
void write_tga(char const * base)
{
    char header[] = {
        0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        (char)(width  >> 0U),
        (char)(width  >> 8U),
        (char)(height >> 0U),
        (char)(height >> 8U),
        4*8, 0,
    };
    char data[4*width*height];
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    glReadPixels(0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, data);
    auto t  = std::time(nullptr);
    auto pt = std::put_time(std::localtime(&t), "%Y-%m-%d_%H-%M-%S");
    std::ofstream((std::ostringstream{} << base << "_" << pt << ".tga").str())
        .write(header, sizeof(header))
        .write(data,   sizeof(data));
}

/// Main

int main(int /*argc*/, char * argv[])
{
    //// Window/context
    glfwInit();
    auto window = glfwCreateWindow(width, height, title, nullptr, nullptr);
    glfwMakeContextCurrent(window);
    glewInit();

    //// Callbacks
    glfwSetWindowUserPointer(window, argv[0]);
    glfwSetKeyCallback(window,
        [](GLFWwindow* w, int key, int /*scancode*/, int action, int /*mods*/)
        {
            auto ptr = glfwGetWindowUserPointer(w);
            if (action != GLFW_RELEASE) return;
            if (key == GLFW_KEY_Q) glfwSetWindowShouldClose(w, GLFW_TRUE);
            if (key == GLFW_KEY_W) write_tga((char *)ptr);
        }
    );

    //// Debug
    glEnable(GL_DEBUG_OUTPUT);
    glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
    glDebugMessageCallback(debug_message_callback, nullptr);

    //// Shader
    auto make_program = [](char const * vert_source, char const * frag_source)
    {
        auto program = glCreateProgram();
        auto vert    = glCreateShader(GL_VERTEX_SHADER);
        auto frag    = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(vert, 1, &vert_source, nullptr);
        glShaderSource(frag, 1, &frag_source, nullptr);
        glCompileShader(vert);
        glCompileShader(frag);
        glAttachShader(program, vert);
        glAttachShader(program, frag);
        glLinkProgram(program);
        return program;
    };
    auto vert_source = R"(
        #version 140

        out vec2 tex_coord;

        void main()
        {
            tex_coord = vec2[](
                vec2(0, 0),
                vec2(2, 0),
                vec2(0, 2)
            )[gl_VertexID];
            gl_Position = vec4(tex_coord*2-1, 0, 1);
        }
    )";
    auto tex_coord_program = make_program(vert_source, R"(
        #version 140

        uniform int   view;
        uniform bool  debug;
        uniform float time;

        in vec2 tex_coord;

        void main()
        {
            gl_FragColor = vec4(fract(tex_coord - float(!debug)*time), 0, 1);

            // Exercise the debugging colors.
            // <https://www.desmos.com/calculator/uijwefecqs>
            if (view >= 2)
            {
                float x = tex_coord.x;
                x -= 0.5;
                x *= 10;
                x  = clamp(x, -3, +3);
                x /= (3 + x) * (3 - x);
                x *= (3 + 1) * (3 - 1);
                x *= 0.5;
                x += 0.5;
                gl_FragColor = vec4(x / (1 / float(tex_coord.y > 0.5)));
            }

            bvec3 debug_color = bvec3(
                any(lessThan   (gl_FragColor, vec4(0))),
                any(greaterThan(gl_FragColor, vec4(1))),
                any(isinf      (gl_FragColor)) ||
                any(isnan      (gl_FragColor))
            );
            if (debug && any(debug_color))
                gl_FragColor = vec4(debug_color, 1);
        }
    )");

    //// Main loop
    int   view  = 1;
    bool  debug = true;
    float time  = (float)glfwGetTime();
    while (glfwPollEvents(), !glfwWindowShouldClose(window))
    {
        ///// Time
        float pt = time;
        float dt = (time = (float)glfwGetTime()) - pt;

        ///// Input
        for (int num = 0; num <= 9; ++num)
        if (glfwGetKey(window, GLFW_KEY_0 + num)) view  = num;
        if (glfwGetKey(window, GLFW_KEY_R))       debug = false;
        if (glfwGetKey(window, GLFW_KEY_E))       debug = true;

        ///// Draw
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(tex_coord_program);
        uniform("view",  view);
        uniform("debug", debug);
        uniform("time",  time);
        uniform("dt",    dt);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        ///// Swap
        glfwSwapBuffers(window);
    }

    //// Cleanup
    glfwDestroyWindow(window);
    glfwTerminate();
}