
#include <stdlib.h>                 /* exit() */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <math.h>                   /* fabs(), tan() */
#include <GL/gl.h>
#include <GL/glu.h>
#include "SDL.h"

#include <lua.hpp>
#include <iostream>
#include <sstream>
#include <string>

#include "ag.h"
#include "anaglym.h"
#include "Engine.h"
#include "AV.h"
#include "PhyObj/PhyObj.h"
#include "sdl_keymap.h"
#include "cfs.h"
#include "glslUtil/glslUtil.h"

using namespace std;

#define AG_EVENT_PREFIX "__ag_event_"
#define EVENT_PRESENT_FLAG(event) m_event_ ## event ## _present
#define EVENT_HANDLER_AG_NAME(event) AG_EVENT_PREFIX #event
#define checkForFunction(lua_name, event) \
    checkForFunctionFull(lua_name, #event, EVENT_PRESENT_FLAG(event))
#define doRegisterHandler(index, event) \
    doRegisterHandlerFull(index, EVENT_HANDLER_AG_NAME(event), \
            EVENT_PRESENT_FLAG(event))
#define doClearHandler(event) \
    do { EVENT_PRESENT_FLAG(event) = false; } while(0)

#define HOOK_TIMEOUT    10000u
#define HOOK_STEPS      250000000
#define FONT_NAME       "FreeSans.ttf"

#ifdef DEBUG_GL_ERROR
#define checkGLError() checkGLErrorLine(__FUNCTION__, __LINE__)
static void checkGLErrorLine(const char * function, int line)
{
    GLenum err = glGetError();
    if (err != 0)
    {
        std::cerr << "gl error in " << function
            << ": " << err << " (0x" << std::hex << err << ") at line "
            << std::dec << line << std::endl;
    }
}
#else
#define checkGLError()
#endif

#define FP_EQ(a,b) (fabsf(a - b) < 0.0001)

/* Global data */
Engine * g_engine;

/* static helper functions */

static void debug_hook(lua_State * L, lua_Debug * debug)
{
    g_engine->debug_hook(debug);
}

static void normalize(dVector3 vec)
{
    float len = vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2];
    if (! FP_EQ(len, 1.0))          /* normalize if necessary */
    {
        len = sqrtf(len);
        vec[0] /= len;
        vec[1] /= len;
        vec[2] /= len;
    }
}

static vector<string> split(const string & str, char delim)
{
    vector<string> ret;
    ssize_t begin = 0, len = str.length();
    size_t pos;
    while ((pos = str.find(delim)) != string::npos
            && begin < len)
    {
        ret.push_back(string(str, begin, pos - begin));
        begin = pos + 1;
    }
    if (begin < len)
    {
        ret.push_back(string(str, begin));
    }
    return ret;
}

static bool loadWFObjFile(const char *fname, WFObj::Buffer & buff)
{
    return g_engine->loadWFObjFile(fname, buff);
}

static GLuint loadTexture(const char *fname)
{
    return g_engine->loadTexture(fname);
}


/******** Engine functions ********/

Engine::Engine(const string & path, AV & av)
    : m_av(av)
{
    m_eye[0] = 0;
    m_eye[1] = -1;
    m_eye[2] = 0;
    m_center[0] = 0;
    m_center[1] = 0;
    m_center[2] = 0;
    m_up[0] = 0;
    m_up[1] = 0;
    m_up[2] = 1;
    m_drawing = false;
    m_autoPhysics = true;
    m_autoStartFrame = true;
    m_autoEndFrame = true;
    m_autoDrawObjects = true;
    m_event_time = 0;
    m_font = NULL;
    m_engine_cursor_visible = m_av.getCursorVisible();
    m_script_cursor_visible = false;
    m_input_grabbed = m_av.getInputGrabbed();
    m_screen_dist = (m_av.getHeight() / 2.0) / tan(30.0 * M_PI / 180.0);

    size_t pos = path.find_last_of("\\/");
    m_engine_path = (pos != string::npos) ? string(path, 0, pos) : ".";

    /* setup SDL user event structures */
    m_updateEvent.type = SDL_USEREVENT;
    m_updateEvent.user.code = 0;
    m_exitEvent.type = SDL_USEREVENT;
    m_exitEvent.user.code = 1;

    m_event_init_present = false;
    m_event_reinit_present = false;
    m_event_update_present = false;
    m_event_update_overlay_present = false;
    m_event_key_down_present = false;
    m_event_key_up_present = false;
    m_event_mousebutton_down_present = false;
    m_event_mousebutton_up_present = false;
    m_event_mouse_motion_present = false;

    m_luaState = lua_open();

    registerLibraries();

    const char *obj_v_src = (const char *) getFile("shaders/obj.vp", NULL);
    const char *obj_f_src = (const char *) getFile("shaders/obj.fp", NULL);
    const static guAttribBinding obj_bindings[] = {
        {ATTRIBUTE_OBJ_POS, "pos"},
        {ATTRIBUTE_OBJ_NORMAL, "normal"},
        {0, NULL}
    };
    if (obj_v_src != NULL && obj_f_src != NULL)
    {
        m_programs[PROG_OBJ] = guMakeProgramFromSource(obj_v_src, obj_f_src,
                obj_bindings);
    }
    else
    {
        m_programs[PROG_OBJ] = 0;
        cerr << "Error: Could not obtain 'obj' shader sources" << endl;
    }
    const char *obj_tex_v_src =
        (const char *) getFile("shaders/obj_tex.vp", NULL);
    const char *obj_tex_f_src =
        (const char *) getFile("shaders/obj_tex.fp", NULL);
    const static guAttribBinding obj_tex_bindings[] = {
        {ATTRIBUTE_OBJ_TEX_POS, "pos"},
        {ATTRIBUTE_OBJ_TEX_NORMAL, "normal"},
        {ATTRIBUTE_OBJ_TEX_TEX_COORD, "tex_coord"},
        {0, NULL}
    };
    if (obj_tex_v_src != NULL && obj_tex_f_src != NULL)
    {
        m_programs[PROG_OBJ_TEX] = guMakeProgramFromSource(obj_tex_v_src,
                obj_tex_f_src, obj_tex_bindings);
    }
    else
    {
        m_programs[PROG_OBJ_TEX] = 0;
        cerr << "Error: Could not obtain 'obj_tex' shader sources" << endl;
    }
}

Engine::~Engine()
{
    lua_close(m_luaState);
    for (std::map<int, Object *>::iterator it = m_objects.begin();
         it != m_objects.end();
         it++)
    {
        delete it->second;
    }
    if (m_font != NULL)
        delete m_font;
}

bool Engine::load(const char * program)
{
    m_program_path = program;
    m_program_directory = program;
    size_t pos = m_program_directory.find_last_of("/\\");
    m_program_directory = (pos != string::npos)
                        ? m_program_directory.substr(0, pos)
                        : ".";

    string path = locateResource(FONT_NAME);
    if (path == "")
    {
        cerr << "Couldn't locate " FONT_NAME << endl;
        return false;
    }
    else
    {
        m_font = new FTBufferFont(path.c_str());
        //m_font = new FTTextureFont(path.c_str());
        if (m_font->Error() != 0)
        {
            cerr << "Error loading font '" << path << "'" << endl;
        }
    }

    int s = luaL_loadfile(m_luaState, program);

    if (s == 0)
    {
        // execute Lua program
        lua_sethook(m_luaState, ::debug_hook, LUA_MASKCOUNT, HOOK_STEPS);
        s = lua_pcall(m_luaState, 0, 0, 0);
    }
    else
    {
        reportErrors(s);
        cerr << "Error loading '" << program << "'" << endl;
        cerr << "Exiting." << endl;
        return false;
    }

    if (s != 0)
    {
        reportErrors(s);
        return false;
    }

    checkForAllHandlerFunctions();

    if (m_event_init_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(init));
        /* call the init function - pops the function ref from the stack */
        int s = lua_pcall(m_luaState, 0, 0, 0);
        reportErrors(s);
    }

    return true;
}

void Engine::checkForAllHandlerFunctions()
{
    checkForFunction("init_event", init);
    checkForFunction("reinit_event", reinit);
    checkForFunction("update_event", update);
    checkForFunction("update_overlay_event", update_overlay);
    checkForFunction("key_down_event", key_down);
    checkForFunction("key_up_event", key_up);
    checkForFunction("mousebutton_down_event", mousebutton_down);
    checkForFunction("mousebutton_up_event", mousebutton_up);
    checkForFunction("mouse_motion_event", mouse_motion);
}

bool Engine::validatePath(const string & path)
{
    if (path[0] == '/')
        return false;
    vector<string> parts = split(path, '/');
    for (int i = 0, sz = parts.size(); i < sz; i++)
    {
        if (parts[i].find_first_not_of(FILENAME_SAFE_CHARS) != string::npos)
            return false;
        if (parts[i].find_first_not_of(".") == string::npos)
            return false;
    }
    return true;
}

void Engine::reportErrors(int status)
{
    if (status != 0)
    {
        cerr << "Engine: Error: " << lua_tostring(m_luaState, -1) << endl;
        lua_pop(m_luaState, 1); // remove error message
    }
}

void Engine::registerLibraries()
{
    /* Load the Lua string library */
    lua_pushcfunction(m_luaState, luaopen_string);
    lua_pcall(m_luaState, 0, 0, 0);

    /* Load the Lua math library */
    lua_pushcfunction(m_luaState, luaopen_math);
    lua_pcall(m_luaState, 0, 0, 0);

    /* Load the Lua table library */
    lua_pushcfunction(m_luaState, luaopen_table);
    lua_pcall(m_luaState, 0, 0, 0);

    ag::register_functions(m_luaState);
}

string Engine::locateResource(const string & shortname)
{
    string try_path;

    /* look for the resource relative to the loaded script's directory */
    try_path = m_program_directory + "/" + shortname;
    if (fileExists(try_path))
        return try_path;

    /* next look for the resource in the engine's library directory */
    try_path = m_engine_path + "/lib/" + shortname;
    if (fileExists(try_path))
        return try_path;

    return "";
}

bool Engine::fileExists(const string & path)
{
    struct stat st;
    if (stat(path.c_str(), &st) == 0)
    {
        return S_ISREG(st.st_mode) && (st.st_size > 0);
    }
    return false;
}

int Engine::addObject(WFObj * obj, bool is_static, bool is_reference,
        bool enable_blending, float scale)
{
    Object * o = new Object(is_static, is_reference, enable_blending,
            m_world, *this, obj, scale);
    int id = m_objects.add(o);
    o->setID(id);
    return id;
}

int Engine::addObject(bool is_static, bool is_reference, bool enable_blending,
        OdeWorld::GeomType geom_type, refptr< vector<float> > args)
{
    Object * o = new Object(is_static, is_reference, enable_blending,
            m_world, *this, geom_type, args);
    int id = m_objects.add(o);
    o->setID(id);
    return id;
}

int Engine::addQuad(float cx, float cy, float cz,
                float v1x, float v1y, float v1z,
                float v2x, float v2y, float v2z)
{
    Quad * q = new Quad(cx, cy, cz, v1x, v1y, v1z, v2x, v2y, v2z);
    int id = m_quads.add(q);
    return id;
}

int Engine::addSound(refptr<AV::Sound> avs)
{
    return m_sounds.add(avs);
}

int Engine::addAMotor(Object * o1, Object * o2)
{
    dBodyID b1 = 0;
    dBodyID b2 = 0;
    if (o1 != NULL)
        b1 = o1->getBody();
    if (o2 != NULL)
        b2 = o2->getBody();
    dJointID jid = m_world.createAMotor(b1, b2);
    return m_joints.add(jid);
}

int Engine::addHinge(Object * o1, Object * o2,
        dReal anchor_x, dReal anchor_y, dReal anchor_z,
        dReal axis_x, dReal axis_y, dReal axis_z)
{
    dBodyID b1 = 0;
    dBodyID b2 = 0;
    if (o1 != NULL)
        b1 = o1->getBody();
    if (o2 != NULL)
        b2 = o2->getBody();
    dJointID jid = m_world.createHinge(b1, b2,
            anchor_x, anchor_y, anchor_z,
            axis_x, axis_y, axis_z);
    return m_joints.add(jid);
}

void Engine::setAMotorAxis(int jid, int anum, int rel,
        dReal x, dReal y, dReal z)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorAxis(m_joints[jid], anum, rel, x, y, z);
    }
}

void Engine::setAMotorNumAxes(int jid, int num_axes)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorNumAxes(m_joints[jid], num_axes);
    }
}

void Engine::setAMotorAngle(int jid, int anum, dReal val)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorAngle(m_joints[jid], anum, val);
    }
}

void Engine::setAMotorLoStop(int jid, dReal val)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorLoStop(m_joints[jid], val);
    }
}

void Engine::setAMotorHiStop(int jid, dReal val)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorHiStop(m_joints[jid], val);
    }
}

void Engine::setAMotorVel(int jid, dReal val)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorVel(m_joints[jid], val);
    }
}

void Engine::setAMotorFMax(int jid, dReal val)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorFMax(m_joints[jid], val);
    }
}

void Engine::setAMotorBounce(int jid, dReal val)
{
    if (m_joints.contains(jid))
    {
        m_world.setAMotorBounce(m_joints[jid], val);
    }
}

void Engine::removeObject(int id)
{
    Object * obj = getObject(id);
    if (obj != NULL)
    {
        m_objects.erase(id);
        delete obj;
    }
}

int Engine::cloneObject(const Engine::Object * obj)
{
    Object * o = new Object(*obj);
    int id = m_objects.add(o);
    o->setID(id);
    return id;
}

Engine::Object * Engine::getObject(int id)
{
    return m_objects.contains(id) ? m_objects[id] : NULL;
}

Engine::Quad * Engine::getQuad(int id)
{
    return m_quads.contains(id) ? m_quads[id] : NULL;
}

refptr<AV::Sound> Engine::getSound(int id)
{
    return m_sounds.contains(id) ? m_sounds[id] : NULL;
}

void Engine::startFrame()
{
    checkGLError();
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(m_eye[0], m_eye[1], m_eye[2],
              m_center[0], m_center[1], m_center[2],
              m_up[0], m_up[1], m_up[2]);
    checkGLError();
}

void Engine::endFrame()
{
    SDL_GL_SwapBuffers();
}

int Engine::setCamera(lua_State * L)
{
    int argc = lua_gettop(L);

    vector<double> args;
    for (int i = 1; i <= argc; i++)
    {
        int type = lua_type(L, i);
        if (type == LUA_TNUMBER || type == LUA_TSTRING)
            args.push_back(lua_tonumber(L, i));
        else
            args.push_back(0);
    }

    if (argc >= 3)
    {
        m_eye[0] = args[0];
        m_eye[1] = args[1];
        m_eye[2] = args[2];
    }
    if (argc >= 6)
    {
        m_center[0] = args[3];
        m_center[1] = args[4];
        m_center[2] = args[5];
    }
    if (argc >= 9)
    {
        m_up[0] = args[6];
        m_up[1] = args[7];
        m_up[2] = args[8];
    }

    return 0;
}

int Engine::getCamera(lua_State * L)
{
    lua_pushnumber(L, m_eye[0]);
    lua_pushnumber(L, m_eye[1]);
    lua_pushnumber(L, m_eye[2]);
    lua_pushnumber(L, m_center[0]);
    lua_pushnumber(L, m_center[1]);
    lua_pushnumber(L, m_center[2]);
    lua_pushnumber(L, m_up[0]);
    lua_pushnumber(L, m_up[1]);
    lua_pushnumber(L, m_up[2]);
    return 9;
}

int Engine::registerEventHandler(lua_State * L)
{
    int argc = lua_gettop(L);
    if (argc == 2 && lua_isstring(L, 1) && lua_isfunction(L, 2))
    {
        string event = lua_tostring(L, 1);
        if      (event == "update")
            doRegisterHandler(2, update);
        else if (event == "update_overlay")
            doRegisterHandler(2, update_overlay);
        else if (event == "key_down")
            doRegisterHandler(2, key_down);
        else if (event == "key_up")
            doRegisterHandler(2, key_up);
        else if (event == "mousebutton_down")
            doRegisterHandler(2, mousebutton_down);
        else if (event == "mousebutton_up")
            doRegisterHandler(2, mousebutton_up);
        else if (event == "mouse_motion")
            doRegisterHandler(2, mouse_motion);
    }
    return 0;
}

int Engine::clearEventHandler(lua_State * L)
{
    int argc = lua_gettop(L);
    if (argc == 1 && lua_isstring(L, 1))
    {
        string event = lua_tostring(L, 1);
        if      (event == "update")
            doClearHandler(update);
        else if (event == "update_overlay")
            doClearHandler(update_overlay);
        else if (event == "key_down")
            doClearHandler(key_down);
        else if (event == "key_up")
            doClearHandler(key_up);
        else if (event == "mousebutton_down")
            doClearHandler(mousebutton_down);
        else if (event == "mousebutton_up")
            doClearHandler(mousebutton_up);
        else if (event == "mouse_motion")
            doClearHandler(mouse_motion);
    }
    return 0;
}

int Engine::loadModel(const string & name, bool is_static, bool is_reference,
        bool enable_blending, float scale)
{
    string path = name;
    if (validatePath(path))
    {
        string model_path = path + ".obj";
        string phys_path = path + ".phy";

        WFObj * obj = new WFObj();

        if (obj->load(model_path.c_str(), &::loadWFObjFile, &::loadTexture))
        {
            int id = addObject(obj, is_static, is_reference, enable_blending,
                    scale);
            Engine::Object * engine_obj = getObject(id);
            if (engine_obj != NULL)
            {
                unsigned int size;
                const unsigned char *phy_data =
                    loadFile(phys_path.c_str(), &size);
                engine_obj->loadPhy(phy_data, size);
                free((void *) phy_data);
            }
            return id;
        }
        else
        {
            cerr << "error loading object '" << name << "'" << endl;
        }
        delete obj;
    }
    return 0;
}

int Engine::loadSound(const string & name)
{
    if (validatePath(name))
    {
        string nm = locateResource(name.c_str());

        refptr<AV::Sound> avs = m_av.createSound();

        unsigned int size;
        const unsigned char *data = loadFile(nm.c_str(), &size);
        if (data != NULL)
        {
            bool loaded = avs->load(data, size);
            delete[] data;
            if (loaded)
                return addSound(avs);
        }
        cerr << "error loading sound '" << name << "'" << endl;
    }
    return 0;
}

bool Engine::isKeyDown(const std::string & key)
{
    return m_keysDown.find(key) != m_keysDown.end();
}

void Engine::exit()
{
    SDL_PushEvent(&m_exitEvent);
}

bool Engine::import(const char * name)
{
    string nm(name);
    if (validatePath(nm))
    {
        string file_path = locateResource(nm + ".lua");
        if (file_path != "")
        {
            return importFullPath(file_path.c_str());
        }
    }
    return false;
}

bool Engine::importFullPath(const char * path)
{
    int s = luaL_loadfile(m_luaState, path);
    if (s == 0)
    {
        s = lua_pcall(m_luaState, 0, 0, 0);
        if (s == 0)
            return true;
        reportErrors(s);
    }
    else
    {
        cerr << "Error " << s << " loading '" << path << "'" << endl;
    }
    return false;
}

GLuint Engine::startList()
{
    GLuint list = glGenLists(1);
    glNewList(list, GL_COMPILE);
    return list;
}

void Engine::endList()
{
    glEndList();
}

void Engine::callList(GLuint list)
{
    glCallList(list);
}

void Engine::clearWorld()
{
    while (!m_objects.empty())
    {
        removeObject(m_objects.begin()->first);
    }
}

refptr< vector<Engine::PickedObjectRef> > Engine::pickObjects(int x, int y)
{
    dMatrix3 r;
    dVector3 right, forward, up;
    dVector3 rotated_direction, initial_direction;
    up[0] = m_up[0];
    up[1] = m_up[1];
    up[2] = m_up[2];
    forward[0] = m_center[0] - m_eye[0];
    forward[1] = m_center[1] - m_eye[1];
    forward[2] = m_center[2] - m_eye[2];
    dCROSS(right, =, forward, up);
    dRFrom2Axes(r, right[0], right[1], right[2],
            forward[0], forward[1], forward[2]);
    initial_direction[0] = x - m_av.getWidth() / 2;
    initial_direction[1] = m_screen_dist;
    initial_direction[2] = y - m_av.getHeight() / 2;
    dMultiply0(rotated_direction, r, initial_direction, 3, 3, 1);
    normalize(rotated_direction);

    refptr< vector<OdeWorld::PickedObjectRef> > objects =
        m_world.pickObjects(m_eye[0], m_eye[1], m_eye[2],
            rotated_direction[0], rotated_direction[1], rotated_direction[2]);

    refptr< vector<PickedObjectRef> > ret = new vector<PickedObjectRef>();
    for (vector<OdeWorld::PickedObjectRef>::const_iterator it
            = objects->begin();
         it != objects->end();
         it++)
    {
        ret->push_back(new PickedObject(*it));
    }
    return ret;
}

Engine::PickedObjectRef Engine::pickOne(int x, int y, Engine::Object *obj)
{
    dMatrix3 r;
    dVector3 right, forward, up;
    dVector3 rotated_direction, initial_direction;
    up[0] = m_up[0];
    up[1] = m_up[1];
    up[2] = m_up[2];
    forward[0] = m_center[0] - m_eye[0];
    forward[1] = m_center[1] - m_eye[1];
    forward[2] = m_center[2] - m_eye[2];
    dCROSS(right, =, forward, up);
    dRFrom2Axes(r, right[0], right[1], right[2],
            forward[0], forward[1], forward[2]);
    initial_direction[0] = x - m_av.getWidth() / 2;
    initial_direction[1] = m_screen_dist;
    initial_direction[2] = y - m_av.getHeight() / 2;
    dMultiply0(rotated_direction, r, initial_direction, 3, 3, 1);
    normalize(rotated_direction);

    OdeWorld::PickedObjectRef owpor =
        m_world.pickOne(m_eye[0], m_eye[1], m_eye[2],
            rotated_direction[0], rotated_direction[1], rotated_direction[2],
            obj->getOdeWorldObject());

    PickedObjectRef ret;

    if (!owpor.isNull())
    {
        ret = new PickedObject(owpor);
    }
    return ret;
}

void Engine::debug_hook(lua_Debug * debug)
{
    Uint32 ticks = SDL_GetTicks();
    if (ticks - m_event_time > HOOK_TIMEOUT)
    {
        ::exit(3);
    }
}

void Engine::getScreenSize(int * width, int * height)
{
    *width = m_av.getWidth();
    *height = m_av.getHeight();
}

void Engine::drawText(const char * text, GLfloat r, GLfloat g, GLfloat b,
        int ptsize, float x, float y)
{
    checkGLError();
    m_font->FaceSize(ptsize);
    glPushAttrib(GL_ENABLE_BIT | GL_TEXTURE_BIT);
        glEnable(GL_TEXTURE_2D);
        glDisable(GL_LIGHTING);
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
        glPushMatrix();
            glTranslatef(x, y, 0);
            glColor3f(r, g, b);
            m_font->Render(text);
        glPopMatrix();
    glPopAttrib();
    checkGLError();
}

void Engine::getTextSize(const char * text, int ptsize,
        float * width, float * height)
{
    m_font->FaceSize(ptsize);
    FTBBox box = m_font->BBox(text);
    *width = box.Upper().Xf() - box.Lower().Xf();
    *height = box.Upper().Yf() - box.Lower().Yf();
}

void Engine::drawLine(float r, float g, float b,
        float x1, float y1, float x2, float y2, float width)
{
    checkGLError();
    glPushAttrib(GL_LINE_BIT | GL_ENABLE_BIT);
        glEnable(GL_LINE_SMOOTH);
        glLineWidth(width);
        glBegin(GL_LINES);
            glColor3f(r, g, b);
            glVertex2f(x1, y1);
            glVertex2f(x2, y2);
        glEnd();
    glPopAttrib();
    checkGLError();
}

void Engine::drawRect(float r, float g, float b,
        float width, float height, float x, float y, float rot)
{
    checkGLError();
    glPushAttrib(GL_ENABLE_BIT);
        glPushMatrix();
            glTranslatef(x, y, 0);
            glRotatef(rot, 0, 0, 1);
            glEnable(GL_LINE_SMOOTH);
            glBegin(GL_LINE_LOOP);
                glColor3f(r, g, b);
                glVertex2f(-width / 2, -height / 2);
                glVertex2f(width / 2, -height / 2);
                glVertex2f(width / 2, height / 2);
                glVertex2f(-width / 2, height / 2);
            glEnd();
        glPopMatrix();
    glPopAttrib();
    checkGLError();
}

void Engine::fillRect(float r, float g, float b,
        float width, float height, float x, float y, float rot)
{
    checkGLError();
    glPushAttrib(GL_ENABLE_BIT);
        glPushMatrix();
            glTranslatef(x, y, 0);
            glRotatef(rot, 0, 0, 1);
            glEnable(GL_POLYGON_SMOOTH);
            glBegin(GL_QUADS);
                glColor3f(r, g, b);
                glVertex2f(-width / 2, -height / 2);
                glVertex2f(width / 2, -height / 2);
                glVertex2f(width / 2, height / 2);
                glVertex2f(-width / 2, height / 2);
            glEnd();
        glPopMatrix();
    glPopAttrib();
    checkGLError();
}

void Engine::drawImage(float width, float height, float x, float y,
        int tex, float rot)
{
    checkGLError();
    glPushAttrib(GL_ENABLE_BIT          /* for enablings */
            | GL_TEXTURE_BIT            /* texture bindings and env. mode */
            | GL_COLOR_BUFFER_BIT);     /* blend src. and dst. equations */
        glPushMatrix();
            glTranslatef(x, y, 0);
            glRotatef(rot, 0, 0, 1);
            glEnable(GL_POLYGON_SMOOTH);
            glEnable(GL_TEXTURE_2D);
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
            glBindTexture(GL_TEXTURE_2D, tex);
            glBegin(GL_QUADS);
                glTexCoord2f(0, 0);
                glVertex2f(-width / 2, -height / 2);
                glTexCoord2f(1, 0);
                glVertex2f(width / 2, -height / 2);
                glTexCoord2f(1, 1);
                glVertex2f(width / 2, height / 2);
                glTexCoord2f(0, 1);
                glVertex2f(-width / 2, height / 2);
            glEnd();
        glPopMatrix();
    glPopAttrib();
    checkGLError();
}

void Engine::drawArc(float r, float g, float b, float x, float y,
        float radius, float a1, float a2)
{
    a1 *= M_PI / 180.0f;
    a2 *= M_PI / 180.0f;
    int segments = 1 + (int) (fabsf(a2 - a1) / (M_2_PI / 16.0f));
    float step = (a2 - a1) / segments;
    glPushMatrix();
        glTranslatef(x, y, 0);
        glColor3f(r, g, b);
        glBegin(GL_LINE_STRIP);
        float angle = a1;
        for (int i = 0; i <= segments; i++)
        {
            glVertex2f(radius * cos(angle), radius * sin(angle));
            angle += step;
        }
        glEnd();
    glPopMatrix();
}

void Engine::fillArc(float r, float g, float b, float x, float y,
        float radius, float a1, float a2)
{
    a1 *= M_PI / 180.0f;
    a2 *= M_PI / 180.0f;
    int segments = 1 + (int) (fabsf(a2 - a1) / (M_2_PI / 16.0f));
    float step = (a2 - a1) / segments;
    glPushAttrib(GL_ENABLE_BIT);
        glDisable(GL_LIGHTING);
        glPushMatrix();
            glTranslatef(x, y, 0);
            glColor3f(r, g, b);
            glBegin(GL_TRIANGLE_FAN);
            glVertex2f(0.0f, 0.0f);
            float angle = a1;
            for (int i = 0; i <= segments; i++)
            {
                glVertex2f(radius * cos(angle), radius * sin(angle));
                angle += step;
            }
            glEnd();
        glPopMatrix();
    glPopAttrib();
}

void Engine::drawPoint(float size, float r, float g, float b,
        float x, float y, float z)
{
    glPushAttrib(GL_ENABLE_BIT);
        glEnable(GL_POINT_SMOOTH);
        glEnable(GL_BLEND);
        glDisable(GL_LIGHTING);
        glColor3f(r, g, b);
        glPointSize(size);
        glBegin(GL_POINTS);
            glVertex3f(x, y, z);
        glEnd();
    glPopAttrib();
}

/* called by SDL when the update timer expires */
Uint32 Engine::updateCallback(Uint32 interval, void * param)
{
    Engine * engine = (Engine *) param;
    return engine->updateCallback(interval);
}

/* member update function to be called by our registered
 * SDL callback non-member function */
Uint32 Engine::updateCallback(Uint32 interval)
{
    if (!m_drawing)
    {
        SDL_PushEvent(&m_updateEvent);
    }
    return interval;
}

void Engine::run()
{
    /* register a screen redrawing SDL event */
    SDL_AddTimer(25, &updateCallback, this);

    SDL_Event event;
    while (SDL_WaitEvent(&event))
    {
        m_event_time = SDL_GetTicks();
        switch (event.type)
        {
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                        goto RET;
                    case SDLK_F1:
                        m_av.toggleFullScreen();
                        break;
                    case SDLK_F2:
                        m_engine_cursor_visible = !m_engine_cursor_visible;
                        m_input_grabbed = !m_input_grabbed;
                        updateCursorVisibility();
                        m_av.setInputGrabbed(m_input_grabbed);
                        break;
                    case SDLK_F5:
                        reloadProgram();
                        break;
                    default:
                        key_down_event(event.key.keysym.sym);
                }
                break;
            case SDL_KEYUP:
                key_up_event(event.key.keysym.sym);
                break;
            case SDL_MOUSEBUTTONDOWN:
                mousebutton_down_event(event.button.button,
                        event.button.x, event.button.y);
                break;
            case SDL_MOUSEBUTTONUP:
                mousebutton_up_event(event.button.button,
                        event.button.x, event.button.y);
                break;
            case SDL_MOUSEMOTION:
                mouse_motion_event(event.motion.x, event.motion.y,
                        event.motion.xrel, event.motion.yrel);
                break;
            case SDL_QUIT:
                goto RET;
                break;
            case SDL_USEREVENT:
                switch (event.user.code)
                {
                    case 0:
                        update_event();
                        break;
                    case 1:
                        goto RET;
                        break;
                }
                break;
        }
    }
RET:
    ;
}

void Engine::update_event()
{
    m_drawing = true;
    if (m_autoPhysics)
        doPhysics();
    if (m_autoStartFrame)
        startFrame();
    if (m_event_update_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(update));
        /* call the update function - pops the function ref from the stack */
        int s = lua_pcall(m_luaState, 0, 0, 0);
        reportErrors(s);
    }
    if (m_autoDrawObjects)
        drawObjects();
    update_overlay_event();
    if (m_autoEndFrame)
        endFrame();
    m_drawing = false;
}

void Engine::update_overlay_event()
{
    if (m_event_update_overlay_present)
    {
        checkGLError();
        glPushAttrib(GL_ENABLE_BIT);
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_LIGHTING);
            int width = m_av.getWidth();
            int height = m_av.getHeight();
            glMatrixMode(GL_PROJECTION);
            glPushMatrix();
            glLoadIdentity();
            glOrtho(0, width, 0, height, -1.01, 1.01);
            glMatrixMode(GL_MODELVIEW);
            glLoadIdentity();

            lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                    EVENT_HANDLER_AG_NAME(update_overlay));
            lua_pushnumber(m_luaState, width);
            lua_pushnumber(m_luaState, height);
            /* call the update_overlay function
             * - pops the function ref from the stack, then the arguments */
            int s = lua_pcall(m_luaState, 2, 0, 0);
            reportErrors(s);
            glMatrixMode(GL_PROJECTION);
            glPopMatrix();
            glMatrixMode(GL_MODELVIEW);
        glPopAttrib();
        checkGLError();
    }
}

void Engine::key_down_event(int keysym)
{
    m_keysDown[sdl_keymap[keysym]] = true;
    if (m_event_key_down_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(key_down));
        lua_pushstring(m_luaState, sdl_keymap[keysym]);
        /* call the key down event function
         * This pops the function ref and arguments from the stack */
        int s = lua_pcall(m_luaState, 1, 0, 0);
        reportErrors(s);
    }
}

void Engine::key_up_event(int keysym)
{
    m_keysDown.erase(sdl_keymap[keysym]);
    if (m_event_key_up_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(key_up));
        lua_pushstring(m_luaState, sdl_keymap[keysym]);
        /* call the key up event function
         * This pops the function ref and arguments from the stack */
        int s = lua_pcall(m_luaState, 1, 0, 0);
        reportErrors(s);
    }
}

void Engine::mousebutton_down_event(int button, int x, int y)
{
    if (m_event_mousebutton_down_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(mousebutton_down));
        lua_pushinteger(m_luaState, button);
        lua_pushnumber(m_luaState, x);
        lua_pushnumber(m_luaState, m_av.getHeight() - y);
        /* call the mouse button down event function
         * This pops the function ref and arguments from the stack */
        int s = lua_pcall(m_luaState, 3, 0, 0);
        reportErrors(s);
    }
}

void Engine::mousebutton_up_event(int button, int x, int y)
{
    if (m_event_mousebutton_up_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(mousebutton_up));
        lua_pushinteger(m_luaState, button);
        lua_pushnumber(m_luaState, x);
        lua_pushnumber(m_luaState, m_av.getHeight() - y);
        /* call the mouse button up event function
         * This pops the function ref and arguments from the stack */
        int s = lua_pcall(m_luaState, 3, 0, 0);
        reportErrors(s);
    }
}

void Engine::mouse_motion_event(int x, int y, int xrel, int yrel)
{
    if (m_event_mouse_motion_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(mouse_motion));
        lua_pushnumber(m_luaState, x);
        lua_pushnumber(m_luaState, m_av.getHeight() - y);
        lua_pushnumber(m_luaState, xrel);
        lua_pushnumber(m_luaState, -yrel);
        /* call the mouse motion event function
         * This pops the function ref and arguments from the stack */
        int s = lua_pcall(m_luaState, 4, 0, 0);
        reportErrors(s);
    }
}

void Engine::checkForFunctionFull(const std::string & lua_fn_name,
        const std::string & event_name, bool & presentFlag)
{
    lua_getfield(m_luaState, LUA_GLOBALSINDEX, lua_fn_name.c_str());
    if (lua_isfunction(m_luaState, -1))
    {
        lua_setfield(m_luaState, LUA_GLOBALSINDEX,
                (AG_EVENT_PREFIX + event_name).c_str());
        presentFlag = true;
    }
    else
    {
        lua_pop(m_luaState, 1);
        presentFlag = false;
    }
}

void Engine::doRegisterHandlerFull(int index,
        const std::string & event_name, bool & presentFlag)
{
    lua_pushvalue(m_luaState, index);
    lua_setfield(m_luaState, LUA_GLOBALSINDEX, event_name.c_str());
    presentFlag = true;
}

void Engine::reloadProgram()
{
    importFullPath(m_program_path.c_str());
    checkForAllHandlerFunctions();
    if (m_event_reinit_present)
    {
        lua_getfield(m_luaState, LUA_GLOBALSINDEX,
                EVENT_HANDLER_AG_NAME(reinit));
        /* call the init function - pops the function ref from the stack */
        int s = lua_pcall(m_luaState, 0, 0, 0);
        reportErrors(s);
    }

}

void Engine::doPhysics()
{
    static Uint32 last_updated = 0;
    Uint32 current_ticks = SDL_GetTicks();
    if (last_updated > 0)
    {
        Uint32 msec_steps = current_ticks - last_updated;
        for (Uint32 i = 0; i < msec_steps; i++)
            m_world.step();
    }
    last_updated = current_ticks;
}

void Engine::drawObjects()
{
    for (std::map<int, Object *>::const_iterator it = m_objects.begin();
         it != m_objects.end();
         it++)
    {
        it->second->draw();
    }
    for (std::map<int, Quad *>::const_iterator it = m_quads.begin();
         it != m_quads.end();
         it++)
    {
        it->second->draw();
    }
}

int Engine::getFileSize(const char *fname)
{
    struct stat st;

    if (stat(fname, &st))
        return -2;
    return st.st_size;
}

const unsigned char *Engine::loadFile(const char *fname, unsigned int *len)
{
    string file_path = locateResource(fname);
    unsigned int size = getFileSize(file_path.c_str());
    if (size > 0)
    {
        FILE * fp = fopen(file_path.c_str(), "rb");
        if (fp != NULL)
        {
            unsigned char *data = new unsigned char[size];
            int num_read = fread(data, size, 1, fp);
            fclose(fp);
            if (num_read > 0)
            {
                if (len != NULL)
                    *len = size;
                return data;
            }
        }
    }
    return NULL;
}

bool Engine::loadWFObjFile(const char *fname, WFObj::Buffer & buff)
{
    unsigned int size;
    const unsigned char *data = loadFile(fname, &size);
    if (data != NULL)
    {
        buff.alloc(size);
        memcpy(buff.data, data, size);
        delete[] data;
        return true;
    }
    return false;
}

GLuint Engine::loadTexture(const char *fname)
{
    string path = locateResource(fname);
    GLuint id = m_textureCache.load(path.c_str());
    if (id != 0)
    {
        int width, height;
        glBindTexture(GL_TEXTURE_2D, id);
        glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
        glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
        char *data = (char *) malloc(4 * width * height);
        glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, width, height, GL_RGBA,
                GL_UNSIGNED_BYTE, data);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                GL_LINEAR_MIPMAP_LINEAR);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        free(data);
    }
    return id;
}

/******** Engine::PickedObject functions ********/

Engine::PickedObject::PickedObject(OdeWorld::PickedObjectRef por)
{
    Object * o = (Object *) por->obj->getUserData();
    id = o->getID();
    dist = por->dist;
    pos[0] = por->pos[0];
    pos[1] = por->pos[1];
    pos[2] = por->pos[2];
    normal[0] = por->normal[0];
    normal[1] = por->normal[1];
    normal[2] = por->normal[2];
}

/******** Engine::Quad functions ********/

Engine::Quad::Quad(float cx, float cy, float cz,
                   float v1x, float v1y, float v1z,
                   float v2x, float v2y, float v2z)
{
    m_center[0] = cx;
    m_center[1] = cy;
    m_center[2] = cz;
    m_v1[0] = v1x;
    m_v1[1] = v1y;
    m_v1[2] = v1z;
    m_v2[0] = v2x;
    m_v2[1] = v2y;
    m_v2[2] = v2z;
    dCROSS(m_normal, =, m_v1, m_v2);
    dNormalize3(m_normal);
    m_dl = 0;
    m_visible = true;
    m_offset = 0.0f;
    m_texture = 0;
    m_enable_blending = false;
    m_color[0] = m_color[1] = m_color[2] = m_color[3] = 1.0f;
    m_position[0] = m_position[1] = m_position[2] = 0.0f;
    render();
}

Engine::Quad::~Quad()
{
    if (m_dl != 0)
    {
        glDeleteLists(m_dl, 1);
    }
}

int Engine::Quad::render()
{
    if (m_dl == 0)
    {
        m_dl = glGenLists(1);
    }
    glNewList(m_dl, GL_COMPILE);
    bool do_offset = !FP_EQ(m_offset, 0.0f);
    glPushAttrib(GL_POLYGON_BIT | GL_TEXTURE_BIT | GL_COLOR_BUFFER_BIT);
    if (do_offset)
    {
        glEnable(GL_POLYGON_OFFSET_FILL);
        glPolygonOffset(0.0, -2.0f * m_offset);
    }
    if (m_texture != 0)
    {
        glEnable(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, m_texture);
    }
    else
    {
        glDisable(GL_TEXTURE_2D);
        glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, &m_color[0]);
    }
    if (m_enable_blending)
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }
    glPushMatrix();
    glTranslatef(m_position[0], m_position[1], m_position[2]);
    glBegin(GL_QUADS);
    glNormal3f(m_normal[0], m_normal[1], m_normal[2]);
    if (m_texture != 0)
        glTexCoord2f(1.0, 1.0);
    glVertex3f(m_center[0] + m_v1[0] + m_v2[0],
               m_center[1] + m_v1[1] + m_v2[1],
               m_center[2] + m_v1[2] + m_v2[2]);
    if (m_texture != 0)
        glTexCoord2f(0, 1.0);
    glVertex3f(m_center[0] - m_v1[0] + m_v2[0],
               m_center[1] - m_v1[1] + m_v2[1],
               m_center[2] - m_v1[2] + m_v2[2]);
    if (m_texture != 0)
        glTexCoord2f(0, 0);
    glVertex3f(m_center[0] - m_v1[0] - m_v2[0],
               m_center[1] - m_v1[1] - m_v2[1],
               m_center[2] - m_v1[2] - m_v2[2]);
    if (m_texture != 0)
        glTexCoord2f(1.0, 0);
    glVertex3f(m_center[0] + m_v1[0] - m_v2[0],
               m_center[1] + m_v1[1] - m_v2[1],
               m_center[2] + m_v1[2] - m_v2[2]);
    glEnd();
    glPopMatrix();
    glPopAttrib();
    glEndList();
    return m_dl;
}
