/// Includes

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

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

/// Constants

constexpr auto width  = 419;
constexpr auto height = 293;
constexpr auto title  = "sdf-winding";

/// 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 circle_program = make_program(vert_source, R"(
        #version 140

        uniform float sharpness;
        uniform vec2  center;
        uniform float radius;
        uniform int   winding;

        out float windings;

        void main()
        {
            vec2  pos = gl_FragCoord.xy;
            float sdf = length(pos - center) - radius;
            windings = winding * 1 / (1 + exp2(sharpness*sdf));
        }
    )");
    auto resolve_program = make_program(vert_source, R"(
        #version 140

        uniform int       view;
        uniform bool      debug;
        uniform float     sharpness;
        uniform float     smoothing;
        uniform sampler2D windings;

        in vec2 tex_coord;

        void main()
        {
            float winding = texture(windings, tex_coord).r;
            if (view == 1) winding = 0.5 + 0.25 * winding;                        // view packed, remapped
            if (view >= 2) winding = clamp(abs(winding), 0, 1);                   // discard sign/sum
            if (view == 3) winding = 0.5 + 0.25 * winding;                        // view flattened, remapped
            if (view >= 4) winding = log2(1 / winding - 1) / sharpness;           // unpack (reversible)
            if (view == 5) winding = winding / 100;                               // view unpacked, expanded
            if (view >= 6) winding = smoothstep(-smoothing, +smoothing, winding); // smooth
            gl_FragColor = vec4(vec3(winding), 1);

            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);
        }
    )");

    /// Texture
    GLuint windings;
    glGenTextures(1, &windings);
    glBindTexture(GL_TEXTURE_2D, windings);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
    glTexImage2D(
        GL_TEXTURE_2D, 0,
        GL_R32F,
        width, height, 0,
        GL_RED, GL_FLOAT, nullptr
    );

    /// Framebuffer
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glFramebufferTexture(
        GL_FRAMEBUFFER,
        GL_COLOR_ATTACHMENT0,
        windings,
        0
    );

    //// Data
    struct Circle
    {
        GLfloat center[2];
        GLfloat radius;
        GLint   winding;
    }
    circles[] =
    {
        {{(249+ 69)/2.0+0.5, height-1-(251+ 71)/2.0+0.5}, (251- 71)/2.0, +1},
        {{(192+133)/2.0+0.5, height-1-(241+182)/2.0+0.5}, (241-182)/2.0, +1},
        {{(138+ 79)/2.0+0.5, height-1-(199+140)/2.0+0.5}, (199-140)/2.0, -1},
        {{(274+173)/2.0+0.5, height-1-(134+ 33)/2.0+0.5}, (134- 33)/2.0, -1},
    };

    //// Main loop
    int     view      = 1;
    bool    debug     = true;
    float   time      = (float)glfwGetTime();
    float   sharpness = 1;
    float   smoothing = 5;
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);
    glBlendEquation(GL_FUNC_ADD);
    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_E))       debug      = true;
        if (glfwGetKey(window, GLFW_KEY_R))       debug      = false;
        if (glfwGetKey(window, GLFW_KEY_H))       sharpness -= 0.5F * dt;
        if (glfwGetKey(window, GLFW_KEY_L))       sharpness += 0.5F * dt;
        if (glfwGetKey(window, GLFW_KEY_J))       smoothing -= 5.0F * dt;
        if (glfwGetKey(window, GLFW_KEY_K))       smoothing += 5.0F * dt;
        sharpness = std::max(0.1F, sharpness);
        smoothing = std::max(0.0F, smoothing);

        ///// Generate smoothed windings
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(circle_program);
        for (auto const & circle : circles)
        {
            uniform("sharpness", sharpness);
            uniform("center",    circle.center);
            uniform("radius",    circle.radius);
            uniform("winding",   circle.winding);
            glDrawArrays(GL_TRIANGLES, 0, 3);
        }

        ///// Resolve
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(resolve_program);
        uniform("view",      view);
        uniform("debug",     debug);
        uniform("sharpness", sharpness);
        uniform("smoothing", smoothing);
        glDrawArrays(GL_TRIANGLES, 0, 3);

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

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