#include "./Registry.hpp"
#include <algorithm>
#include <limits>
#include <filesystem>
#include <dlfcn.h>
#include "modules/core/Core.hpp"
#include "../utils/logger.hpp"

using namespace world::module;

owner<Registry*> Registry::sInstance = nullptr;
void Registry::Load() {
    assert(sInstance == nullptr);
    sInstance = new Registry();
}
Registry::Registry() {
    if (sInstance == nullptr)
        sInstance = this;

    modules.emplace_back(nullptr, new module::core::Core());

    std::filesystem::create_directories("./modules");
    for(const auto& entry: std::filesystem::directory_iterator("./modules")) {
        if(entry.path().extension() == ".mod") {
            const auto handle = dlopen(entry.path().c_str(), RTLD_NOW);
            if (!handle) {
                LOG_E("Failed to open library" << entry.path() << " : " << dlerror());
                continue;
            }

            const auto loader = (Module::load_fn*) dlsym(handle, "loadModule");
            if (!loader) {
                LOG_E("Failed to load library" << entry.path() << " : " << dlerror());
                continue;
            }
            [[maybe_unused]]
            const auto instance = modules.emplace_back(handle, loader()).instance;
            LOG_T("Module " << instance->getName() << " registred");
        }
    }

    auto loaded = std::vector<bool>(modules.size(), false);
    bool progress = false;
    do {
        progress = false;
        for (size_t id = 0; id < modules.size(); id++) {
            if (loaded[id])
                continue;

            const auto inst = modules.at(id).instance;
            if([&]{
                auto reqs = inst->getRequirements();
                while (*reqs) {
                    const auto req = std::string(reqs);
                    auto mod_id = findModuleId(req);
                    if (!mod_id) {
                        LOG_E("Module " << inst->getName() << " requires missing module " << req);
                        return false;
                    }
                    if (!loaded[mod_id.value().val]) {
                        return false;
                    }
                    reqs += req.length() + 1;
                }
                return true;
            }()) {
                inst->setup(Loader{id, this});
                LOG_D("Module " << inst->getName() << " loaded");
                loaded[id] = true;
                progress = true;
            }
        }
    } while (progress);
    {
        std::string cycle = "";
        for (size_t id = 0; id < modules.size(); id++) {
            if (!loaded[id]) {
                cycle += modules[id].instance->getName();
                cycle += ',';
            }
        }
        if (!cycle.empty()) {
            cycle.pop_back();
            LOG_E("Modules circular dependencies between " << cycle);
        }
    }
}

void Registry::Unload() {
    assert(sInstance != nullptr);
    delete sInstance;
    sInstance = nullptr;
}
Registry::ModHandle::~ModHandle() {
    if(instance)
        delete instance;
    if(handle) {
        dlclose(handle);
        handle = nullptr;
    }
}

std::optional<module_id> Registry::findModuleId(const std::string& name) const {
    for (size_t id = 0; id < modules.size(); id++) {
        if (modules[id].instance->getName() == name)
            return id;
    }
    return std::nullopt;
}
std::optional<material_id> Registry::findMaterialId(module_id mod_id, const std::string& mat) const {
    for (size_t id = 0; id < _materials.size(); id++) {
        if (_materials.module_ids[id] == mod_id.val && _materials.names[id] == mat)
            return id;
    }
    return std::nullopt;
}
std::optional<material_id> Registry::findMaterialId(const std::string& mod, const std::string& mat) const {
    if (const auto mod_id = findModuleId(mod))
        return findMaterialId(mod_id.value(), mat);
    return std::nullopt;
}
std::optional<texture_id> Registry::findTextureId(const std::string& path) const {
    for (size_t id = 0; id < _textures.size(); id++) {
        if (_textures[id] == path)
            return id;
    }
    return std::nullopt;
}
std::optional<texture_id> Registry::findTextureId(material_id mat_id) const {
    if (mat_id.val < _materials.size()) {
        return _materials.texture_ids[mat_id.val];
    }
    return std::nullopt;
}
std::optional<model_id> Registry::findModelId(module_id mod_id, const std::string& model) const {
    for (size_t id = 0; id < _models.size(); id++) {
        if (_models[id].module_id == mod_id.val && _models[id].name == model)
            return id;
    }
    return std::nullopt;
}
std::optional<model_id> Registry::findModelId(const std::string& module, const std::string& model) const {
    if (const auto mod_id = findModuleId(module))
        return findModelId(mod_id.value(), model);
    return std::nullopt;
}
std::optional<generator_id> Registry::findGeneratorId(module_id mod_id, const std::string& gen) const {
    for (size_t id = 0; id < _generators.size(); id++) {
        if (_generators[id].module_id == mod_id.val && _generators[id].name == gen)
            return id;
    }
    return std::nullopt;
}
std::optional<generator_id> Registry::findGeneratorId(const std::string& mod, const std::string& gen) const {
    if (const auto mod_id = findModuleId(mod))
        return findGeneratorId(mod_id.value(), gen);
    return std::nullopt;
}
owner<world::generator::Abstract*> Registry::createGenerator(generator_id genId, generator::params ps) const {
    if (genId.val < _generators.size()) {
        return _generators[genId.val].factory(ps);
    }
    return nullptr;
}

void Registry::reserveMaterials(size_t n) {
    assert(n < (std::numeric_limits<ushort>::max() >> 4) && "for byte packing see Voxel");
    _materials.reserve(n);
}
material_id Registry::addMaterial(module_id mod_id, const material& m) {
    assert(!findMaterialId(mod_id, m.name));
    auto tex_id = findTextureId(m.texture);
    if (!tex_id) {
        tex_id = _textures.size();
        _textures.push_back(m.texture);
    }
    return _materials.push(m, mod_id.val, tex_id.value().val);
}

model_id Registry::addModel(module_id mod_id, const model& m) {
    assert(!findModelId(mod_id, m.name));
    auto id = _models.size();
    _models.emplace_back(m, mod_id.val);
    return id;
}
generator_id Registry::addGenerator(module_id mod_id, const generator& g) {
    assert(!findGeneratorId(mod_id, g.name));
    auto id = _generators.size();
    _generators.emplace_back(g, mod_id.val);
    return id;
}