
#include <GL/gl.h>
#include <GL/glu.h>
#include "SDL.h"

#include "Engine.h"

using namespace std;

#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)

/* used for objects loaded directly from model files */
Engine::Object::Object(bool is_static, bool is_reference, bool enable_blending,
        OdeWorld & world, Engine & e, WFObj * obj, float scale)
    : m_engine(e), m_world(world)
{
    m_is_reference = is_reference;
    if (m_is_reference)
    {
        m_ode_object = NULL;
    }
    else
    {
        m_ode_object = world.createObject(is_static, scale);
        m_ode_object->setUserData(this);
    }
    m_is_static = is_static;
    m_enable_blending = enable_blending;
    const float * obj_aabb = obj->getAABB();
    for (int i = 0; i < 6; i++)
        m_aabb[i] = scale * obj_aabb[i];
    m_is_visible = true;
    m_scale = scale;
    m_is_scaled = ! (fabs(scale - 1.0) < 0.0001);
    m_display_list = 0;
    m_is_managed = false;
    m_mass = 0.0;
    m_mass_is_set = false;
    m_gravity_mode = true;
    m_obj = obj;
    m_obj_refcnt = new int;
    *m_obj_refcnt = 1;
}

/* used for "managed" objects with one geom */
Engine::Object::Object(bool is_static, bool is_reference, bool enable_blending,
                        OdeWorld & world, Engine & e,
                        OdeWorld::GeomType geom_type,
                        refptr< std::vector<float> > args)
    : m_engine(e), m_world(world)
{
    m_is_reference = is_reference;
    m_enable_blending = enable_blending;
    m_is_static = geom_type == OdeWorld::PLANE ? false : is_static;
    m_is_visible = true;
    m_scale = 1.0f;
    m_is_scaled = false;
    m_display_list = 0;
    if (m_is_reference)
    {
        m_ode_object = NULL;
    }
    else
    {
        m_ode_object = world.createObject(m_is_static, m_scale);
        m_ode_object->setUserData(this);
    }
    m_is_managed = true;
    m_geom_type = geom_type;
    m_args = args;
    for (int i = 0; i < 4; i++)
        m_color[i] = 1.0f;
    m_texture = 0;
    m_texture_scale = 1.0f;
    createManagedObject();
    render();
    m_mass = 0.0;
    m_mass_is_set = false;
    m_gravity_mode = true;
    m_obj = NULL;
    m_obj_refcnt = NULL;
}

/* used to clone objects */
Engine::Object::Object(const Engine::Object & orig)
    : m_engine(orig.m_engine), m_world(orig.m_world)
{
    m_is_reference = false;
    m_is_visible = orig.m_is_visible;
    m_is_static = orig.m_is_static;
    m_enable_blending = orig.m_enable_blending;
    m_scale = orig.m_scale;
    m_is_scaled = orig.m_is_scaled;
    m_phy = orig.m_phy;
    m_is_managed = orig.m_is_managed;
    m_display_list = 0;
    m_geom_type = orig.m_geom_type;
    m_args = orig.m_args;
    for (int i = 0; i < 4; i++)
        m_color[i] = orig.m_color[i];
    m_texture = orig.m_texture;
    m_texture_scale = orig.m_texture_scale;
    if (orig.m_is_reference)
    {
        m_ode_object = m_world.createObject(m_is_static, m_scale);
        instantiatePhy();
    }
    else
    {
        m_ode_object = new OdeWorld::Object(*orig.m_ode_object);
    }
    m_ode_object->setUserData(this);
    if (m_is_managed)
    {
        createManagedObject();
        render();
    }
    m_mass = orig.m_mass;
    m_mass_is_set = orig.m_mass_is_set;
    if (m_mass_is_set)
    {
        setMass(orig.getMass());
    }
    m_gravity_mode = orig.m_gravity_mode;
    setGravityMode(m_gravity_mode);
    m_obj = orig.m_obj;
    m_obj_refcnt = orig.m_obj_refcnt;
    if (m_obj_refcnt)
        (*m_obj_refcnt)++;
}

Engine::Object::~Object()
{
    if (m_ode_object != NULL)
        delete m_ode_object;
    if (m_display_list != 0)
    {
        glDeleteLists(m_display_list, 1);
    }
    if (m_obj_refcnt != NULL)
    {
        (*m_obj_refcnt)--;
        if (*m_obj_refcnt < 1)
            delete m_obj;
    }
}

void Engine::Object::createManagedObject()
{
    switch (m_geom_type)
    {
        case OdeWorld::BOX: {
            while (m_args->size() < 9)
                m_args->push_back(0);
            if (m_ode_object != NULL)
                m_ode_object->addBox(m_args);
            float aabb[6] = {-(*m_args)[0], -(*m_args)[1], -(*m_args)[2],
                             (*m_args)[0], (*m_args)[1], (*m_args)[2]};
            for (int i = 0; i < 6; i++)
                aabb[i] /= 2.0f;
            memcpy(m_aabb, aabb, sizeof(m_aabb));
            break;
        }
        case OdeWorld::SPHERE: {
            while (m_args->size() < 4)
                m_args->push_back(0);
            if (m_ode_object != NULL)
                m_ode_object->addSphere(m_args);
            float aabb[6] = {-(*m_args)[0], -(*m_args)[0], -(*m_args)[0],
                             (*m_args)[0], (*m_args)[0], (*m_args)[0]};
            memcpy(m_aabb, aabb, sizeof(m_aabb));
            break;
        }
        case OdeWorld::PLANE: {
            while (m_args->size() < 4 || m_args->size() == 5)
                m_args->push_back(0);
            if (m_ode_object != NULL)
                m_ode_object->addPlane(m_args);
            for (int i = 0; i < 6; i++)
                m_aabb[i] = 0.0f;
            break;
        }
        case OdeWorld::CYLINDER: {
            while (m_args->size() < 8)
                m_args->push_back(0);
            if (m_ode_object != NULL)
                m_ode_object->addCylinder(m_args);
            float aabb[6] = {-(*m_args)[0], -(*m_args)[0], -(*m_args)[1],
                             (*m_args)[0], (*m_args)[0], (*m_args)[1]};
            memcpy(m_aabb, aabb, sizeof(m_aabb));
            break;
        }
        case OdeWorld::CAPSULE: {
            while (m_args->size() < 8)
                m_args->push_back(0);
            if (m_ode_object != NULL)
                m_ode_object->addCapsule(m_args);
            float aabb[6] = {-(*m_args)[0], -(*m_args)[0], -(*m_args)[1],
                             (*m_args)[0], (*m_args)[0], (*m_args)[1]};
            memcpy(m_aabb, aabb, sizeof(m_aabb));
            break;
        }
    }
    if (m_ode_object != NULL)
    {
        m_ode_object->finalize();
    }
    if (!m_is_reference)
    {
        render();
    }
}

/* render a managed object */
/* prerequisite: m_args->size() is big enough */
void Engine::Object::render()
{
    if (!m_is_managed || m_is_reference)
        return;
    GLUquadric * quad = gluNewQuadric();
    if (m_display_list <= 0)
    {
        m_display_list = glGenLists(1);
    }
    checkGLError();
    glNewList(m_display_list, GL_COMPILE);
    int attrib_flags = GL_ENABLE_BIT;
    if (m_enable_blending)
    {
        attrib_flags |= GL_COLOR_BUFFER_BIT | GL_TEXTURE_BIT;
    }
    glPushAttrib(attrib_flags);
    if (m_enable_blending)
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    }
    if (m_texture != 0)
    {
        gluQuadricTexture(quad, 1);
        glEnable(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, m_texture);
    }
    else
    {
        glDisable(GL_TEXTURE_2D);
        glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, m_color);
    }
    switch (m_geom_type)
    {
        case OdeWorld::BOX: {
                const static struct {
                    float norm[3];
                    float verts[4][3];
                } sides[] = {
                    { {1, 0, 0},        /* right */
                        {{1, -1, -1}, {1, 1, -1}, {1, 1, 1}, {1, -1, 1}} },
                    { {-1, 0, 0},       /* left */
                        {{-1, 1, -1}, {-1, -1, -1}, {-1, -1, 1}, {-1, 1, 1}} },
                    { {0, 1, 0},        /* back */
                        {{1, 1, -1}, {-1, 1, -1}, {-1, 1, 1}, {1, 1, 1}} },
                    { {0, -1, 0},       /* front */
                        {{-1, -1, -1}, {1, -1, -1}, {1, -1, 1}, {-1, -1, 1}} },
                    { {0, 0, 1},        /* top */
                        {{-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}} },
                    { {0, 0, -1},       /* bottom */
                        {{1, -1, -1}, {-1, -1, -1}, {-1, 1, -1}, {1, 1, -1}} }
                };
                const float tex_coords[4][2] = {
                    {0, 0}, {1, 0}, {1, 1}, {0, 1}
                };
                glBegin(GL_QUADS);
                float width = (*m_args)[0];
                float depth = (*m_args)[1];
                float height = (*m_args)[2];
                for (unsigned int s = 0;
                        s < (sizeof(sides)/sizeof(sides[0]));
                        s++)
                {
                    glNormal3fv(sides[s].norm);
                    for (int v = 0; v < 4; v++)
                    {
                        if (m_texture != 0)
                        {
                            glTexCoord2f(tex_coords[v][0] * m_texture_scale,
                                    tex_coords[v][1] * m_texture_scale);
                        }
                        glVertex3f(sides[s].verts[v][0] * width / 2.0,
                                sides[s].verts[v][1] * depth / 2.0,
                                sides[s].verts[v][2] * height / 2.0);
                    }
                }
                glEnd();
            }
            break;
        case OdeWorld::SPHERE:
            gluSphere(quad, (*m_args)[0], 16, 8);
            break;
        case OdeWorld::PLANE: {
            dVector3 normal;
            dVector3 rotated_x;
            dVector3 rotated_y;
            float x_o, y_o, z_o;
            if (m_args->size() == 6)
            {
                dMatrix3 r;
                dRFromEulerAngles(r, (*m_args)[3], (*m_args)[4], (*m_args)[5]);
                dVector3 default_plane_direction = {0, 0, 1, 0};
                dVector3 default_x = {1, 0, 0, 0};
                dVector3 default_y = {0, 1, 0, 0};
                dMultiply0(normal, r, default_plane_direction, 3, 3, 1);
                dMultiply0(rotated_x, r, default_x, 3, 3, 1);
                dMultiply0(rotated_y, r, default_y, 3, 3, 1);
                x_o = (*m_args)[0];
                y_o = (*m_args)[1];
                z_o = (*m_args)[2];
            }
            else
            {
                normal[0] = (*m_args)[0];
                normal[1] = (*m_args)[1];
                normal[2] = (*m_args)[2];
                float d = (*m_args)[3];
                float len = normal[0] * normal[0]
                            + normal[1] * normal[1]
                            + normal[2] * normal[2];
                if (! FP_EQ(len, 1.0))          /* normalize if necessary */
                {
                    len = sqrtf(len);
                    normal[0] /= len;
                    normal[1] /= len;
                    normal[2] /= len;
                    d /= len;
                }
                int min_component = 0;
                float comp = 10000.0f;
                for (int i = 0; i < 3; i++)
                {
                    if (normal[i] < comp)
                    {
                        min_component = i;
                        comp = normal[i];
                    }
                }
                dVector3 cross_vec;
                for (int i = 0; i < 3; i++)
                    cross_vec[i] = (i == min_component) ? 1 : 0;
                dCROSS(rotated_x, =, normal, cross_vec);
                dCROSS(rotated_y, =, normal, rotated_x);
                x_o = normal[0] * d;
                y_o = normal[1] * d;
                z_o = normal[2] * d;
            }

            /* plane equation: ax + by + cz = d, plane normal: (a, b, c) */

            const int num_sections = 10;
            const float section_size = 100.0f;
#ifdef dSINGLE
            glNormal3fv(normal);
#else
            glNormal3dv(normal);
#endif
            for (int x = 0; x < num_sections; x++)
            {
                glBegin(GL_QUAD_STRIP);
                for (int y = 0; y <= num_sections; y++)
                {
                    float half_span = num_sections * section_size / 2.0;
                    float x_c = x * section_size - half_span;
                    float y_c = y * section_size - half_span;
                    glTexCoord2f(0.0, y * m_texture_scale);
                    glVertex3f(x_o + rotated_x[0] * x_c + rotated_y[0] * y_c,
                            y_o + rotated_x[1] * x_c + rotated_y[1] * y_c,
                            z_o + rotated_x[2] * x_c + rotated_y[2] * y_c);
                    x_c += section_size;
                    glTexCoord2f(1.0, y * m_texture_scale);
                    glVertex3f(x_o + rotated_x[0] * x_c + rotated_y[0] * y_c,
                            y_o + rotated_x[1] * x_c + rotated_y[1] * y_c,
                            z_o + rotated_x[2] * x_c + rotated_y[2] * y_c);
                }
                glEnd();
            }
            }
            break;
        case OdeWorld::CYLINDER: {
            glPushMatrix();
            float half_span = (*m_args)[1] / 2.0;
            glTranslatef(0, 0, -half_span);
            gluCylinder(quad, (*m_args)[0], (*m_args)[0], (*m_args)[1], 12, 1);
            gluDisk(quad, 0.0, (*m_args)[0], 12, 4);
            glTranslatef(0, 0, 2 * half_span);
            gluDisk(quad, 0.0, (*m_args)[0], 12, 4);
            glPopMatrix();
            }
            break;
        case OdeWorld::CAPSULE: {
            glPushAttrib(GL_TRANSFORM_BIT);      /* save current clip planes */
            double halfheight = (*m_args)[1] / 2.0;
            glPushMatrix();
            glTranslatef(0, 0, -halfheight);
            gluCylinder(quad, (*m_args)[0], (*m_args)[0], (*m_args)[1], 12, 1);

            double clip_plane[] = {0, 0, -1, halfheight};
            glClipPlane(GL_CLIP_PLANE0, clip_plane);
            glEnable(GL_CLIP_PLANE0);
            gluSphere(quad, (*m_args)[0], 12, 8);
            glTranslatef(0, 0, 2*halfheight);
            clip_plane[2] = 1.0;
            glClipPlane(GL_CLIP_PLANE0, clip_plane);
            gluSphere(quad, (*m_args)[0], 12, 8);
            glPopMatrix();
            glPopAttrib();
            }
            break;
    }
    gluDeleteQuadric(quad);
    glPopAttrib();
    glEndList();
    checkGLError();
}

void Engine::Object::setPosition(double x, double y, double z)
{
    if (m_ode_object != NULL)
        m_ode_object->setPosition(x, y, z);
}

void Engine::Object::getPosition(double * x, double * y, double * z)
{
    if (m_ode_object != NULL)
        m_ode_object->getPosition(x, y, z);
    else
        *x = *y = *z = 0.0;
}

void Engine::Object::loadPhy(const unsigned char *data, unsigned int size)
{
    m_phy = new PhyObj();
    m_phy->load(data, size);
    if (!m_is_managed)
        instantiatePhy();
}

void Engine::Object::instantiatePhy()
{
    if (m_ode_object != NULL && !m_is_managed && !m_phy.isNull())
    {
        for (int i = 0, sz = m_phy->getNumGeoms(); i < sz; i++)
        {
            refptr<PhyObj::Geom> geom = m_phy->getGeom(i);
            refptr< vector<float> > args = geom->getArgs();
            switch (geom->getType())
            {
                case PhyObj::BOX:
                    m_ode_object->addBox(args);
                    break;
                case PhyObj::SPHERE:
                    m_ode_object->addSphere(args);
                    break;
                case PhyObj::CAPSULE:
                    m_ode_object->addCapsule(args);
                    break;
                case PhyObj::PLANE:
                    m_ode_object->addPlane(args);
                    break;
                default:
                    break;
            }
        }
        m_ode_object->finalize();
    }
}

void Engine::Object::setTexture(GLuint tex)
{
    if (m_is_managed)
    {
        m_texture = tex;
        render();
    }
}

void Engine::Object::draw()
{
    if (!m_is_reference && m_is_visible)
    {
        checkGLError();
        const dReal * pos = m_ode_object->getPosition();
        const dReal * rot = m_ode_object->getRotation();
        bool transform = (pos != NULL && rot != NULL);

        if (transform)
            OdeWorld::pushTransform(pos, rot);

        if (m_is_scaled)
        {
            glPushAttrib(GL_TRANSFORM_BIT);
            glEnable(GL_NORMALIZE);
            glPushMatrix();
            glScalef(m_scale, m_scale, m_scale);
        }

        checkGLError();

        if (m_is_managed)
        {
            glCallList(m_display_list);
        }
        else
        {
            m_obj->bindBuffers();
            GLuint program = m_engine.m_programs[PROG_OBJ];
            glUseProgram(program);

            glEnableVertexAttribArray(ATTRIBUTE_OBJ_POS);
            glEnableVertexAttribArray(ATTRIBUTE_OBJ_NORMAL);
            int stride = m_obj->getStride();
            glVertexAttribPointer(ATTRIBUTE_OBJ_POS, 3, GL_FLOAT, GL_FALSE,
                    stride, (GLvoid *) m_obj->getVertexOffset());
            glVertexAttribPointer(ATTRIBUTE_OBJ_NORMAL, 3, GL_FLOAT, GL_FALSE,
                    stride, (GLvoid *) m_obj->getNormalOffset());
            if (m_obj->doTextures())
            {
                glVertexAttribPointer(ATTRIBUTE_OBJ_TEX_TEX_COORD,
                        2, GL_FLOAT, GL_FALSE,
                        stride, (GLvoid *) m_obj->getTextureCoordOffset());
            }
            for (map<string, WFObj::Material>::iterator it =
                    m_obj->getMaterials().begin();
                    it != m_obj->getMaterials().end();
                    it++)
            {
                WFObj::Material & m = it->second;
                if (m.flags & WFObj::Material::TEXTURE_BIT)
                {
                    glBindTexture(GL_TEXTURE_2D, m.texture);
                    if (program != m_engine.m_programs[PROG_OBJ_TEX])
                    {
                        program = m_engine.m_programs[PROG_OBJ_TEX];
                        glUseProgram(program);
                        glEnableVertexAttribArray(ATTRIBUTE_OBJ_TEX_TEX_COORD);
                    }
                }
                else
                {
                    if (program != m_engine.m_programs[PROG_OBJ])
                    {
                        program = m_engine.m_programs[PROG_OBJ];
                        glUseProgram(program);
                        glDisableVertexAttribArray(ATTRIBUTE_OBJ_TEX_TEX_COORD);
                    }
                }
                if (m.flags & WFObj::Material::SHININESS_BIT)
                {
                    if (program == m_engine.m_programs[PROG_OBJ_TEX])
                        glUniform1f(m_engine.m_uniforms_obj_tex[
                                UNIFORM_OBJ_TEX_SHININESS],
                                m.shininess);
                    else
                        glUniform1f(m_engine.m_uniforms_obj[
                                UNIFORM_OBJ_SHININESS],
                                m.shininess);
                }
                if (m.flags & WFObj::Material::AMBIENT_BIT)
                {
                    if (program == m_engine.m_programs[PROG_OBJ_TEX])
                        glUniform4fv(m_engine.m_uniforms_obj_tex[
                                UNIFORM_OBJ_TEX_AMBIENT],
                                1, &m.ambient[0]);
                    else
                        glUniform4fv(m_engine.m_uniforms_obj[
                                UNIFORM_OBJ_AMBIENT],
                                1, &m.ambient[0]);
                }
                if (m.flags & WFObj::Material::DIFFUSE_BIT)
                {
                    if (program != m_engine.m_programs[PROG_OBJ_TEX])
                        glUniform4fv(m_engine.m_uniforms_obj[
                                UNIFORM_OBJ_DIFFUSE],
                                1, &m.diffuse[0]);
                }
                if (m.flags & WFObj::Material::SPECULAR_BIT)
                {
                    if (program == m_engine.m_programs[PROG_OBJ_TEX])
                        glUniform4fv(m_engine.m_uniforms_obj_tex[
                                UNIFORM_OBJ_TEX_SPECULAR],
                                1, &m.specular[0]);
                    else
                        glUniform4fv(m_engine.m_uniforms_obj[
                                UNIFORM_OBJ_SPECULAR],
                                1, &m.specular[0]);
                }
                glDrawElements(GL_TRIANGLES, m.num_vertices,
                        GL_UNSIGNED_SHORT,
                        (GLvoid *) (sizeof(GLushort) * m.first_vertex));
                glBindBuffer(GL_ARRAY_BUFFER, 0);
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
                glUseProgram(0);
            }
        }

        checkGLError();

        if (m_is_scaled)
        {
            glPopMatrix();
            glPopAttrib();
        }

        if (transform)
            glPopMatrix();
        checkGLError();
    }
}

dReal Engine::Object::getMass() const
{
    return (m_ode_object != NULL) ? m_ode_object->getMass() : m_mass;
}

void Engine::Object::setMass(dReal mass)
{
    m_mass = mass;
    m_mass_is_set = true;
    if (m_ode_object != NULL)
    {
        m_ode_object->setMass(mass);
    }
}
