#ifdef GLSHADER_TEST


#include <iostream>
#include <iomanip>
#include <stdexcept>

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

#include "str.hpp"
#include "shader.hpp"


static auto debugMessageDisable = false;


// Only check the first line, the others are from the driver dependent OpenGL
// info log.
#define EXPECT_EXCEPTION(CODE, WHAT) \
    { \
        std::string what; \
        debugMessageDisable = true; \
        try { \
            CODE; \
        } catch (std::exception const & e) { \
            what = e.what(); \
            what = what.substr(0, what.find("\n")); \
        } \
        debugMessageDisable = false; \
        if (what.empty()) \
            throw std::runtime_error{STR( \
                "Expected exception \"" << WHAT << "\", got none." \
            )}; \
        if (what != WHAT) \
            throw std::runtime_error{STR( \
                "Expected exception \"" << WHAT << "\", got \"" << what \
                << "\"." \
            )}; \
    }


void test() {
    EXPECT_EXCEPTION(
        Shader({ "bad_noextension" }),
        "Failed to infer shader type of 'bad_noextension' of shader program 'bad_noextension'; no file extension."
    )
    EXPECT_EXCEPTION(
        Shader({ "bad.unknownextension" }),
        "Failed to infer shader type of 'bad.unknownextension' of shader program 'bad.unknownextension'; unknown file extension 'unknownextension'."
    )
    EXPECT_EXCEPTION(
        Shader({ "bad_nonexistent.vert" }),
        "Failed to open vert shader 'bad_nonexistent.vert' of shader program 'bad_nonexistent.vert'."
    )
    EXPECT_EXCEPTION(
        Shader({ "bad_compile.vert" }),
        "Failed to compile vert shader 'bad_compile.vert' of shader program 'bad_compile.vert'."
        // "0:4(5): error: main() must return void"
        // "0:4(1): error: function `main' has non-void return type int, but no return statement"
    )
    EXPECT_EXCEPTION(
        Shader({ "bad_link.vert", "good.frag" }),
        "Failed to link shader program 'bad_link.vert', 'good.frag'."
        // "error: vertex shader does not write to `gl_Position'. "
    )
    {
        Shader shader{std::move(Shader({
            "good.vert",
            "good.frag",
        }))};
        shader.use();
        shader = Shader({
            "good.vert",
            "good.frag",
        });
        shader.use();
    }
    Shader shader({
        "good.vert",
        "good.frag",
    });

    Shader shaderMove;
    Shader shaderCopy;

    std::cout << "Moving shader" << std::endl;
    shaderMove = Shader({"good.vert", "good.frag"});
    std::cout << "Moving shader done" << std::endl;

    std::cout << "Copying shader" << std::endl;
    shaderCopy = shaderMove;
    std::cout << "Copying shader done" << std::endl;

    shader.use();
    shader.validate();

    glm::vec4 color(1, 0, 0, 1);
    EXPECT_EXCEPTION(
        Shader({ "good.vert", "good.frag" }).uniform("noncurrent", color),
        "Failed to set uniform 'noncurrent' of shader program 'good.vert', 'good.frag'; program is not current."
    )
    EXPECT_EXCEPTION(
        shader.uniform("nonexistent", color),
        "Failed to get location of uniform 'nonexistent' of shader program 'good.vert', 'good.frag'."
    )
    shader.uniform("color", color);

    struct Matrices {
        glm::mat4 view;
        glm::mat4 projection;
    } matrices;
    EXPECT_EXCEPTION(
        Shader({ "good.vert", "good.frag" }).uniform("noncurrent", matrices),
        "Failed to set uniform block 'noncurrent' of shader program 'good.vert', 'good.frag'; program is not current."
    )
    EXPECT_EXCEPTION(
        shader.uniform("nonexistent", matrices),
        "Failed to get index of uniform block 'nonexistent' in shader program 'good.vert', 'good.frag'."
    )
    // (Allocate), set data and bind UBO implicitly.
    // shader.uniform("matrices", matrices);
    // (Allocate), set data and bind UBO explicitly.
    Shader::uniform_buffer("matrices", matrices);
    // shader.uniform("matrices", "matrices");
    EXPECT_EXCEPTION(
        Shader::uniform_buffer("matrices", matrices, GL_STATIC_DRAW),
        "Failed to set data of uniform buffer 'matrices'; data has usage GL_STATIC_DRAW but buffer has usage GL_DYNAMIC_DRAW."
    )
    EXPECT_EXCEPTION(
        Shader::uniform_buffer("matrices", color),
        "Failed to set data of uniform buffer 'matrices'; data has size 16 but buffer has size 128."
    )
    EXPECT_EXCEPTION(
        Shader::uniform_buffer_delete("nonexistent"),
        "Failed to delete uniform buffer 'nonexistent'; does not exist."
    )
    // Delete UBO.
    // Shader::uniform_buffer_delete("matrices");
}


void errorCallback(int /* error */, char const * description) {
    std::cerr << "GLFW ERROR: " << description << std::endl;
    exit(1);
}


void APIENTRY debugMessageCallback(
    GLenum /* source */,
    GLenum /* type */,
    GLuint /* id */,
    GLenum severity,
    GLsizei /* length */,
    const GLchar *message,
    const void * /* userParam */
) {
    if (debugMessageDisable)
        return;
    std::cerr << "GL DEBUG MESSAGE: " << message << std::endl;
    if (
        severity == GL_DEBUG_SEVERITY_HIGH ||
        severity == GL_DEBUG_SEVERITY_MEDIUM
    )
        exit(1);
}


int main() {
    try {
        glfwInit();
        glfwSetErrorCallback(errorCallback);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
        glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
        auto window = glfwCreateWindow(
            640, 480, "glshader test", nullptr, nullptr
        );
        glfwMakeContextCurrent(window);
        glfwSwapInterval(0);

        glewInit();
        #define GLSHADER_TEST_PRINT(NAME) \
            std::cout << std::setw(24+2) << std::left << #NAME ": " << \
                glGetString(GL_##NAME) << std::endl;
        GLSHADER_TEST_PRINT(VENDOR);
        GLSHADER_TEST_PRINT(RENDERER);
        GLSHADER_TEST_PRINT(VERSION);
        GLSHADER_TEST_PRINT(SHADING_LANGUAGE_VERSION);
        glEnable(GL_DEBUG_OUTPUT);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(
            GL_DONT_CARE,
            GL_DONT_CARE,
            GL_DEBUG_SEVERITY_NOTIFICATION,
            0,
            nullptr,
            GL_FALSE
        );
        glDebugMessageCallback(debugMessageCallback, 0);

        std::cout << "---\n";

        int frame = 0;
        glfwSetTime(0);
        while (glfwGetTime() < 1) {
            ++frame;
            test();

            glBegin(GL_TRIANGLE_STRIP);
            glVertex3f(-0.5, -0.5, 0);
            glVertex3f(-0.5, +0.5, 0);
            glVertex3f(+0.5, +0.5, 0);
            glVertex3f(+0.5, -0.5, 0);
            glVertex3f(-0.5, -0.5, 0);
            glEnd();

            glfwSwapBuffers(window);
            glfwPollEvents();
        }

        std::cout << "---\n";

        std::cout << "Frames run: " << frame << "\n";

        glfwDestroyWindow(window);
        glfwTerminate();
    } catch (std::exception const & e) {
        std::cerr << e.what() << std::endl;
        return 1;
    }
}


#endif // GLSHADER_TEST
