#include <iostream>

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

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

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

int main()
{
    /// Window/context
    glfwInit();
    auto window = glfwCreateWindow(width, height, title, nullptr, nullptr);
    glfwMakeContextCurrent(window);
    glewInit();

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

    /// Shader
    auto make_program = [](char const * fragment_source)
    {
        auto program = glCreateProgram();
        auto attach  = [&](GLenum type, char const * source)
        {
            auto shader = glCreateShader(type);
            glShaderSource(shader, 1, &source, nullptr);
            glCompileShader(shader);
            glAttachShader(program, shader);
        };
        attach(GL_VERTEX_SHADER, 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);
            }
        )");
        attach(GL_FRAGMENT_SHADER, fragment_source);
        glLinkProgram(program);
        return program;
    };
    auto circle_program = make_program(R"(
        #version 140

        uniform float smoothing;
        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 * smoothstep(
                -smoothing/2,
                +smoothing/2,
                -sdf
            );
        }
    )");
    auto resolve_program = make_program(R"(
        #version 140

        uniform int       view;
        uniform sampler2D windings;

        in vec2 tex_coord;

        void main()
        {
            float winding = texture(windings, tex_coord).r;
            switch (view)
            {
                case 0: gl_FragColor = vec4(0.5 + 0.25 * winding); break;
                case 1: gl_FragColor = vec4(abs(winding));         break;
            }
        }
    )");

    /// 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
    );

    /// Draw loop
    float smoothing = 10;
    int   view      = 0;
    struct Circle
    {
        float center[2];
        float radius;
        int   winding;
    };
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);
    glBlendEquation(GL_FUNC_ADD);
    while (glfwPollEvents(), !glfwWindowShouldClose(window))
    {
        //// Input
        if (glfwGetKey(window, GLFW_KEY_Q))
            glfwSetWindowShouldClose(window, GLFW_TRUE);
        if (glfwGetKey(window, GLFW_KEY_J))
            smoothing += 1;
        if (glfwGetKey(window, GLFW_KEY_K))
            smoothing -= smoothing < 1 ? smoothing : 1;
        if (glfwGetKey(window, GLFW_KEY_H))
            view = 0;
        if (glfwGetKey(window, GLFW_KEY_L))
            view = 1;

        //// Generate smoothed windings
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(circle_program);
        for (auto const & circle : {
            Circle{{(249+ 69)/2.0+0.5, height-1-(251+ 71)/2.0+0.5}, (251- 71)/2.0, +1},
            Circle{{(192+133)/2.0+0.5, height-1-(241+182)/2.0+0.5}, (241-182)/2.0, +1},
            Circle{{(138+ 79)/2.0+0.5, height-1-(199+140)/2.0+0.5}, (199-140)/2.0, -1},
            Circle{{(274+173)/2.0+0.5, height-1-(134+ 33)/2.0+0.5}, (134- 33)/2.0, -1},
        })
        {
            glUniform1fv(glGetUniformLocation(circle_program, "smoothing"), 1, &smoothing);
            glUniform2fv(glGetUniformLocation(circle_program, "center"),    1,  circle.center);
            glUniform1fv(glGetUniformLocation(circle_program, "radius"),    1, &circle.radius);
            glUniform1iv(glGetUniformLocation(circle_program, "winding"),   1, &circle.winding);
            glDrawArrays(GL_TRIANGLES, 0, 3);
        }

        //// Resolve
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(resolve_program);
        glUniform1iv(glGetUniformLocation(resolve_program, "view"), 1, &view);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        //// Swap
        glfwSwapBuffers(window);
    }

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