#include <BlockRegistry/BlockRegistry.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Item/IEquipment.hpp>
#include <Material/Material.hpp>
#include <RemoteMap/RemoteMap.hpp>
#include <ToolTip/ToolTip.hpp>
#include <Utilities/CircularInt.hpp>
#include <Utilities/RGBABytes.hpp>
#include <Utilities/Utilities.hpp>
#include <Widget/MapView/MapView.hpp>

void MapView::drawLandMark(GraphicsContext &context, Material &icon, wvec2 pos, const std::string &tooltip)
{
    wvec2 absolutePos = pos;
    wvec2 refPoint = wvec2(cameraPos) + wvec2(m_size) / wvec2(2);

    cint32 minX = cameraPos.x + icon.width() / 2;
    cint32 maxX = cameraPos.x + m_size.x - icon.width() / 2;

    cint32 minY = cameraPos.y + icon.height() / 2;
    cint32 maxY = cameraPos.y + m_size.y - icon.height() / 2;

    if (pos.x < minX)
    {
        pos = interpolateLine(pos, refPoint, minX, 0);
    }

    if (pos.x > maxX)
    {
        pos = interpolateLine(pos, refPoint, maxX, 0);
    }

    if (pos.y < minY)
    {
        pos = interpolateLine(pos, refPoint, minY, 1);
    }

    if (pos.y > maxY)
    {
        pos = interpolateLine(pos, refPoint, maxY, 1);
    }

    glm::ivec2 landMarkPos = pos - wvec2(cameraPos) - wvec2(icon.width() / 2, icon.height() / 2);
    landMarkPos.y = m_size.y - 1 - landMarkPos.y - icon.height();

    context.color = glm::vec4(1.0f);
    context.drawMaterial(icon, landMarkPos.x, landMarkPos.y);

    if (
        m_mousePos.x >= landMarkPos.x && m_mousePos.x < landMarkPos.x + icon.width() &&
        m_mousePos.y >= landMarkPos.y && m_mousePos.y < landMarkPos.y + icon.height() &&
        !tooltip.empty()
    )
    {
        ToolTip &tip = context.createToolTip();
        tip.addLine(glm::vec4(1.0f), tooltip);

        if (envVars.devMode)
        {
            tip.addLine(formatString("$+", absolutePos));
        }
    }
}

MapView::MapView(
    WorldDisplay &worldDisplay,
    RemoteMap &remoteMap,
    Player &player,
    MapTool* &tool
) :
    remoteMap(remoteMap),
    tool(tool),
    player(player)
{
    remoteMap.setTileUpdatedCallback([this, &worldDisplay](wvec2 base, const RemoteMap::Tile &tileRef, cint32 targetZ)
    {
        RemoteMap::Tile tile = tileRef;
        materialQueue.push_back([this, &worldDisplay, base, tile, targetZ]()
        {
            uint64_t key = RemoteMap::coords2key(base);

            RGBABytes grid[16 * 16];
            for (size_t i = 0 ; i < 16 * 16; i++)
            {
                const RemoteMap::Cell &cell = tile.cells[i];
                RGBABytes &color = grid[i];

                float level = (float) (cell.z - targetZ) / (float) (2 * rangeZ - 1);
                float brightness = 0.2f + 0.8f * level;

                glm::vec4 mapColor = (cell.blockId == 0xFFFFFFFF) ?
                    glm::vec4(1.0f, 0.5f, 0.1f, 1.0f) :
                    worldDisplay.getWorld()->getBlockById(cell.blockId).mapColor;

                color.red = brightness * mapColor.r * (float) 0xFF;
                color.green = brightness * mapColor.g * (float) 0xFF;
                color.blue = brightness * mapColor.b * (float) 0xFF;
                color.alpha = mapColor.a * (float) 0xFF;
            }

            GraphicsContext::instance->holdResource(tileMaterials[key]);
            tileMaterials[key] = GraphicsContext::instance->createMaterialFromPixels(glm::ivec2(16, 16), grid);
        });
    });
}

MapView::~MapView()
{
    for (const auto &[_, material] : tileMaterials)
    {
        GraphicsContext::instance->holdResource(material);
    }

    remoteMap.setTileUpdatedCallback([](wvec2, const RemoteMap::Tile &, cint32){});
}

glm::ivec2 MapView::getHoveredMapPos()
{
    glm::ivec2 flippedPos = m_mousePos;
    flippedPos.y = m_size.y - 1 - flippedPos.y;
    return flippedPos + glm::ivec2(cameraPos);
}

glm::ivec2 MapView::getMinSize()
{
    return glm::ivec2(64, 64);
}

void MapView::draw(GraphicsContext &context)
{
    if (isMouseInside())
    {
        Widget::setCursor(tool->getIcon(), glm::ivec2(4));
    }

    if (lastCameraMinBound.x == lastCameraMaxBound.x || shouldTrackPlayer)
    {
        cameraPos = wvec2(glm::floor(player.pos)) - wvec2(m_size / 2);
    }

    wvec2 minBound = cameraPos & wvec2(~0xF);
    wvec2 maxBound = (cameraPos + wvec2(m_size) + wvec2(0xF)) & wvec2(~0xF);
    
    if (minBound != lastCameraMinBound || maxBound != lastCameraMaxBound)
    {
        remoteMap.setCameraBounds(minBound, maxBound);
        lastCameraMinBound = minBound;
        lastCameraMaxBound = maxBound;
    }

    for (auto &callback : materialQueue)
    {
        callback();
    }

    materialQueue.clear();

    // Remove out-of-bounds tiles.
    auto it = tileMaterials.begin();
    while (it != tileMaterials.end())
    {
        wvec2 pos = RemoteMap::key2coords(it->first);
        if (pos.x < minBound.x || pos.x >= maxBound.x || pos.y < minBound.y || pos.y >= maxBound.y)
        {
            context.holdResource(it->second);
            it = tileMaterials.erase(it);
        }
        else
        {
            it++;
        }
    }

    context.color = glm::vec4(1.0f);

    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 = RemoteMap::coords2key(pos);
            auto materialIt = tileMaterials.find(key);
            
            if (materialIt != tileMaterials.end())
            {
                wvec2 drawAt = pos - cameraPos;
                drawAt.y = m_size.y - 1 - drawAt.y - 16;
                context.drawMaterial(*materialIt->second, drawAt.x, drawAt.y);
            }
        }
    }

    if ((SDL_GetTicks() / playerBlinkPeriod) & 1)
    {
        drawLandMark(context, Material::kevinAvatar, glm::ivec2(glm::floor(player.pos)));
    }

    drawLandMark(context, Material::spawnPoint, glm::ivec2(0), "Spawn");

    for (const auto &pin : player.mapPins)
    {
        if (!pin.switchedOn)
        {
            continue;
        }

        drawLandMark(context, Material::mapPin, pin.pos, pin.label);
    }

    glm::dvec2 forward2D = glm::normalize(glm::vec2(player.getForwardVector()));
    glm::ivec2 ptrPos = glm::ivec2(glm::floor(glm::dvec2(player.pos) + 8.0 * forward2D));
    drawLandMark(context, Material::crosshair, ptrPos);
}

void MapView::onMouseDown(Uint8 button)
{
    if (isMouseInside())
    {
        tool->onMouseDown(*this, button, m_mousePos);
    }

    Widget::onMouseDown(button);
}

void MapView::onMouseUp(Uint8 button)
{
    if (isMouseInside())
    {
        tool->onMouseUp(*this, button, m_mousePos);
    }

    Widget::onMouseUp(button);
}

void MapView::onMouseMoved(glm::ivec2 pos)
{
    Widget::onMouseMoved(pos);
    tool->onMouseMoved(*this, pos);
}

void MapView::openPopupAtCursor(glm::ivec2 offset, Widget *popup)
{
    openPopup(m_mousePos + offset, popup);
}

void MapView::placePin(const std::string &label, glm::ivec2 pos)
{
    remoteMap.placePin(label, pos);
}

MapView* MapView::withPlayerTracking()
{
    shouldTrackPlayer = true;
    return this;
}