#include "World.h"

#include <AsynchronCall.h>
#include <GraphicsApi.h>
#include <iostream>
#include <Network.h>
#include <Welt3D.h>

#include "Constants.h"
#include "Game.h"
#include "Globals.h"
#include "Registries.h"
#include "WorldUpdate.h"

using namespace Network;
using namespace Framework;

World* World::INSTANCE = 0;

World::World(Bildschirm3D* zScreen, FactoryClient* client)
    : Thread(),
      client(client)
{
    renderedWorld = new Welt3D();
    renderedWorld->addDiffuseLight(DiffuseLight{
        Vec3<float>(0.5f, 0.5f, -1.f), Vec3<float>(1.f, 1.f, 1.f)});
    currentDimension = new Dimension();
    zScreenPtr = zScreen;
    kam = new PlayerKam(zScreen);
    kam->setWelt(renderedWorld);
    zScreen->addKamera(kam);
    firstMessage = 1;
    ownEntityId = -1;
    currentTarget = 0;
    start();
}

World::~World()
{
    zScreenPtr->removeKamera(kam);
    currentDimension->release();
    if (currentTarget) currentTarget->release();
    client->release();
}

void World::update(bool background)
{
    NetworkReader* serverMessageReader = 0;
    unsigned char type = 0;
    while (background
               ? serverMessageReader = client->getNextBackgroundMessage()
               : serverMessageReader = client->getNextForegroundMessage())
    {
        serverMessageReader->lese((char*)&type, 1);
        if (type == 2) // WORLD UPDATE
        {
            int id = 0;
            serverMessageReader->lese((char*)&id, 4);
            STATIC_REGISTRY(WorldUpdateType)
                .zElement(id)
                ->applyUpdateAndCheck(serverMessageReader);
        }
        if (type == 3) // API MESSAGE
        {
            int length;
            serverMessageReader->lese((char*)&length, 4);
            char* data = new char[length];
            serverMessageReader->lese(data, length);
            switch (data[0])
            {
            case 1: // dimension message
                {
                    int dimId = *(int*)(data + 1);
                    if (dimId == currentDimension->getId())
                    {
                        currentDimension->api(data + 5);
                    }
                    break;
                }
            case 2: // gui message
                ((Game*)(Menu*)menuRegister->get("game"))->api(data + 1);
                break;
            case 3: // set target
                {
                    switch (data[1])
                    {
                    case 0:
                        setTarget(0);
                        ((Game*)(Menu*)menuRegister->get("game"))
                            ->setTargetUIML("");
                        break;
                    case 1:
                        setTarget(zEntity(*(int*)(data + 2)));
                        ((Game*)(Menu*)menuRegister->get("game"))
                            ->setTargetUIML("");
                        break;
                    case 2:
                        setTarget(zBlockAt(Vec3<int>(*(int*)(data + 2),
                            *(int*)(data + 6),
                            *(int*)(data + 10))));
                        int side = *(int*)(data + 14);
                        short len = *(short*)(data + 18);
                        char* uiml = new char[len + 1];
                        memcpy(uiml, data + 20, len);
                        uiml[len] = 0;
                        ((Game*)(Menu*)menuRegister->get("game"))
                            ->setTargetUIML(uiml);
                        delete[] uiml;
                        break;
                    }
                    break;
                }
            case 4: // chat message
                {
                    ((Game*)(Menu*)menuRegister->get("game"))
                        ->zChat()
                        ->addMessage(data + 1);
                    break;
                }
            case 5: // chat options
                {
                    ((Game*)(Menu*)menuRegister->get("game"))
                        ->zChat()
                        ->initOptions(data + 1);
                    break;
                }
            case 6: // map data
                {
                    ByteArrayReader reader(data + 1, length - 1, 0);
                    ChunkMap* map = new ChunkMap(&reader);
                    ((Game*)(Menu*)menuRegister->get("game"))
                        ->zMap()
                        ->addChunk(map);
                    break;
                }
            }
            delete[] data;
            // TODO: process messages
        }
        if (type == 4) // POSITION UPDATE
        {
            int old = ownEntityId;
            serverMessageReader->lese((char*)&ownEntityId, 4);
            int dimensionId = 0;
            serverMessageReader->lese((char*)&dimensionId, 4);
            currentDimension->setId(dimensionId);
            kam->setEntityId(ownEntityId);
            Entity* p = zEntity(ownEntityId);
            if (p) p->setPlayerControlled();
            if (old != ownEntityId) client->sendPlayerAction("\5", 1);
        }
        client->endMessageReading(background);
    }
    client->endMessageReading(background);
    Entity* player = getCurrentPlayerEntity();
    if (player)
    {
        renderedWorld->lock();
        currentDimension->removeDistantChunks(
            {(int)player->getPos().x, (int)player->getPos().y});
        Punkt currentChunk
            = getChunkCenter((int)player->getX(), (int)player->getY());
        for (int x = 0; x <= CHUNK_VISIBILITY_RANGE; x++)
        {
            for (int y = 0; y <= CHUNK_VISIBILITY_RANGE; y++)
            {
                std::function<void(Punkt)> requestChunk = [this](Punkt center) {
                    Chunk* zC = currentDimension->zChunk(center);
                    if (!zC)
                    {
                        char msg[1];
                        msg[0] = 0; // add observer and request chaunk data
                        Punkt pos = center;
                        subLock.lock();
                        bool found = 0;
                        for (Punkt p : subscriptions)
                        {
                            if (p == pos)
                            {
                                found = 1;
                                break;
                            }
                        }
                        if (!found)
                        {
                            client->chunkAPIRequest(pos, msg, 1);
                            subscriptions.add(pos);
                        }
                        subLock.unlock();
                    }
                };
                requestChunk(
                    currentChunk + Punkt(x * CHUNK_SIZE, y * CHUNK_SIZE));
                if (y > 0)
                    requestChunk(
                        currentChunk + Punkt(x * CHUNK_SIZE, -y * CHUNK_SIZE));
                if (x > 0)
                {
                    requestChunk(
                        currentChunk + Punkt(-x * CHUNK_SIZE, y * CHUNK_SIZE));
                    if (y > 0)
                        requestChunk(currentChunk
                                     + Punkt(-x * CHUNK_SIZE, -y * CHUNK_SIZE));
                }
            }
        }
        renderedWorld->unlock();
    }
}

void World::setChunk(Chunk* chunk)
{
    zScreenPtr->lock();
    renderedWorld->lock();
    currentDimension->setChunk(chunk, chunk->getCenter());
    renderedWorld->unlock();
    zScreenPtr->unlock();
}

void World::thread()
{
    new AsynchronCall("World Update", [this]() {
        while (client->isConnected())
        {
            zScreenPtr->lock();
            if (World::INSTANCE != this)
            {
                zScreenPtr->unlock();
                return;
            }
            zScreenPtr->unlock();
            update(0);
            Sleep(10);
        }
        std::cout << "foreground connection lost\n";
    });
    while (client->isConnected())
    {
        zScreenPtr->lock();
        if (World::INSTANCE != this)
        {
            zScreenPtr->unlock();
            return;
        }
        zScreenPtr->unlock();
        update(1);
        Sleep(10);
    }
    std::cout << "background connection lost\n";
}

Block* World::zBlockAt(Framework::Vec3<int> location) const
{
    return currentDimension->zBlock(location);
    return 0;
}

Block* World::getBlockAt(Framework::Vec3<int> location) const
{
    return currentDimension->getBlock(location);
    return 0;
}

Dimension* World::zDimension() const
{
    return currentDimension;
}

void World::setVisibility(Chunk* zChunk, bool visible)
{
    renderedWorld->lock();
    if (visible)
        renderedWorld->addCollection(
            dynamic_cast<Framework::Model3DCollection*>(zChunk->getThis()));
    else
        renderedWorld->removeCollection(zChunk);
    renderedWorld->unlock();
}

void World::setVisibility(Entity* zEntity, bool visible)
{
    renderedWorld->lock();
    if (visible)
        renderedWorld->addZeichnung(
            dynamic_cast<Framework::Model3D*>(zEntity->getThis()));
    else
        renderedWorld->removeZeichnung(zEntity);
    renderedWorld->unlock();
}

Framework::Punkt World::getChunkCenter(int x, int y) const
{
    return Punkt(((x < 0 ? x + 1 : x) / CHUNK_SIZE) * CHUNK_SIZE
                     + (x < 0 ? -CHUNK_SIZE : CHUNK_SIZE) / 2,
        ((y < 0 ? y + 1 : y) / CHUNK_SIZE) * CHUNK_SIZE
            + (y < 0 ? -CHUNK_SIZE : CHUNK_SIZE) / 2);
}

Entity* World::zEntity(int id) const
{
    Entity* e = currentDimension->zEntity(id);
    if (e) return e;
    return 0;
}

Entity* World::getEntity(int id) const
{
    Entity* e = currentDimension->getEntity(id);
    if (e) return e;
    return 0;
}

void World::removeEntity(int id)
{
    currentDimension->removeEntity(id);
}

PlayerKam* World::zKamera() const
{
    return kam;
}

int World::getCurrentPlayerId() const
{
    return ownEntityId;
}

Entity* World::getCurrentPlayerEntity() const
{
    return zEntity(ownEntityId);
}

void World::setTarget(Framework::Model3D* zTarget)
{
    if (zTarget != currentTarget)
    {
        targetLock.lock();
        if (currentTarget)
        {
            currentTarget->setAmbientFactor(
                currentTarget->getAmbientFactor() - 0.2f);
            currentTarget->release();
            currentTarget = 0;
        }
        if (zTarget)
        {
            currentTarget
                = dynamic_cast<Framework::Model3D*>(zTarget->getThis());
            if (currentTarget)
                currentTarget->setAmbientFactor(
                    currentTarget->getAmbientFactor() + 0.2f);
        }
        targetLock.unlock();
    }
}

void World::lockWorld()
{
    renderedWorld->lock();
}

void World::unlockWorld()
{
    renderedWorld->unlock();
}

void World::onChunkAdded(Punkt pos)
{
    subLock.lock();
    int index = 0;
    for (Punkt p : subscriptions)
    {
        if (p == pos)
        {
            subscriptions.remove(index);
            break;
        }
        index++;
    }
    subLock.unlock();
}

Framework::Model3D* World::getCurrentTarget() const
{
    while (targetLock.isLocked())
        Sleep(1);
    return currentTarget
             ? dynamic_cast<Framework::Model3D*>(currentTarget->getThis())
             : 0;
}

Chunk* World::zChunk(Punkt center)
{
    return currentDimension->zChunk(center);
}

FactoryClient* World::zClient() const
{
    return client;
}