#include "gl3w.h"
#include "GLBuffer.h"
#include "GLProgram.h"
#include "ruby.h"
#include "Window.h"
#include <SDL.h>

#define WIDTH   500
#define HEIGHT  500

enum
{
    PROGRAM_TEXT,
    PROGRAM_BASIC,
    PROGRAM_RECT,
    PROGRAM_COUNT
};

const char * program_names[PROGRAM_COUNT] = {
    "text",
    "basic",
    "rect",
};

typedef struct
{
    SDL_Window * sdl_window;
    GLBufferRef rect_buffer;
    GLuint programs[PROGRAM_COUNT];
} Window;

static VALUE ruby_class;
static VALUE window_instance;

static void Init_SDL()
{
    static bool initialized = false;

    if (initialized)
        return;

    if (SDL_Init(SDL_INIT_VIDEO))
    {
        rb_raise(rb_eRuntimeError, "Failed to initialize SDL");
    }

    atexit(SDL_Quit);

    initialized = true;
}

static void load_gl_buffers(Window * window)
{
    static const GLint rect_coords[4][2] = {
        {0, 0},
        {1, 0},
        {0, 1},
        {1, 1},
    };

    window->rect_buffer = new GLBuffer();
    if (!window->rect_buffer->create(GL_ARRAY_BUFFER, GL_STATIC_DRAW,
                                     &rect_coords, sizeof(rect_coords)))
    {
        rb_raise(rb_eRuntimeError, "Failed to create rectangle VBO");
    }
}

static void Init_OpenGL()
{
    static bool initialized = false;

    if (initialized)
        return;

    if (gl3wInit() != 0)
    {
        SDL_Quit();
        rb_raise(rb_eRuntimeError, "Failed to gl3wInit()");
    }

    if (!gl3wIsSupported(3, 0))
    {
        SDL_Quit();
        rb_raise(rb_eRuntimeError, "OpenGL 3.0 is not supported");
    }

    initialized = true;
}

static void Window_free(void * ptr)
{
    Window * window = (Window *)ptr;

    if (window->sdl_window)
        SDL_DestroyWindow(window->sdl_window);

    delete window;
}

static void resize()
{
    GLint viewport_size[2];
    Window * window;
    Data_Get_Struct(window_instance, Window, window);
    SDL_GetWindowSize(window->sdl_window, &viewport_size[0], &viewport_size[1]);
    glViewport(0, 0, viewport_size[0], viewport_size[1]);
    rb_funcall(window_instance,
               rb_intern("resize"),
               4,
               INT2FIX(0),
               INT2FIX(0),
               INT2FIX(viewport_size[0]),
               INT2FIX(viewport_size[1]));
}

static VALUE Window_new(VALUE klass)
{
    static bool one_created = false;

    if (one_created)
    {
        rb_raise(rb_eRuntimeError, "Only one Window for now");
    }

    Init_SDL();

    SDL_Window * sdl_window =
        SDL_CreateWindow("jes",
                         SDL_WINDOWPOS_UNDEFINED,
                         SDL_WINDOWPOS_UNDEFINED,
                         WIDTH,
                         HEIGHT,
                         SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (sdl_window == NULL)
    {
        rb_raise(rb_eRuntimeError, "Failed to create SDL window");
    }

    Window * window = new Window();
    window->sdl_window = sdl_window;

    (void)SDL_GL_CreateContext(window->sdl_window);

    Init_OpenGL();

    load_gl_buffers(window);

    one_created = true;

    VALUE rv = Data_Wrap_Struct(ruby_class, NULL, Window_free, window);
    rb_obj_call_init(rv, 0, NULL);
    VALUE programs = rb_iv_get(rv, "@programs");
    for (int i = 0; i < PROGRAM_COUNT; i++)
    {
        VALUE program = rb_funcall(programs,
                                   rb_intern("[]"),
                                   1,
                                   ID2SYM(rb_intern(program_names[i])));
        if (RTEST(program))
        {
            window->programs[i] = GLProgram_GetID(program);
        }
        else
        {
            delete window;
            rb_raise(rb_eLoadError,
                     "Error resolving %s program",
                     program_names[i]);
        }
    }

    /* TODO: remove window_instance and map SDL_Window * to Window Ruby object
     * to support multiple windows. */
    window_instance = rv;
    resize();

    return rv;
}

static void draw()
{
    rb_funcall(window_instance, rb_intern("draw"), 0);
}

static VALUE Window_event_loop(VALUE klass)
{
    SDL_Event event;

    while (SDL_WaitEvent(&event))
    {
        if (event.type == SDL_QUIT)
            break;
        else if (event.type == SDL_KEYDOWN)
        {
            if (event.key.keysym.sym == SDLK_ESCAPE)
                break;
        }
        else if (event.type == SDL_WINDOWEVENT)
        {
            switch (event.window.event)
            {
            case SDL_WINDOWEVENT_EXPOSED:
                draw();
                break;
            case SDL_WINDOWEVENT_RESIZED:
                resize();
                draw();
                break;
            }
        }
    }

    return Qnil;
}

static VALUE Window_render(VALUE self)
{
    glClearColor (0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);

    return Qnil;
}

static VALUE Window_render_end(VALUE self)
{
    Window * window;
    Data_Get_Struct(window_instance, Window, window);
    SDL_GL_SwapWindow(window->sdl_window);

    return Qnil;
}

void Window_Init(void)
{
    ruby_class = rb_define_class("Window", rb_cObject);
    rb_define_singleton_method(ruby_class, "new", (VALUE(*)(...))Window_new, 0);
    rb_define_singleton_method(ruby_class, "event_loop", (VALUE(*)(...))Window_event_loop, 0);
    rb_define_method(ruby_class, "render", (VALUE(*)(...))Window_render, 0);
    rb_define_method(ruby_class, "render_end", (VALUE(*)(...))Window_render_end, 0);
}
