#include <cstdlib>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <string>

#include <SDL.h>
#include <GL/glew.h>

using namespace std::literals::string_literals;

#define WINDOW_WIDTH  640
#define WINDOW_HEIGHT 480

std::string file_to_string(char const *path) {
    std::string fileContent;
    std::string line;
    std::ifstream fileStream(path);
    if(!fileStream.is_open())
        throw "Error opening \""s + path + "\"";
    while(std::getline(fileStream, line)) {
        fileContent += line + "\n";
    }
    fileStream.close();
    return fileContent;
}

GLuint load_shader(std::string const& shaderContent, GLenum type) {
    GLuint shader = glCreateShader(type);
    if(shader == 0)
        return 0;

    char const* shaderContentPtr = shaderContent.c_str();
    glShaderSource(shader, 1, &shaderContentPtr, nullptr);
    glCompileShader(shader);

    GLint result = GL_FALSE;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
    if(result == GL_FALSE) {
        GLint infoLogLength = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
        if(infoLogLength == 0)
            return 0;
        std::unique_ptr<char[]> logBuffer(new char[infoLogLength]);
        glGetShaderInfoLog(shader, infoLogLength, nullptr, logBuffer.get());
        std::cerr << "Error compiling the given shader: " << logBuffer.get() << std::endl;
        return 0;
    }

    return shader;
}

GLuint load_program(char const* fs_path) {
    static std::string const vs_content =
R"END(#version 330 core

#define VERTICES 0

layout(location = VERTICES) in vec2 vpos;
out vec2 fragCoord;

uniform vec2 iResolution;

void main() {
    vec2 tmp = (vpos + 1.0) / 2.0;
    fragCoord = vec2(tmp.x * iResolution.x, tmp.y * iResolution.y);
    gl_Position = vec4(vpos, 0.0, 1.0);
}
)END";
    static std::string  const fs_header =
R"END(#version 330 core

in  vec2 fragCoord;
out vec4 fragColor;

uniform float iGlobalTime;
uniform vec2  iResolution;
uniform int   iFrame;
)END";
    static std::string const fs_footer =
R"END(
void main() {
    mainImage(fragColor, fragCoord);
}
)END";
    std::string fs_content;
    try {
        fs_content = file_to_string(fs_path);
    } catch(std::string& err) {
        std::cerr << err << "\n";
        return 0;
    }
    std::string fragment_shader_content = fs_header + fs_content + fs_footer;
    std::cout << fragment_shader_content << std::endl;
    GLuint vertexShader = load_shader(vs_content, GL_VERTEX_SHADER);
    GLuint fragmentShader = load_shader(fragment_shader_content, GL_FRAGMENT_SHADER);

    if(!vertexShader || !fragmentShader) {
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
        return 0;
    }

    GLuint program = glCreateProgram();
    if(!program) {
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
        return 0;
    }

    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glBindFragDataLocation(program, 0, "fragColor");
    glLinkProgram(program);

    GLint result = GL_FALSE;
    glGetProgramiv(program, GL_LINK_STATUS, &result);
    if(result == GL_FALSE) {
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);

        GLint infoLogLength = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        if(infoLogLength == 0)
            return 0;
        std::unique_ptr<char[]> logBuffer(new char[infoLogLength]);
        glGetProgramInfoLog(program, infoLogLength, nullptr, logBuffer.get());
        std::cerr << "Error linking the given program: " << logBuffer.get() << std::endl;

        glDeleteProgram(program);
        return 0;
    }

    glDetachShader(program, vertexShader);
    glDetachShader(program, fragmentShader);
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return program;
}

int main(int argc, char *argv[]) {
    SDL_Window *window;
    SDL_GLContext context;
    GLenum glew_err;
    GLuint program;
    GLuint vao;
    GLuint vbo;
    GLint iGlobalTime, iResolution, iFrame;
    int curframe = 0;
    bool quit;
    static const GLfloat vertices[] = {
        -1.0f,  1.0f,
        -1.0f, -1.0f,
         1.0f,  1.0f,
         1.0f, -1.0f
    };

    if(argc != 2) {
        std::cerr << "Usage: " << argv[0] << " fragment_shader.glsl\n";
        return EXIT_FAILURE;
    }

    if(SDL_Init(SDL_INIT_VIDEO) != 0) {
        std::cerr << "Error initializing SDL (" << SDL_GetError() << ")\n";
        goto error_a;
    }

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS,
                        SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG | SDL_GL_CONTEXT_DEBUG_FLAG);

    window = SDL_CreateWindow(u8"Shader loader", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                              WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL);
    if(window == nullptr) {
        std::cerr << "Error creating window (" << SDL_GetError() << ")\n";
        goto error_b;
    }

    context = SDL_GL_CreateContext(window);
    if(context == NULL) {
        std::cerr << "Error initializing OpenGL context (" << SDL_GetError() << ")\n";
        goto error_c;
    }

    glewExperimental = GL_TRUE;

    glew_err = glewInit();
    if(glew_err != GLEW_OK) {
        std::cerr << "Error initializing GLEW (" << glewGetErrorString(glew_err) << ")\n";
        goto error_d;
    }

    if(!GLEW_ARB_debug_output) {
        std::cerr << "Error: ARB_debug_output isn't supported\n";
        goto error_d;
    }

    glDebugMessageCallbackARB(
        [](GLenum source, GLenum type, GLuint id, GLenum severity,
           GLsizei length, GLchar const* message, GLvoid const* userParam) -> void {
            std::function<std::string(GLenum source)> source_to_str
                ([](GLenum source) -> std::string {
                    if(source == GL_DEBUG_SOURCE_API_ARB) return "OpenGL"s;
                    else if(source == GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB) return "Window System"s;
                    else if(source == GL_DEBUG_SOURCE_SHADER_COMPILER_ARB) return "Shader Compiler"s;
                    else if(source == GL_DEBUG_SOURCE_THIRD_PARTY_ARB) return "Third Party"s;
                    else if(source == GL_DEBUG_SOURCE_APPLICATION_ARB) return "Application"s;
                    else if(source == GL_DEBUG_SOURCE_OTHER_ARB) return "Other"s;
                    else return "?"s;
                });

            std::function<std::string(GLenum)> type_to_str
                ([](GLenum type) -> std::string {
                    if(type == GL_DEBUG_TYPE_ERROR_ARB) return "Error"s;
                    else if(type == GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB) return "Deprecated Behavior"s;
                    else if(type == GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB) return "Undefined Behavior"s;
                    else if(type == GL_DEBUG_TYPE_PORTABILITY_ARB) return "Portability"s;
                    else if(type == GL_DEBUG_TYPE_PERFORMANCE_ARB) return "Performance"s;
                    else if(type == GL_DEBUG_TYPE_OTHER_ARB) return "Other"s;
                    else return "?"s;
                });

            std::function<std::string(GLenum)> severity_to_str
                ([](GLenum severity) -> std::string {
                    if(severity == GL_DEBUG_SEVERITY_LOW_ARB) return "Low"s;
                    else if(severity == GL_DEBUG_SEVERITY_MEDIUM_ARB) return "Medium"s;
                    else if(severity == GL_DEBUG_SEVERITY_HIGH_ARB) return "High"s;
                    else return "?"s;
                });

            std::cout << "ARB_debug_output: "
                      << "source = " << source_to_str(source) << ", "
                      << "type = " << type_to_str(type) << ", "
                      << "severity = " << severity_to_str(severity) << ", "
                      << "id = " << id << ", "
                      << "message = " << message << std::endl;
        },
        nullptr);

    program = load_program(argv[1]);
    if(program == 0) {
        goto error_d;
    }

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    quit = false;
    while(!quit) {
        SDL_Event event;
        while(SDL_PollEvent(&event)) {
            if(event.type == SDL_QUIT)
                quit = true;
            else if(event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE)
                quit = true;
            else if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_ESCAPE)
                quit = true;
        }

        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(program);

        iGlobalTime = glGetUniformLocation(program, "iGlobalTime");
        glUniform1f(iGlobalTime, (float) SDL_GetTicks() / 1000.0);
        iResolution = glGetUniformLocation(program, "iResolution");
        glUniform2f(iResolution, (GLfloat) WINDOW_WIDTH, (GLfloat) WINDOW_HEIGHT);
        iFrame = glGetUniformLocation(program, "iFrame");
        glUniform1i(iFrame, curframe++);

        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) 0);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glDisableVertexAttribArray(0);
        SDL_GL_SwapWindow(window);
    }

    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);

    SDL_GL_DeleteContext(context);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return EXIT_SUCCESS;

error_d:
    SDL_GL_DeleteContext(context);
error_c:
    SDL_DestroyWindow(window);
error_b:
    SDL_Quit();
error_a:
    return EXIT_FAILURE;
}