#include <ReTexture/rTexture.h>
#include <Redacted3D/types/texture.h>
#include <Redacted3D/engine/engine.h>

void Texture::load(Entity* entity, const std::string& file, bool storeOnTextureList) {
    auto* texture = new RTexture(file, {RTextureFlag::INVERT_Y});

    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);
    if (texture->format == RTextureFormat::RGBA)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture->width, texture->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture->pixelData.data());
    if (texture->format == RTextureFormat::RGB)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture->width, texture->height, 0, GL_RGB, GL_UNSIGNED_BYTE, texture->pixelData.data());
    delete texture;

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    //If we can't load the missing texture
    if (file == "assets/textures/missing.png" && id == 0)
        engine->setError(ENGINE_ERROR_CODE::TEXTURE_NOT_FOUND, true);

    if (id == 0)
        engine->setError(ENGINE_ERROR_CODE::TEXTURE_NOT_FOUND, false),
        load(entity, "assets/textures/missing.png", false);

    usedBy.push_back(entity);
    if (storeOnTextureList)
        engine->world->textureList.push_back(new Texture(*this));
}

void Texture::erase() {
    Texture::erase(this);
}

Texture::Texture(Entity* entity, const char *filePath, bool storeOnTextureList) {
    load(entity, filePath, storeOnTextureList);
}

void Texture::erase(Texture* texture) const {
    glDeleteTextures(1, &texture->id);

    //If texture is being deleted while in use.
    //It won't crash the program. But it'll cause the texture to be reloaded from disk unnecessarily.
    if (!usedBy.empty())
        engine->setError(ENGINE_ERROR_CODE::TEXTURE_ERASED_WHILE_IN_USE, false);

    if (auto* t = dynamic_cast<MultiTexture*>(texture))
        for (auto &m : t->multi)
            glDeleteTextures(1, &m.id);

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

MultiTexture::MultiTexture(Entity* entity, const std::vector<std::string>& textures, bool storeOnTextureList) {
    Texture base;
    for (const auto& t : textures) {
        if (id != 0)
            multi.emplace_back(entity, t.c_str(), false);
        else
            base = Texture(entity, t.c_str(), false),
            id = base.id,
            usedBy = base.usedBy;
    }

    if (storeOnTextureList)
        engine->world->textureList.push_back(new MultiTexture(*this));

    if (multi.size() > 6)
        engine->setError(ENGINE_ERROR_CODE::MULTI_TEXTURE_SIZE_EXCEEDS, false);
}

void MultiTexture::erase() {
    Texture::erase(this);
}

MotionTexture::MotionTexture(Entity* entity, const std::vector<std::string>& textures, u16 msBetweenFrames, bool storeOnTextureList, bool doAnim) {
    msDelay = msBetweenFrames;
    this->doAnim = doAnim;
    Texture base(entity, textures[0].c_str(), false);
    std::vector<std::string> extra(textures.begin()+1, textures.end());

    if(!extra.empty())
        for (auto& t : extra)
            motion.emplace_back(entity, t.c_str(), false);

    id = base.id;
    usedBy = base.usedBy;
    if (storeOnTextureList)
        engine->world->textureList.push_back(new MotionTexture(*this));
}

Texture* MotionTexture::current() {
    //If animation is disabled.
    if (!doAnim)
        return this;

    //If it's our first time getting the current texture
    if (lastUpdate == std::chrono::high_resolution_clock::time_point()) {
        lastUpdate = std::chrono::high_resolution_clock::now();
        lastDisplayedIndex = 0;
        return this;
    }

    auto now = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastUpdate).count();

    if (elapsed >= msDelay) {
        lastDisplayedIndex++;
        if (lastDisplayedIndex > motion.size())
            lastDisplayedIndex = 0;

        lastUpdate = now;
    }

    if (lastDisplayedIndex == 0)
        return this;

    return &motion[lastDisplayedIndex - 1];
}

void MotionTexture::advance() {
    lastDisplayedIndex++;
    if (lastDisplayedIndex > motion.size())
        lastDisplayedIndex = 0;
}

void MotionTexture::toggleAnim() {
    doAnim = !doAnim;
}

Texture* MotionTexture::base() {
    return this;
}
