#include <ChunkHandle/ChunkHandle.hpp>
#include <ScriptEngine/ScriptEngine.hpp>
#include <World/LocalWorld/ScriptedWorld/ScriptedWorld.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <string>
#include <unordered_map>

ScriptedWorld::Agent::Agent(
    ScriptedWorld &world,
    std::string scriptPath,
    glm::ivec3 pos,
    Side sideFacing,
    const std::unordered_map<std::string, std::string> &params
) : world(world),
    pos(pos),
    sideFacing(sideFacing),
    engine()
{
    engine.addFunc(
        "place_block",
        [this](int dx, int dy, int dz, std::string blockName) -> ScriptEngine::Async<void>
        {
            glm::ivec3 npos;
            bool allReady = true;
            for (npos.z = -1; npos.z <= 1; npos.z++)
            {
                for (npos.y = -1; npos.y <= 1; npos.y++)
                {
                    for (npos.x = -1; npos.x <= 1; npos.x++)
                    {
                        ChunkHandle chunk = this->world.requestChunk(
                            (this->pos + glm::ivec3(dx, dy, dz) + npos) & glm::ivec3(~0xF)
                        );

                        allReady = allReady && chunk->isReady();
                    }
                }
            }

            if (!allReady)
            {
                return ScriptEngine::Async<void>::yield();
            }

            glm::ivec3 targetPos = this->pos + glm::ivec3(dx, dy, dz);
            if (!this->world.setBlockAt(targetPos, blockName, 0))
            {
                return ScriptEngine::Async<void>::yield();
            }

            return ScriptEngine::Async<void>::done();
        }
    );

    engine.addFunc(
        "move_by",
        [this](int dx, int dy, int dz) -> void
        {
            this->pos += glm::ivec3(dx, dy, dz);
        }
    );

    engine.addFunc("move_forward", [this](int steps) -> void
    {
        this->pos += this->sideFacing.out() * steps;
    });

    engine.addFunc("turn_left", [this](int turns) -> void
    {
        this->sideFacing = this->sideFacing.rotate(Side::Top, -turns);
    });

    engine.addFunc("turn_right", [this](int turns) -> void
    {
        this->sideFacing = this->sideFacing.rotate(Side::Top, turns);
    });

    engine.addFunc("spawn_agent", [this](
        std::string type,
        int x, int y, int z,
        std::unordered_map<std::string, std::string> params
    )
    {
        this->world.spawnAgent(type, this->pos + glm::ivec3(x, y, z), this->sideFacing, params);
    });

    engine.push(params);
    engine.setGlobal("agent_params");

    if (!engine.loadFromFile(scriptPath))
    {
        Log::error(formatString("Failed to load agent script '$+': $+", scriptPath, engine.pop<std::string>()));
        ok = false;
    }
}

bool ScriptedWorld::Agent::update()
{
    if (!ok)
    {
        return false;
    }

    auto result = engine.resume();
    if (result.hasYielded())
    {
        return true;
    }

    if (!result.get())
    {
        Log::error(formatString("Agent script error: $+", engine.pop<std::string>()));
        return false;
    }

    return false;
}

bool ScriptedWorld::Agent::isOK() const
{
    return ok;
}

void ScriptedWorld::spawnAgent(
    std::string type,
    glm::ivec3 pos,
    Side sideFacing,
    const std::unordered_map<std::string, std::string> &params
)
{
    Log::info(formatString("Spawned agent $+ at $+", type, pos));
    std::string scriptPath = formatString("$+/agents/$+.lua", scriptDir, type);
    agents.push_back(std::make_unique<Agent>(*this, scriptPath, pos, sideFacing, params));
}

ScriptedWorld::ScriptedWorld(
    std::string worldDir,
    std::string scriptDir,
    std::string initialAgentType,
    glm::ivec3 agentStartPos
) : LocalWorld(worldDir),
    scriptDir(scriptDir)
{
    if (!fatalError.empty())
    {
        return;
    }

    std::unordered_map<std::string, std::string> params;
    spawnAgent(initialAgentType, agentStartPos, Side::North, params);
}

bool ScriptedWorld::isDone()
{
    return agents.empty();
}

void ScriptedWorld::sendMessageToEntity(uint64_t id, const void *msg, size_t size)
{
    deliverEntityMessage(id, msg, size);
}

void ScriptedWorld::sendCreateEntityMessageToClients(std::shared_ptr<Entity> entity)
{
    // NOP
}

void ScriptedWorld::sendDeleteEntityMessageToClients(uint64_t id)
{
    // NOP
}

void ScriptedWorld::onPlayerUpdateOver(Player &player)
{
    // NOP
}

void ScriptedWorld::drawBlockSelector(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player)
{
    // NOP
}

void ScriptedWorld::onPlayerMessage(Player &player, const void *msg, size_t size)
{
    // NOP
}

void ScriptedWorld::playSound(Sound &sound, glm::ivec3 blockPos, glm::vec3 vel)
{
    sound.play(glm::vec3(blockPos) + glm::vec3(0.5f), vel);
}

void ScriptedWorld::fastUpdate(ChunkWindow *chunkWindow)
{
    // NOP
}

void ScriptedWorld::update()
{
    LocalWorld::update();

    for (auto it = agents.begin(); it != agents.end(); )
    {
        if (!(*it)->update())
        {
            if (!(*it)->isOK())
            {
                fatalError = "A script agent has encountered an error.";
                break;
            }

            it = agents.erase(it);
        }
        else
        {
            ++it;
        }
    }
}

std::shared_ptr<Player> ScriptedWorld::getPlayer(const std::string &userName)
{
    return nullptr;
}