#include "World.h"

#include <AsynchronCall.h>
#include <DX12Buffer.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();
    renderedWorld->addCollection(
        dynamic_cast<Model3DCollection*>(currentDimension->getThis()));
    zScreenPtr = zScreen;
    kam = new PlayerKam(zScreen);
    kam->setWelt(renderedWorld);
    zScreen->addKamera(kam);
    firstMessage = 1;
    ownEntityId = -1;
    currentTarget = 0;
    dayLightFactor = 1;
    time = 0;
    dayLength = 1000;
    transitionLength = 0;
    nightLength = 0;
    fallbackVertexLightBuffer
        = zScreen->zGraphicsApi()->createStructuredBuffer(16);
    char data[16];
    memset(data, 0, 16);
    *(int*)data = 0xFFFFFF00;
    fallbackVertexLightBuffer->setData(data);
    fallbackVertexLightBuffer->setLength(16);
    fallbackVertexLightBuffer->copieren();
    selectionModel = new FactoryCraftModel();
    selectionModel->setModelDaten(zScreen->zGraphicsApi()->getModel("cube"));
    selectionModel->setSize(1.005f);
    selectionModel->setVisible(0);
    selectionModel->setModelTextur(new Model3DTextur());
    for (int i = 0; i < 6; i++)
    {
        selectionModel->zTextur()->setPolygonTextur(i,
            zScreen->zGraphicsApi()->createOrGetTextur(
                "blocks.ltdb/selectedblock.p"));
    }
    selectionModel->setAlpha(1);
    selectionModel->setUseEffectAlpha(1);
    renderedWorld->addZeichnung(selectionModel);
    start();
}

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

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;
                }
            case 7: // map player data
                {
                    ((Game*)(Menu*)menuRegister->get("game"))
                        ->zMap()
                        ->updatePlayerData(data + 1);
                    break;
                }
            case 8: // time sync
                {
                    nightLength = *(double*)(data + 9);
                    transitionLength = *(double*)(data + 17);
                    dayLength = *(double*)(data + 25);
                    time = *(double*)(data + 1);
                    break;
                }
            }
            delete[] data;
        }
        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::onTick(double time)
{
    selectionModel->tick(0.1);
    this->time += time;
    if (this->time >= dayLength + nightLength + transitionLength * 2)
    {
        this->time -= dayLength + nightLength + transitionLength * 2;
    }
    if (this->time > dayLength && this->time < dayLength + transitionLength)
    {
        dayLightFactor
            = 1.f
            - (float)((this->time - dayLength) / transitionLength) * 0.95f;
    }
    else if (this->time > dayLength + transitionLength
             && this->time < dayLength + transitionLength + nightLength)
    {
        // night
        dayLightFactor = 0.05f;
        if (renderedWorld->getDiffuseLightCount() > 0)
        {
            renderedWorld->removeDiffuseLight(0);
        }
    }
    else if (this->time > dayLength + transitionLength + nightLength)
    {
        dayLightFactor
            = (float)((this->time - dayLength - nightLength - transitionLength)
                      / transitionLength)
                * 0.95f
            + 0.05f;
    }
    else
    {
        dayLightFactor = 1.f;
    }
    if (this->time < dayLength + transitionLength
        || this->time > dayLength + transitionLength + nightLength)
    {
        float currentDayTime
            = (float)(this->time < dayLength + transitionLength
                          ? this->time + transitionLength
                          : this->time - dayLength + transitionLength
                                + nightLength);
        Vec3<float> sunPos = Vec3<float>(-200.f, 50.f, 0.f);
        sunPos.rotateY((
            float)((currentDayTime / (dayLength + transitionLength * 2)) * PI));
        sunPos.normalize();
        if (!renderedWorld->getDiffuseLightCount())
        {
            renderedWorld->addDiffuseLight(
                {-sunPos, Vec3<float>(1.0, 1.0, 1.0)});
        }
        else
        {
            renderedWorld->getDiffuseLight(0).direction = -sunPos;
        }
    }
}

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);
    }
    Sleep(1000);
    zScreenPtr->lock();
    std::cout << "background connection lost\n";
    menuRegister->get("game")->hide();
    menuRegister->get("serverSelection")->show();
    release();
    zScreenPtr->unlock();
    if (World::INSTANCE == this)
    {
        World::INSTANCE = 0;
    }
}

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(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 (!zTarget)
        {
            selectionModel->setVisible(0);
        }
        else
        {
            selectionModel->setPosition(
                zTarget->getPos().x, zTarget->getPos().y, zTarget->getPos().z);
            Block* b = dynamic_cast<Block*>(zTarget);
            if (b && b->getPartOfModels())
            {
                selectionModel->setDestroyedState(b->getEffectPercentage());
            }
            else
            {
                selectionModel->setDestroyedState(0);
            }
            selectionModel->setVisible(1);
        }
        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;
}

FactoryCraftModel* World::zSelectedEffectModel() const
{
    return selectionModel;
}

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

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

float World::getDayLightFactor() const
{
    return dayLightFactor;
}

Framework::DXBuffer* World::zFallbackVertexLightBuffer()
{
    return fallbackVertexLightBuffer;
}