#include <Redacted3D/types/entity/entity.h>
#include <Redacted3D/engine/occlusion.h>
#include <Redacted3D/types/entity/camera.h>
#include <Redacted3D/engine/engine.h>
#include <Redacted3D/engine/utils/instanceOf.h>

//inline LinearAlgebra::Vector3 Entity::GetPos() const
//{ return LinearAlgebra::Vector3(coordinates[1]); }

//void Entity::SetPos(const LinearAlgebra::Vector3& rhs)
//{ coordinates[1] = LinearAlgebra::Vector4(rhs.x, rhs.y, rhs.z, 0); }

//LinearAlgebra::Matrix4x4 Entity::GetMatrix() const
//{ return coordinates;}

void Entity::SetMatrix(const Matrix4x4& rhs)
{ coordinates = rhs; }

bool Entity::occluded() {
    Camera* camera;
    for (auto& e : engine->world->GetChildren())
        if (auto* c = dynamic_cast<Camera*>(e))
            camera = c;

    if (!Occlusion::frustumCull(camera, this))
    return false;
return true;
}

Vector3 Entity::GetPos() const {
    return position;
}

void Entity::SetPos(const Vector3 &rhs) {
    position = rhs;
}

Entity::Entity() {
    position = {0,0,0};
    ticksAlive = 0;
    children = std::vector<Entity*> ();
    parent = nullptr;
    scale = 1.0f;

    //UUIDv4::UUIDGenerator<std::mt19937_64> uuidGenerator;
    //UUIDv4::UUID id = uuidGenerator.getUUID();
    //uuid = id.bytes();
}

void Entity::setScale(const float &multiplier) {
    scale = multiplier;
}

GLfloat Entity::getScale() const {
    return scale;
}

void Entity::loadTexture() {
    Texture texture(this, texturePath.c_str(), true);
}

void Entity::loadGeometry() {
    VertexArray vArray(this, modelPath, true);
}

VertexArray* Entity::getGeometry() {
    if (engine->world->geometryList.empty())
        loadGeometry();
    for (auto& vArray : engine->world->geometryList)
        for (auto& entity : vArray->usedBy)
            if (instanceOf(this, entity)) {
                if (std::find(vArray->usedBy.begin(), vArray->usedBy.end(), this) == vArray->usedBy.end()) //If this is the first time the entity is using it.
                    vArray->usedBy.push_back(this);
            return vArray;
        }
    loadGeometry();
    return getGeometry();
}

void Entity::render() {
    bool multiTexture = false;
    bool motionTexture = false;

    if (auto* t = dynamic_cast<MultiTexture*>(getTexture()))
        multiTexture = true;

    if (!multiTexture)
        if (auto* t = dynamic_cast<MotionTexture*>(getTexture()))
            motionTexture = true;

    glPushMatrix();
    glTranslatef(position.x, position.y, position.z);
    glRotatef(angle.x, 1.0f, 0.0f, 0.0f);
    glRotatef(angle.y, 0.0f, 1.0f, 0.0f);
    glRotatef(angle.z, 0.0f, 0.0f, 1.0f);
    if (getScale() != 1.0f)
        glScalef(getScale(), getScale(), getScale());

    //Set up texture.
    glActiveTexture(GL_TEXTURE0);
    glClientActiveTexture(GL_TEXTURE0);
    glEnable(GL_TEXTURE_2D);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glBindTexture(GL_TEXTURE_2D, motionTexture ? ((MotionTexture*)getTexture())->current()->id : getTexture()->id);

    if (!engine->useVBO)
        glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &getGeometry()->vertices[0].x),
        glTexCoordPointer(2, GL_FLOAT, sizeof(TextureCoordinate), &getGeometry()->texCoords[0].x);

    if (engine->useVBO)
        glBindBuffer(GL_ARRAY_BUFFER, getGeometry()->tbo),
        glTexCoordPointer(2, GL_FLOAT, 0, nullptr);

    if (!multiTexture)
        getGeometry()->draw();

    if (multiTexture) {
        auto* multi = (MultiTexture*) getTexture();

        int i = 0;
        GLfloat blendingFactor[3] = {engine->world->getAmbientLightColor().x * 0.2f, engine->world->getAmbientLightColor().y * 0.2f, engine->world->getAmbientLightColor().z * 0.2f};
        for(auto& texture : multi->multi) {
            glActiveTexture(GL_TEXTURE1 + i);
            glClientActiveTexture(GL_TEXTURE1 + i);
            glEnable(GL_TEXTURE_2D);
            glEnableClientState(GL_VERTEX_ARRAY);
            glEnableClientState(GL_TEXTURE_COORD_ARRAY);

            if (!engine->useVBO)
                glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &getGeometry()->vertices[0].x),
                glTexCoordPointer(2, GL_FLOAT, sizeof(TextureCoordinate), &getGeometry()->texCoords[0].x);

            if (engine->useVBO)
                glBindBuffer(GL_ARRAY_BUFFER, getGeometry()->tbo),
                glTexCoordPointer(2, GL_FLOAT, 0, nullptr);

            //Texture unit mode.
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);

            //Makes the lighting work the way you'd think it would.
            glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, blendingFactor);
            glBindTexture(GL_TEXTURE_2D, texture.id);
            i++;
        }
        getGeometry()->draw();

        //Reset the multi-texture units.
        for (int j = 0; j <= i; j++) {
            glActiveTexture(GL_TEXTURE1 + j);
            glClientActiveTexture(GL_TEXTURE1 + j);
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
            glDisable(GL_TEXTURE_2D);
            glDisableClientState(GL_VERTEX_ARRAY);
            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
        }
    }

    //Reset texture unit 0.
    glActiveTexture(GL_TEXTURE0);
    glClientActiveTexture(GL_TEXTURE0);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glDisable(GL_TEXTURE_2D);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glPopMatrix();

    if (engine->debug) {
        glDisable(GL_LIGHTING);
        glPushMatrix();
        glColor4f(1, 0, 0, 1);
        Collision::getDrawable(getSphere()).drawWireframe(); //TODO drawing this solid segfaults with useVBO because we attempt to draw buffer 0.
        glColor4f(1, 1, 1, 1);
        glPopMatrix();
        glEnable(GL_LIGHTING);
    }
}

bool Entity::isCollidingWith(Entity *entity) {

}

Texture* Entity::getTexture() {
    if (engine->world->textureList.empty())
        loadTexture();
    for (auto& texture : engine->world->textureList)
        for (auto& entity : texture->usedBy)
            if (instanceOf(this, entity)) {
                if (std::find(texture->usedBy.begin(), texture->usedBy.end(), this) == texture->usedBy.end()) //If this is the first time the entity is using it.
                    texture->usedBy.push_back(this);
                return texture;
            }
    loadTexture();
    return getTexture();
}

void Entity::erase() {
    Texture* t = getTexture();
    VertexArray* vA = getGeometry();

    for (int i = 0; i < t->usedBy.size(); i++)
        if (t->usedBy[i] == this)
            t->usedBy.erase(t->usedBy.begin() + i);

    if (t->usedBy.empty())
        t->erase();

    for (int i = 0; i < vA->usedBy.size(); i++)
        if (vA->usedBy[i] == this)
            vA->usedBy.erase(vA->usedBy.begin() + 1);

    if (vA->usedBy.empty())
        vA->erase();

    for (int i = 0; i < engine->world->GetChildren().size(); i++)
        if (engine->world->GetChildren()[i] == this)
            engine->world->GetChildren().erase(engine->world->GetChildren().begin() + i);
    delete this;
}

AABB Entity::getAABB() {
    AABB aabb = Collision::genMinimallyEnclosingAABB(getGeometry());
    if (getScale() != 1)
        aabb.minPoint *= scale,
        aabb.maxPoint *= scale;

    aabb.minPoint += position;
    aabb.maxPoint += position;
    return aabb;
}

Sphere Entity::getSphere() {
    Sphere sphere = Collision::genMinimallyEnclosingSphere(getGeometry());
    sphere.Radius *= scale;
    sphere.Position = position;
    return sphere;
}

OBB Entity::getOBB() {
    return getGeometry()->getCachedOBB(angle);
}
