/* platform.c
 *
 * Platform
 *
 * Notes: assumes multisampling is always enabled
 *
 */

#include <lauxlib.h>
#include <lualib.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <GL/glew.h>
#include <string.h>
#include "event.h"
#include "gldraw.h"
#include "gltools.h"
#include "luatools.h"
#include "platform.h"
#include "sdltools.h"
#include "window.h"

/* few light arrays */
const GLfloat fNoLight[]     = { 0.0f, 0.0f, 0.0f, 1.0f    };
const GLfloat fLowLight[]    = { 0.25f, 0.25f, 0.25f, 1.0f };
const GLfloat fMedLight[]    = { 0.50f, 0.50f, 0.50f, 1.0f };
const GLfloat fShinyLight[]  = { 0.70f, 0.70f, 0.70f, 1.0f };
const GLfloat fBrightLight[] = { 1.0f, 1.0f, 1.0f, 1.0f    };

/* light values and coordinates */
const GLfloat lightPos[]     = { -10.f, 5.0f, 5.0f, 1.0f   };

/* variables that should be already defined and declared for us by main program */
extern unsigned int xres_w;
extern unsigned int yres_w;
extern unsigned int bpp_w;
extern unsigned int af_w;
extern const char *window_caption;
extern const char *window_icon_path;
extern const unsigned int sdl_video_flags;
extern unsigned int maxfps_w;

void platform_init(struct platform *p)
{
    if (!p)
    {
        fprintf(stderr, "platform: failed to initialize, parameter = NULL\n");
        exit(-1);
    }

    int ret;

    /* Lua */
    /* create new lua state */
    p->L = luaL_newstate();

    /* load lua libraries */
    luaL_openlibs(p->L);

    /* initialize SDL, GLEW, and OpenGL */
    load_config(p);
    p->screen = setup_sdl();
    setup_glew();
    setup_opengl();

    /* add a simple timer / callback function */
    p->timer_id = SDL_AddTimer(5000, sdlTimerCallback, &(p->camera));

    /* apply a custom cursor */
    p->my_cursor = sdlInitCursor(arrow);
    SDL_SetCursor(p->my_cursor);

    /* set the camera to <0,0,0> */
    glframe_reset(&p->camera);

    /* setup and init the event handler */
    struct event_vtbl event_tbl;
    memset(&event_tbl, 0, sizeof(event_tbl));
    event_tbl.keydown = event_keydown;
    ret = event_handler_init(p, &event_tbl);
    if (ret == -1)
    {
        fprintf(stderr, "platform: failed to init event handler\n");
        exit(-1);
    }

    /* init client */
    p->client_init(p->c);
}

/* it's a good idea to destroy things in reverse order */
void platform_destroy(struct platform *p)
{
    /* destroy client */
    p->client_destroy(p->c);

    SDL_FreeSurface(p->screen);
    lua_close(p->L);
}

/* load Lua config file from the disk */
int load_config(struct platform *p)
{
    if (!p)
        return -1;

    if ((luaLoadConfig(p->L, "config.lua") |
        luaFillTablePlatform(p->L, &p->config_table)) != -1)
    {
        /* print what was loaded from config.lua to stdout */
        luaPrintTablePlatform(&p->config_table);

        /* override any globals that config.lua redefines */
        if (p->config_table.xres)
            xres_w = p->config_table.xres;

        if (p->config_table.yres)
            yres_w = p->config_table.yres;

        if (p->config_table.bpp)
            bpp_w = p->config_table.bpp;

        if (p->config_table.af)
            af_w = p->config_table.af;

        if (p->config_table.name)
            window_caption = p->config_table.name;

        if (p->config_table.icon)
            window_icon_path = p->config_table.icon;

        if (p->config_table.maxfps)
            maxfps_w = p->config_table.maxfps;
    }

    return 0;
}

void setup_opengl(void)
{
    /* setup fog */
    glEnable(GL_FOG);
    glFogfv(GL_FOG_COLOR, fLowLight); /* set fog color to match background */
    glFogf(GL_FOG_START, 4.0f);
    glFogf(GL_FOG_END, 20.0f);
    glFogi(GL_FOG_MODE, GL_LINEAR);   /* fog equation */

    glEnable(GL_DEPTH_TEST); /* hidden surface removal, aka Z-buffer */
    glFrontFace(GL_CCW);     /* counter clock-wise polygons face out */
    glEnable(GL_CULL_FACE);  /* do not calculate inside of a pyramid */

    /* enable lighting, primitives now need to define color properties */
    glEnable(GL_LIGHTING);

    /* global illumination, ambient RGBA intensity of the entire scene */
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, fLowLight);

    /* setup and enable light 0 */
    glLightfv(GL_LIGHT0, GL_AMBIENT,  fMedLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  fShinyLight);
    glLightfv(GL_LIGHT0, GL_SPECULAR, fBrightLight);
    glLightfv(GL_LIGHT0, GL_POSITION, lightPos);
    glEnable(GL_LIGHT0);

    /* set material properties to follow all glColor values */
    /* all primitives specified after the glMaterial call are affected by the
     * last values set, until another call to glMaterial is made */
    glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);

    /* enable color tracking, should be called after glColorMaterial */
    glEnable(GL_COLOR_MATERIAL);

    /* set globally, sets specular reflection to a white color,
     * this call follows all subsequent primitives */
    glMaterialfv(GL_FRONT, GL_SPECULAR, fBrightLight);

    /* default (0), by increasing this value you reduce the size and increase
     * the focus of the specular highlight, causing a shiny spot to appear */
    glMateriali(GL_FRONT, GL_SHININESS, 64);

    if (glIsEnabled(GL_MULTISAMPLE) == GL_FALSE)
    {
        fprintf(stderr, "OpenGL error: multisampling cannot be enabled!\n");
        exit(-1);
    }

    /* multisampling for polygons, conflicts with *polygon* anti-aliasing */
    /* enabled by default, that's what man page says, hmmm! */
    glEnable(GL_MULTISAMPLE);

    /* turn off AA forever, it enables blending globally, so depth test goes out
     * of the window, blended objects should be rendered in back to front order,
     * also, following lines are ignored by OpenGL if multisampling is enabled */
#if 0
    /* turn on anti aliasing for points and lines */
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
    glEnable(GL_POINT_SMOOTH);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_LINE_SMOOTH);
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
#endif

#if 0
    /* screws up snowman, this has been replaced by superior multisampling */
    glEnable(GL_POLYGON_SMOOTH);
    glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
#endif

    /* we will keep the blending function and hints though */
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);

    /* anisotropic filtering */
    if (gltQueryExtension("GL_EXT_texture_filter_anisotropic") == GL_TRUE)
    {
        GLint af_amount;
        glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &af_amount);
        printf("platform: anisotropic filtering is supported, with max amount "
               "of: %d\n", af_amount);

        if (af_amount < af_w)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, af_amount);
        else
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, af_w);

        /* see what value was set */
        glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &af_amount);
        printf("platform: set anisotropic filtering to %d\n", af_amount);
    }
    else
        printf("platform: anisotropic filtering is not supported\n");

    /* texture compression */
    if (gltQueryExtension("GL_ARB_texture_compression") == GL_TRUE)
    {
        printf("platform: host gfx device supports texture compression\n");
        /* ToDo */
    }

    /* how OpenGL combines the colors from texels with the color of the underlying
     * geometry is controlled by the texture environment mode */
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

    /* once texture is loaded and enabled, it will applied to every primitive
     * that specifies coordinates, ORLY? */
    glEnable(GL_TEXTURE_2D);

    /* specular highlights for textured items */
    glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
    /* glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_COLOR_SINGLE); */
    /* only if lighting is disabled */
    /* glEnable(GL_COLOR_SUM); */

    /* draw fragments that pass this test, fragments with lower than 0.2f
     * alpha are discarded, this is just for the kicks */
    glAlphaFunc(GL_GREATER, 0.2f);
    glEnable(GL_ALPHA_TEST);

    /* gray background */
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
}

SDL_Surface *setup_sdl_video(int w, int h, int bpp, unsigned int flags)
{
    SDL_Surface *srfc;

    if (!bpp)
    {
        const SDL_VideoInfo* info = NULL;

        /* get some video information. */
        info = SDL_GetVideoInfo();
        if (!info)
        {
            fprintf( stderr, "SDL: video query failed: %s\n", SDL_GetError());
            exit(-1);
        }

        bpp = info->vfmt->BitsPerPixel;
        printf("SDL: bpp was not specified, chose %d bits\n", bpp);
    }

    if (!flags)
        flags = SDL_OPENGL | SDL_RESIZABLE;

    if ((srfc = SDL_SetVideoMode(w, h, bpp, flags)) == NULL)
    {
        fprintf(stderr, "SDL: unable to set video mode: %s\n", SDL_GetError());
        exit(-1);
    }

    return srfc;
}

SDL_Surface *setup_sdl(void)
{
    SDL_Surface *screen;
    const SDL_VideoInfo* info = NULL;

    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) == -1)
    {
        fprintf(stderr, "SDL: unable to init, %s\n", SDL_GetError());
        exit(-1);
    }
    atexit(SDL_Quit);

    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

    SDL_WM_SetCaption(window_caption, NULL);
    SDL_WM_SetIcon(IMG_Load(window_icon_path), NULL);

    /* set video mode */
    screen = setup_sdl_video(xres_w, yres_w, bpp_w, sdl_video_flags);

    /* get some video information. */
    info = SDL_GetVideoInfo();
    if (!info)
    {
        fprintf( stderr, "SDL: video query failed: %s\n", SDL_GetError());
        exit(-1);
    }

    int bpp = info->vfmt->BitsPerPixel;
    printf("SDL: applied %d bits per pixel\n", bpp);

    /* query opengl attributes after SetVideoMode call */
    int fb_red_comp;
    int fb_green_comp;
    int fb_blue_comp;
    int fb_alpha_comp;
    int double_buff;
    int depth_sz;

    SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &fb_red_comp);
    SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &fb_green_comp);
    SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &fb_blue_comp);
    SDL_GL_GetAttribute(SDL_GL_ALPHA_SIZE, &fb_alpha_comp);
    SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &double_buff);
    SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &depth_sz);
    printf("SDL: framebuffer; size of components, red: %d bits, green: %d bits, "
           "blue: %d bits, alpha: %d bits\n"
           "SDL: double-buffering: %s, depth buffer size: %d bits\n",
           fb_red_comp, fb_green_comp, fb_blue_comp, fb_alpha_comp,
           double_buff ? "enabled" : "disabled", depth_sz);

    SDL_EnableUNICODE(1);

    /* SDL doesn't trigger off a ResizeEvent at startup, but as we need this
     * for OpenGL, we do this ourselves */
    SDL_Event resizeEvent;
    resizeEvent.type = SDL_VIDEORESIZE;
    resizeEvent.resize.w = xres_w;
    resizeEvent.resize.h = yres_w;
    SDL_PushEvent(&resizeEvent);

    /* save the surf pointer in platform */
    return screen;
}

void setup_glew(void)
{
    /* initalize glew */
    GLenum glewerr = glewInit();
    if (GLEW_OK != glewerr)
    {
        fprintf(stderr, "GLEW error: %s\n", glewGetErrorString(glewerr));
        exit(-1);
    }
    else
        fprintf(stdout, "GLEW: using version %s\n", glewGetString(GLEW_VERSION));

    /* display OpenGL version */
    GLint major;
    GLint minor;
    glGetIntegerv(GL_MAJOR_VERSION, &major);
    glGetIntegerv(GL_MINOR_VERSION, &minor);
    fprintf(stdout, "GLEW: initialized OpenGL %d.%d\n", major, minor);
}