#include <Item/IEquipment.hpp>
#include <RemoteMap/RemoteMap.hpp>
#include <World/LocalWorld/LocalWorld.hpp>

RemoteMap::MapTileHandle::MapTileHandle(LocalWorld &world, IEquipment::MapLevel mapLevel, wvec2 base, cint32 startZ, cint32 endZ) :
    base(base),
    startZ(startZ),
    endZ(endZ),
    world(world),
    mapLevel(mapLevel)
{
    // TODO: Check if tile is in range.
    wvec3 pos = wvec3(base, 0);
    for (pos.z = startZ; pos.z < endZ; pos.z+=16)
    {
        chunks.push_back(world.requestChunk(pos));
    }
}

bool RemoteMap::MapTileHandle::isReady()
{
    for (auto &chunk : chunks)
    {
        if (!chunk->isReady())
        {
            return false;
        }
    }

    return true;
}

std::unique_ptr<RemoteMap::Tile> RemoteMap::MapTileHandle::toTile()
{
    auto tile = std::make_unique<Tile>();

    glm::ivec3 offset;
    for (offset.y = 0; offset.y < 16; offset.y++)
    {
        for (offset.x = 0; offset.x < 16; offset.x++)
        {
            Cell &cell = tile->cells[(15 - offset.y) * 16 + offset.x];

            for (auto &chunk : chunks)
            {
                for (offset.z = cint32::rightmost(startZ - chunk->base.z, 0); offset.z < cint32::leftmost(endZ - chunk->base.z, 16); offset.z++)
                {
                    int blockId = std::get<0>(chunk->getBlockAndMeta(offset));

                    if (blockId != 0)
                    {
                        if (mapLevel < IEquipment::MapLevel::PaperMap)
                        {
                            cell.blockId = 0xFFFFFFFF;
                        }
                        else
                        {
                            cell.blockId = blockId;
                        }

                        cell.z = chunk->base.z + offset.z;
                    }
                }
            }
        }
    }

    return tile;
}

void RemoteMap::loadTilesInRange(wvec2 minBound, wvec2 maxBound)
{
    wvec2 pos(0, 0);
    for (pos.y = minBound.y; pos.y < maxBound.y; pos.y+=16)
    {
        for (pos.x = minBound.x; pos.x < maxBound.x; pos.x+=16)
        {
            uint64_t key = coords2key(pos);
            auto [_, inserted] = mapTiles.try_emplace(key, *localWorld, level, pos, targetZ, targetZ + 2 * rangeZ);

            if (inserted)
            {
                pendingKeys.push_back(key);
            }
        }
    }
}

uint64_t RemoteMap::coords2key(wvec2 coords)
{
    return ((uint64_t) coords.x << 32) | ((uint64_t) (uint32_t) coords.y);
}

wvec2 RemoteMap::key2coords(uint64_t key)
{
    return wvec2(key >> 32, key);
}

RemoteMap::RemoteMap(Controller *ctrl) :
    updateLevel(ctrl, Controller::RunsOn::Client,
        [this](IEquipment::MapLevel level) -> uint32_t
    {
        this->level = level;
        return 0;
    }),
    updateTile(ctrl, Controller::RunsOn::Client,
        [this](glm::uvec2 base, Tile tile, cint32 targetZ) -> uint32_t
    {
        onTileUpdated(base, tile, targetZ);
        return 0;
    }),
    setCameraBounds(ctrl, Controller::RunsOn::Server,
        [this](glm::uvec2 newMinBound_, glm::uvec2 newMaxBound_) -> uint32_t
    {
        wvec2 newMinBound = newMinBound_;
        wvec2 newMaxBound = newMaxBound_;
        if (cameraMinBound == newMinBound && cameraMaxBound == newMaxBound)
        {
            return 0;
        }

        if (newMinBound.x & 0xF || newMinBound.y & 0xF || newMaxBound.x & 0xF || newMaxBound.y & 0xF)
        {
            return 1;
        }

        uint32_t width = newMaxBound.x - newMinBound.x;
        uint32_t height = newMaxBound.y - newMinBound.y;

        if (width > 64*16 || height > 64*16)
        {
            return 1;
        }

        // Remove out-of-bounds tiles.
        auto it = mapTiles.begin();
        while (it != mapTiles.end())
        {
            wvec2 pos = key2coords(it->first);
            if (pos.x < newMinBound.x || pos.x >= newMaxBound.x || pos.y < newMinBound.y || pos.y >= newMaxBound.y)
            {
                it = mapTiles.erase(it);
            }
            else
            {
                it++;
            }
        }

        // Now load in the missing tiles.
        loadTilesInRange(newMinBound, newMaxBound);
        cameraMinBound = newMinBound;
        cameraMaxBound = newMaxBound;

        return 0;
    }),
    placePin(ctrl, Controller::RunsOn::Server,
        [this](std::string label, glm::ivec2 pos) -> uint32_t
    {
        player->mapPins.push_back({label, pos, true});
        player->onMapPinsUpdated();

        return 0;
    }),
    setPinSwitch(ctrl, Controller::RunsOn::Server,
        [this](uint32_t index, bool value) -> uint32_t
    {
        player->mapPins[index].switchedOn = value;
        player->onMapPinsUpdated();

        return 0;
    }),
    deletePin(ctrl, Controller::RunsOn::Server,
        [this](uint32_t index) -> uint32_t
    {
        if (index >= player->mapPins.size())
        {
            return 1;
        }

        player->mapPins.erase(player->mapPins.begin() + index);
        player->onMapPinsUpdated();
        return 0;
    })
{
    player = ctrl->getPlayer();
    localWorld = dynamic_cast<LocalWorld*>(player->world);
    targetZ = std::floor(player->pos.z) - rangeZ;
}

void RemoteMap::init(IEquipment::MapLevel level)
{
    this->level = level;
    updateLevel(level);
}

void RemoteMap::update()
{
    if (localWorld == nullptr)
    {
        return;
    }

    // If the player's Z layer changed, reload the map.
    int newTargetZ = std::floor(player->pos.z) - rangeZ;
    if (targetZ != newTargetZ)
    {
        targetZ = newTargetZ;

        pendingKeys.clear();
        mapTiles.clear();
        loadTilesInRange(cameraMinBound, cameraMaxBound);
    }

    // Send over any tiles that are done loading.
    auto it = pendingKeys.begin();
    while (it != pendingKeys.end())
    {
        uint64_t key = *it;

        auto tileIt = mapTiles.find(key);
        if (tileIt == mapTiles.end())
        {
            it = pendingKeys.erase(it);
            continue;
        }

        MapTileHandle &tileHandle = tileIt->second;
        if (!tileHandle.isReady())
        {
            it++;
            continue;
        }

        auto tile = tileHandle.toTile();
        updateTile(tileHandle.base, *tile, cint32(targetZ));

        it = pendingKeys.erase(it);
    }
}

void RemoteMap::setTileUpdatedCallback(TileUpdatedCallback callback)
{
    onTileUpdated = callback;
}

IEquipment::MapLevel RemoteMap::getMapLevel()
{
    return level;
}