#include "DimensionMap.h"

#include <DateiSystem.h>

#include "Constants.h"
#include "Globals.h"
#include "World.h"

DimensionMap::DimensionMap(MapOptions* zOptions)
    : ZeichnungHintergrund(),
      zOptions(zOptions),
      originChunkCenter(0, 0),
      scrollOffset(0, 0),
      chunkCount(0),
      pixelsPerBlock(16),
      requestCount(0),
      drag(0),
      nextPlayersRequest(-1)
{
    setStyle(Style::Sichtbar | Style::Erlaubt);
    chunks = new Framework::RCTrie<ChunkMap>();
    setMausEreignis(_ret1ME);
    requestNextChunk();

    char msg[2];
    msg[0] = 2; // subscribe to map changes
    msg[1] = 1;
    World::INSTANCE->zClient()->dimensionAPIRequest(msg, 2);

    LTDBDatei iconsDat;
    iconsDat.setDatei(new Text("data/images/gui_icons.ltdb"));
    iconsDat.leseDaten(0);

    playerIcon = iconsDat.laden(0, new Text("player.png"));
}

DimensionMap::~DimensionMap()
{
    char msg[2];
    msg[0] = 2; // unsubscribe from map changes
    msg[1] = 2;
    World::INSTANCE->zClient()->dimensionAPIRequest(msg, 2);
    chunks->release();
    playerIcon->release();
}

void DimensionMap::getAddrOf(Punkt cPos, char* addr) const
{
    *(int*)addr = cPos.x;
    *((int*)addr + 1) = cPos.y;
}

void DimensionMap::getAddrOfWorld(Punkt wPos, char* addr) const
{
    // needed because otherwise would (-8, -8) have the same
    // adress as (8, 8)
    if (wPos.x < 0) wPos.x -= CHUNK_SIZE;
    if (wPos.y < 0) wPos.y -= CHUNK_SIZE;

    wPos /= CHUNK_SIZE;
    getAddrOf(wPos, addr);
}

Framework::Punkt DimensionMap::getMinVisibleChunkCenter(
    Framework::Punkt& screenPos) const
{
    screenPos = getSize() / 2 - scrollOffset;
    Punkt currentChunkCenter = originChunkCenter;
    while (screenPos.x + pixelsPerBlock * (CHUNK_SIZE / 2) >= 0)
    {
        screenPos.x -= pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.x -= CHUNK_SIZE;
    }
    while (screenPos.y + pixelsPerBlock * (CHUNK_SIZE / 2) >= 0)
    {
        screenPos.y -= pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.y -= CHUNK_SIZE;
    }
    while (screenPos.x + pixelsPerBlock * (CHUNK_SIZE / 2) < 0)
    {
        screenPos.x += pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.x += CHUNK_SIZE;
    }
    while (screenPos.y + pixelsPerBlock * (CHUNK_SIZE / 2) < 0)
    {
        screenPos.y += pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.y += CHUNK_SIZE;
    }
    return currentChunkCenter;
}

Framework::Punkt DimensionMap::getMaxVisibleChunkCenter(
    Framework::Punkt& screenPos) const
{
    screenPos = getSize() / 2 - scrollOffset;
    Punkt currentChunkCenter = originChunkCenter;
    while (screenPos.x - pixelsPerBlock * (CHUNK_SIZE / 2) < getBreite())
    {
        screenPos.x += pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.x += CHUNK_SIZE;
    }
    while (screenPos.y - pixelsPerBlock * (CHUNK_SIZE / 2) < getHeight())
    {
        screenPos.y += pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.y += CHUNK_SIZE;
    }
    while (screenPos.x - pixelsPerBlock * (CHUNK_SIZE / 2) >= getBreite())
    {
        screenPos.x -= pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.x -= CHUNK_SIZE;
    }
    while (screenPos.y - pixelsPerBlock * (CHUNK_SIZE / 2) >= getHeight())
    {
        screenPos.y -= pixelsPerBlock * CHUNK_SIZE;
        currentChunkCenter.y -= CHUNK_SIZE;
    }
    return currentChunkCenter;
}

void DimensionMap::removeUnused() const
{
    Punkt tmp;
    Framework::Punkt min
        = getMinVisibleChunkCenter(tmp) - Punkt(CHUNK_SIZE, CHUNK_SIZE) * 5;
    Framework::Punkt max
        = getMaxVisibleChunkCenter(tmp) + Punkt(CHUNK_SIZE, CHUNK_SIZE) * 5;
    char addr[8];
    for (auto i = chunkList.begin(); i;)
    {
        if (i->getChunkCenter().x < min.x || i->getChunkCenter().y < min.y
            || i->getChunkCenter().x > max.x || i->getChunkCenter().y > max.y)
        {
            getAddrOfWorld(i->getChunkCenter(), addr);
            chunks->remove(addr, 8);
            i.remove();
        }
        else
        {
            ++i;
        }
    }
}

void DimensionMap::updatePlayers(char* data)
{
    int count = *(int*)data;
    data += 4;
    cs.lock();
    players.leeren();
    // read player information from data buffer
    for (int i = 0; i < count; i++)
    {
        unsigned char nameLen = (unsigned char)*data;
        data++;
        char* name = new char[nameLen + 1];
        memcpy(name, data, nameLen);
        name[nameLen] = 0;
        data += nameLen;
        MapPlayer player;
        player.name = name;
        delete[] name;
        player.position.x = *(float*)data;
        player.position.y = *(float*)(data + 4);
        player.position.z = *(float*)(data + 8);
        data += 12;
        players.add(player);
    }
    cs.unlock();
}

void DimensionMap::requestNextChunk()
{
    cs.lock();
    if (requestCount >= 20)
    {
        cs.unlock();
        return;
    }
    if (chunkCount == 0)
    {
        requestCount++;
        Vec3<float> playerPos
            = World::INSTANCE->getCurrentPlayerEntity()->getPos();
        char msg[10];
        msg[0] = 2;
        msg[1] = 0;
        *(int*)(msg + 2) = (int)playerPos.x;
        *(int*)(msg + 6) = (int)playerPos.y;
        World::INSTANCE->zClient()->dimensionAPIRequest(msg, 10);
    }
    else
    {
        while (requestCount < 20)
        {
            Punkt minScreenPos;
            Punkt minVisibleChunk = getMinVisibleChunkCenter(minScreenPos);
            Punkt maxScreenPos;
            Punkt maxVisibleChunk = getMaxVisibleChunkCenter(maxScreenPos);
            Punkt screenPos = minScreenPos;
            Punkt screenCenter = getSize() / 2;
            double minDist = -1;
            Punkt resultChunk(0, 0);
            char addr[8];
            for (int x = minVisibleChunk.x; x <= maxVisibleChunk.x;
                 x += CHUNK_SIZE)
            {
                for (int y = minVisibleChunk.y; y <= maxVisibleChunk.y;
                     y += CHUNK_SIZE)
                {
                    getAddrOfWorld({x, y}, addr);
                    if (!chunks->z(addr, 8))
                    {
                        if (minDist < 0
                            || (screenCenter - screenPos).getLengthSq()
                                   < minDist)
                        {
                            minDist = (screenCenter - screenPos).getLengthSq();
                            resultChunk = {x, y};
                        }
                    }
                    screenPos.y += pixelsPerBlock * CHUNK_SIZE;
                }
                screenPos.x += pixelsPerBlock * CHUNK_SIZE;
                screenPos.y = minScreenPos.y;
            }
            if (minDist >= 0)
            {
                requestCount++;
                char msg[10];
                msg[0] = 2;
                msg[1] = 0;
                *(int*)(msg + 2) = (int)resultChunk.x;
                *(int*)(msg + 6) = (int)resultChunk.y;
                World::INSTANCE->zClient()->dimensionAPIRequest(msg, 10);
                getAddrOfWorld({resultChunk.x, resultChunk.y}, addr);
                chunks->set(addr, 8, new ChunkMap(resultChunk));
            }
            else
            {
                break;
            }
        }
    }
    cs.unlock();
}

void DimensionMap::addChunk(ChunkMap* chunk)
{
    cs.lock();
    if (chunkCount == 0) originChunkCenter = chunk->getChunkCenter();
    char addr[8];
    getAddrOfWorld(chunk->getChunkCenter(), addr);
    ChunkMap* old = chunks->z(addr, 8);
    if (old) chunkList.removeValue(old);
    chunks->set(addr, 8, chunk);
    chunkList.add(chunk);
    chunkCount++;
    requestCount--;
    removeUnused();
    cs.unlock();
    requestNextChunk();
}

bool DimensionMap::tick(double time)
{
    if (nextPlayersRequest < 0 && zOptions->isShowPlayers())
    {
        nextPlayersRequest = 2;
        char msg[2];
        msg[0] = 2; // request map players
        msg[1] = 3;
        if (World::INSTANCE)
        {
            World::INSTANCE->zClient()->dimensionAPIRequest(msg, 2);
        }
    }
    nextPlayersRequest -= time;
    if (lastSize != getSize())
    {
        lastSize = getSize();
        requestNextChunk();
    }
    return ZeichnungHintergrund::tick(time);
}

void DimensionMap::render(Framework::Bild& rObj)
{
    ZeichnungHintergrund::render(rObj);
    if (!rObj.setDrawOptions(innenPosition, innenSize)) return;
    cs.lock();
    if (zOptions->isFollowPlayer())
    {
        Vec3<float> playerPos
            = World::INSTANCE->getCurrentPlayerEntity()->getPos();
        scrollOffset
            = (Punkt((int)playerPos.x, (int)playerPos.y) - originChunkCenter)
            * pixelsPerBlock;
        requestNextChunk();
    }
    Punkt minScreenPos;
    Punkt minVisibleChunk = getMinVisibleChunkCenter(minScreenPos);
    Punkt maxScreenPos;
    Punkt maxVisibleChunk = getMaxVisibleChunkCenter(maxScreenPos);
    char addr[8];
    // render chunks
    Punkt screenPos = minScreenPos;
    for (int x = minVisibleChunk.x; x <= maxVisibleChunk.x; x += CHUNK_SIZE)
    {
        for (int y = minVisibleChunk.y; y <= maxVisibleChunk.y; y += CHUNK_SIZE)
        {
            getAddrOfWorld({x, y}, addr);
            ChunkMap* map = chunks->z(addr, 8);
            if (map)
            {
                if (zOptions->isUnderground())
                {
                    map->setMaxHeight(
                        (int)(World::INSTANCE->getCurrentPlayerEntity()
                                  ->getPos()
                                  .z
                              / 2));
                }
                else
                {
                    map->setMaxHeight(255);
                }
                Punkt topLeft(screenPos.x - (pixelsPerBlock * CHUNK_SIZE) / 2,
                    screenPos.y - (pixelsPerBlock * CHUNK_SIZE) / 2);
                rObj.drawBildSkall(topLeft.x,
                    topLeft.y,
                    pixelsPerBlock * CHUNK_SIZE,
                    pixelsPerBlock * CHUNK_SIZE,
                    map->getRenderedImage());
            }
            screenPos.y += pixelsPerBlock * CHUNK_SIZE;
        }
        screenPos.x += pixelsPerBlock * CHUNK_SIZE;
        screenPos.y = minScreenPos.y;
    }
    // render shadow and borders
    screenPos = minScreenPos;
    for (int x = minVisibleChunk.x; x <= maxVisibleChunk.x; x += CHUNK_SIZE)
    {
        for (int y = minVisibleChunk.y; y <= maxVisibleChunk.y; y += CHUNK_SIZE)
        {
            getAddrOfWorld({x, y}, addr);
            ChunkMap* map = chunks->z(addr, 8);
            if (map)
            {
                Punkt topLeft(screenPos.x - (pixelsPerBlock * CHUNK_SIZE) / 2,
                    screenPos.y - (pixelsPerBlock * CHUNK_SIZE) / 2);
                getAddrOfWorld({x, y - CHUNK_SIZE}, addr);
                ChunkMap* tmp = chunks->z(addr, 8);
                unsigned char* heightMapTop = tmp ? tmp->getHeightMap() : 0;
                getAddrOfWorld({x + CHUNK_SIZE, y}, addr);
                tmp = chunks->z(addr, 8);
                unsigned char* heightMapRight = tmp ? tmp->getHeightMap() : 0;
                getAddrOfWorld({x, y + CHUNK_SIZE}, addr);
                tmp = chunks->z(addr, 8);
                unsigned char* heightMapBottom = tmp ? tmp->getHeightMap() : 0;
                getAddrOfWorld({x - CHUNK_SIZE, y}, addr);
                tmp = chunks->z(addr, 8);
                unsigned char* heightMapLeft = tmp ? tmp->getHeightMap() : 0;
                unsigned char* heightMap = map->getHeightMap();
                for (int xx = 0; xx < CHUNK_SIZE; xx++)
                {
                    for (int yy = 0; yy < CHUNK_SIZE; yy++)
                    {
                        bool shadowR = 0;
                        bool shadowB = 0;
                        if (xx < CHUNK_SIZE - 1)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMap[yy * CHUNK_SIZE + xx + 1])
                            {
                                rObj.drawLinieVAlpha((xx * pixelsPerBlock)
                                                         + topLeft.x
                                                         + pixelsPerBlock,
                                    (yy * pixelsPerBlock) + topLeft.y,
                                    pixelsPerBlock,
                                    0x40000000);
                                shadowR = 1;
                            }
                        }
                        else if (heightMapRight)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMapRight[yy * CHUNK_SIZE])
                            {
                                rObj.drawLinieVAlpha((xx * pixelsPerBlock)
                                                         + topLeft.x
                                                         + pixelsPerBlock,
                                    (yy * pixelsPerBlock) + topLeft.y,
                                    pixelsPerBlock,
                                    0x40000000);
                                shadowR = 1;
                            }
                        }
                        if (yy < CHUNK_SIZE - 1)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMap[(yy + 1) * CHUNK_SIZE + xx])
                            {
                                rObj.drawLinieHAlpha(
                                    (xx * pixelsPerBlock) + topLeft.x,
                                    (yy * pixelsPerBlock) + topLeft.y
                                        + pixelsPerBlock,
                                    pixelsPerBlock,
                                    0x30000000);
                                shadowB = 1;
                            }
                        }
                        else if (heightMapBottom)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMapBottom[xx])
                            {
                                rObj.drawLinieHAlpha(
                                    (xx * pixelsPerBlock) + topLeft.x,
                                    (yy * pixelsPerBlock) + topLeft.y
                                        + pixelsPerBlock,
                                    pixelsPerBlock,
                                    0x30000000);
                                shadowB = 1;
                            }
                        }
                        if (xx > 0)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMap[yy * CHUNK_SIZE + xx - 1])
                            {
                                rObj.drawLinieVAlpha(
                                    (xx * pixelsPerBlock) + topLeft.x,
                                    (yy * pixelsPerBlock) + topLeft.y,
                                    pixelsPerBlock - shadowB,
                                    0x20FFFFFF);
                            }
                        }
                        else if (heightMapLeft)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMapLeft[yy * CHUNK_SIZE + CHUNK_SIZE
                                                - 1])
                            {
                                rObj.drawLinieVAlpha(
                                    (xx * pixelsPerBlock) + topLeft.x,
                                    (yy * pixelsPerBlock) + topLeft.y,
                                    pixelsPerBlock - shadowB,
                                    0x20FFFFFF);
                            }
                        }
                        if (yy > 0)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMap[(yy - 1) * CHUNK_SIZE + xx])
                            {
                                rObj.drawLinieHAlpha(
                                    (xx * pixelsPerBlock) + topLeft.x,
                                    (yy * pixelsPerBlock) + topLeft.y,
                                    pixelsPerBlock - shadowR,
                                    0x10FFFFFF);
                            }
                        }
                        else if (heightMapTop)
                        {
                            if (heightMap[yy * CHUNK_SIZE + xx]
                                > heightMapTop[(CHUNK_SIZE - 1) * CHUNK_SIZE
                                               + xx])
                            {
                                rObj.drawLinieHAlpha(
                                    (xx * pixelsPerBlock) + topLeft.x,
                                    (yy * pixelsPerBlock) + topLeft.y,
                                    pixelsPerBlock - shadowR,
                                    0x10FFFFFF);
                            }
                        }
                    }
                }
                if (zOptions->isShowChunkBorders())
                {
                    rObj.drawLinieHAlpha(topLeft.x,
                        topLeft.y,
                        pixelsPerBlock * CHUNK_SIZE,
                        0x50FFFFFF);
                    rObj.drawLinieVAlpha(topLeft.x,
                        topLeft.y,
                        pixelsPerBlock * CHUNK_SIZE,
                        0x50FFFFFF);
                }
            }
            screenPos.y += pixelsPerBlock * CHUNK_SIZE;
        }
        screenPos.x += pixelsPerBlock * CHUNK_SIZE;
        screenPos.y = minScreenPos.y;
    }
    // render players
    if (zOptions->isShowPlayers())
    {
        TextRenderer tm(
            dynamic_cast<Schrift*>(uiFactory.initParam.schrift->getThis()));
        tm.setSchriftSize(12);
        for (MapPlayer& player : players)
        {
            Punkt screenPos
                = getSize() / 2 - scrollOffset
                + (Punkt((int)player.position.x, (int)player.position.y)
                      - originChunkCenter)
                      * pixelsPerBlock
                + Punkt(pixelsPerBlock, pixelsPerBlock) / 2
                - playerIcon->getSize() / 2;
            rObj.alphaBild(screenPos.x,
                screenPos.y,
                playerIcon->getBreite(),
                playerIcon->getHeight(),
                *playerIcon);
            int textWidth = tm.getTextBreite(player.name);
            int textheight = tm.getTextHeight(player.name);
            screenPos = screenPos + Punkt(playerIcon->getBreite(), 0) / 2
                      - Punkt(textWidth / 2, textheight + 2);
            rObj.alphaRegion(
                screenPos.x, screenPos.y, textWidth, textheight, 0x70000000);
            tm.renderText(
                screenPos.x, screenPos.y, player.name, rObj, 0xFFFFFFFF);
        }
    }
    cs.unlock();
    rObj.releaseDrawOptions();
}

void DimensionMap::doMausEreignis(Framework::MausEreignis& me, bool userRet)
{
    if (me.id == ME_PLinks)
    {
        drag = 1;
        lastMouse = {me.mx, me.my};
    }
    if (me.id == ME_RLinks || me.id == ME_Leaves) drag = 0;
    if (me.id == ME_Bewegung && drag)
    {
        scrollOffset -= Punkt(me.mx, me.my) - lastMouse;
        lastMouse = Punkt(me.mx, me.my);
        rend = 1;
        requestNextChunk();
    }
    if (me.id == ME_DScroll && pixelsPerBlock > 1)
    {
        scrollOffset = (scrollOffset / pixelsPerBlock) * (pixelsPerBlock - 1);
        pixelsPerBlock--;
        rend = 1;
        requestNextChunk();
    }
    if (me.id == ME_UScroll)
    {
        scrollOffset = (scrollOffset / pixelsPerBlock) * (pixelsPerBlock + 1);
        pixelsPerBlock++;
        rend = 1;
        requestNextChunk();
    }
    ZeichnungHintergrund::doMausEreignis(me, userRet);
}