#include <string.h>
#include <sys/time.h>
#include <pthread.h>
#include <pulse/pulseaudio.h>
#include <pulse/simple.h>

#include "game.h"

#define WND_TITLE       "OpenLara"

#define SND_FRAME_SIZE  4
#define SND_DATA_SIZE   (1024 * SND_FRAME_SIZE)

pa_simple *sndOut;
pthread_t sndThread;
pthread_mutex_t sndMutex;

Sound::Frame *sndData;

void* sndFill(void *arg) {
    while (1) {
        pthread_mutex_lock(&sndMutex);
        Sound::fill(sndData, SND_DATA_SIZE / SND_FRAME_SIZE);
        pthread_mutex_unlock(&sndMutex);
        pa_simple_write(sndOut, sndData, SND_DATA_SIZE, NULL);
    }
    return NULL;
}

void sndInit() {
    static const pa_sample_spec spec = {
        .format   = PA_SAMPLE_S16LE,
        .rate     = 44100,
        .channels = 2
    };
    
    static const pa_buffer_attr attr = {
        .maxlength  = SND_DATA_SIZE * 2,
        .tlength    = 0xFFFFFFFF,
        .prebuf     = 0xFFFFFFFF,
        .minreq     = SND_DATA_SIZE,
        .fragsize   = 0xFFFFFFFF,
    };

    pthread_mutex_init(&sndMutex, NULL);

    int error;    
    if (!(sndOut = pa_simple_new(NULL, WND_TITLE, PA_STREAM_PLAYBACK, NULL, "game", &spec, NULL, &attr, &error))) {
        LOG("pa_simple_new() failed: %s\n", pa_strerror(error));
        sndData = NULL;
        return;
    }
    
    sndData = new Sound::Frame[SND_DATA_SIZE / SND_FRAME_SIZE];     
    pthread_create(&sndThread, NULL, sndFill, NULL);  
}

void sndFree() {
    if (sndOut) {
        pthread_cancel(sndThread);    
        pthread_mutex_lock(&sndMutex);
    //    pa_simple_flush(sndOut, NULL);
    //    pa_simple_free(sndOut);
        pthread_mutex_unlock(&sndMutex);
        delete[] sndData;
    }
    pthread_mutex_destroy(&sndMutex);
}

int getTime() {
    timeval t;
    gettimeofday(&t, NULL);
    return (t.tv_sec * 1000 + t.tv_usec / 1000);
}

InputKey keyToInputKey(int code) {
    int codes[] = {
        113, 114, 111, 116, 65, 36, 9, 50, 37, 64,
        19, 10, 11, 12, 13, 14, 15, 16, 17, 18,
        38, 56, 54, 40, 26, 41, 42, 43, 31, 44, 45, 46, 58,
        57, 32, 33, 24, 27, 39, 28, 30, 55, 25, 53, 29, 52,
    };

    for (int i = 0; i < sizeof(codes) / sizeof(codes[0]); i++)
        if (codes[i] == code)
            return (InputKey)(ikLeft + i);
    return ikNone;
}

InputKey mouseToInputKey(int btn) {
    switch (btn) {
        case 1 : return ikMouseL;
        case 2 : return ikMouseM;
        case 3 : return ikMouseR;
    }
    return ikNone;
}

void WndProc(const XEvent &e) {
    switch (e.type) {
        case ConfigureNotify :
            Core::width  = e.xconfigure.width;
            Core::height = e.xconfigure.height;
            break;
        case KeyPress   : 
        case KeyRelease :
            if (e.type == KeyPress && (e.xkey.state & Mod1Mask) && e.xkey.keycode == 36) {
                // TODO: windowed <-> fullscreen switch
                break;
            }
            Input::setDown(keyToInputKey(e.xkey.keycode), e.type == KeyPress);
            break;
        case ButtonPress :
        case ButtonRelease : {
            InputKey key = mouseToInputKey(e.xbutton.button);
            Input::setPos(key, Input::mouse.pos);
            Input::setDown(key, e.type == ButtonPress);
            break;
        }
        case MotionNotify :
            Input::setPos(ikMouseL, vec2((float)e.xmotion.x, (float)e.xmotion.y));
            break;
    }    
}

int main() {
    static int XGLAttr[] = {
        GLX_RGBA,
        GLX_DOUBLEBUFFER,
        GLX_DEPTH_SIZE, 24,
        0
    };

    Display *dpy = XOpenDisplay(NULL);
    XVisualInfo *vis = glXChooseVisual(dpy, XDefaultScreen(dpy), XGLAttr);

    XSetWindowAttributes attr;
    attr.colormap = XCreateColormap(dpy, RootWindow(dpy, vis->screen), vis->visual, AllocNone);
    attr.border_pixel = 0;
    attr.event_mask = KeyPressMask | KeyReleaseMask | StructureNotifyMask |
                      ButtonPressMask | ButtonReleaseMask |
                      ButtonMotionMask | PointerMotionMask;

    Window wnd = XCreateWindow(dpy, RootWindow(dpy, vis->screen),
                               0, 0, 1280, 720, 0, 
                               vis->depth, InputOutput, vis->visual, 
                               CWColormap | CWBorderPixel | CWEventMask, &attr);
    XStoreName(dpy, wnd, WND_TITLE);

    GLXContext ctx = glXCreateContext(dpy, vis, NULL, true);
    glXMakeCurrent(dpy, wnd, ctx);
    XMapWindow(dpy, wnd);

    Atom WM_DELETE_WINDOW = XInternAtom(dpy, "WM_DELETE_WINDOW", 0);
    XSetWMProtocols(dpy, wnd, &WM_DELETE_WINDOW, 1);

    sndInit();
    Game::init();
    
    int lastTime = getTime(), fpsTime = lastTime + 1000, fps = 0;
    
    while (1) {
        if (XPending(dpy)) {
            XEvent event;
            XNextEvent(dpy, &event);
            if (event.type == ClientMessage && *event.xclient.data.l == WM_DELETE_WINDOW)
                break;
            WndProc(event);
        } else {
            int time = getTime();
            if (time <= lastTime)
                continue;

            float delta = (time - lastTime) * 0.001f;
            pthread_mutex_lock(&sndMutex);
            while (delta > EPS) {
                Core::deltaTime = min(delta, 1.0f / 30.0f);
                Game::update();
                delta -= Core::deltaTime;
            }
            pthread_mutex_unlock(&sndMutex);
            lastTime = time;

            Core::stats.dips = 0;
            Core::stats.tris = 0;
            Game::render();
            glXSwapBuffers(dpy, wnd);

            if (fpsTime < getTime()) {
                LOG("FPS: %d DIP: %d TRI: %d\n", fps, Core::stats.dips, Core::stats.tris);
                fps = 0;
                fpsTime = getTime() + 1000;
            } else
                fps++;
        }
    };
    
    sndFree();
    Game::free();

    glXMakeCurrent(dpy, 0, 0);
    XCloseDisplay(dpy);
    return 0;
}
