#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>
#include <jlog/jlog.hpp>

//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;
    DEBUG(std::format("Set position for entity '{}' to x={} y={} z={}", this->name, rhs.x, rhs.y, rhs.z));
}

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

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

void Entity::setScale(const float &multiplier) {
    scale.Set(multiplier, multiplier, multiplier);
}

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

void Entity::loadTexture(const std::string& file) {
    for (const auto* e : engine->world->GetChildren())
            if (instanceOf(this, e))
                return;
    Texture texture(this, file.c_str(), true);
}

void Entity::loadMultiTexture(const std::vector<std::string>& files) {
    for (const auto* e : engine->world->GetChildren())
        if (instanceOf(this, e))
            return;
    MultiTexture texture(this, files, true);
}

void Entity::loadMotionTexture(const std::vector<std::string>& files, u16 frameDelta, bool doAnim) {
    for (const auto* e : engine->world->GetChildren())
        if (instanceOf(this, e))
            return;
    MotionTexture(this, files, frameDelta, true, doAnim);
}

void Entity::loadGeometry(const std::string& file) {
    for (const auto& vA : engine->world->geometryList)
        for (const auto& reference : vA->usedBy)
            if (instanceOf(this, reference))
                return;
    VertexArray vArray(this, file, true);
}

VertexArray* Entity::getGeometry() {
    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;
        }
    return nullptr;
}

Texture* Entity::getTexture() {
    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;
            }
    return nullptr;
}

inline Texture* baseTexture = new Texture;
void Entity::render() {
    if (!baseTexture) {
        Texture t;
        glGenTextures(1, &t.id);
        glBindTexture(GL_TEXTURE_2D, t.id);

        unsigned char whitePixel[4] = { 255, 255, 255, 255};
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glActiveTexture(0);
        baseTexture = new Texture(t);
    }

    MultiTexture texture;
    texture.id = baseTexture->id;

    if (auto* t = dynamic_cast<MultiTexture*>(getTexture())) {
        texture.multi.emplace_back(*t);
        for (const auto &item: t->multi)
            texture.multi.push_back(item);
    }

    else if (auto* j = dynamic_cast<MotionTexture*>(getTexture()))
        texture.multi.push_back(*j->current());

    else if (auto* k = dynamic_cast<Texture*>(getTexture()))
        texture.multi.push_back(*k);

    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().x != 1.0f || getScale().y != 1.0f || getScale().z != 1.0f)
        glScalef(getScale().x, getScale().y, getScale().z);

    //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, texture.id);
    //Texture unit mode.
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

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


    int i = 0;
    for(auto& t : texture.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);
            glBindTexture(GL_TEXTURE_2D, t.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) {}

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());
    //TODO this won't look right.
    if (getScale().x != 1)
        aabb.minPoint *= scale.x,
        aabb.maxPoint *= scale.x;

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

Sphere Entity::getSphere() {
    Sphere sphere = Collision::genMinimallyEnclosingSphere(getGeometry());
    //TODO this won't look right.
    sphere.Radius *= scale.x;
    sphere.Position = position;
    return sphere;
}

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

Vector3 Entity::getAngle() {
    return {Math::Degrees(Math::Radians(angle.x)), Math::Degrees(Math::Radians(angle.y)), Math::Degrees(Math::Radians(angle.z))};
}

void Entity::setAngle(const Vector3 &a) {
    angle = a;
}

void Entity::setAngle(float pitch, float yaw, float roll) {
    angle = {pitch, yaw, roll};
}

Direction Entity::getAngleDirection() {
    Direction a = Vector3::Direction(getAngle());
    //These two are reversed when moving things in the Re3D scene.
    //-Z is forward.
    a.x = -a.x;
    a.z = -a.z;
    return a;
}
