#include "World.h"

#include <AsynchronCall.h>
#include <DX12Buffer.h>
#include <GraphicsApi.h>
#include <iostream>
#include <Network.h>
#include <World3D.h>

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

using namespace Network;
using namespace Framework;

World* World::INSTANCE = 0;

World::World(Screen3D* zScreen, FactoryClient* client)
    : Thread(),
      client(client)
{
    renderedWorld = new World3D();
    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->setWorld(renderedWorld);
    zScreen->addKamera(kam);
    firstMessage = 1;
    ownEntityId = -1;
    currentTarget = 0;
    dayLightFactor = 1;
    time = 0;
    dayLength = 1000;
    transitionLength = 0;
    nightLength = 0;
    selectionModel = new FactoryCraftModel();
    selectionModel->setModelData(zScreen->zGraphicsApi()->getModel("cube"));
    selectionModel->setSize(1.005f);
    selectionModel->setVisible(0);
    unsigned char light[3] = {0xff, 0xff, 0xff};
    selectionModel->setAverageLight(light);
    selectionModel->setModelTextur(new Model3DTexture());
    for (int i = 0; i < 6; i++)
    {
        selectionModel->zTexture()->setPolygonTexture(i,
            zScreen->zGraphicsApi()->createOrGetTexture(
                "data/textures/blocks.ltdb/selectedblock.p"));
    }
    selectionModel->setAlpha(1);
    selectionModel->setUseEffectAlpha(1);
    renderedWorld->addDrawable(selectionModel);
    start();
}

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

int World::update(bool background)
{
    if (background && currentDimension->getId() < 0) return 0;
    NetworkReader* serverMessageReader = 0;
    unsigned char type = 0;
    int messageCount = 0;
    while (background
               ? serverMessageReader = client->getNextBackgroundMessage()
               : serverMessageReader = client->getNextForegroundMessage())
    {
        messageCount++;
        serverMessageReader->read((char*)&type, 1);
        if (type == 3) // API MESSAGE
        {
            int length;
            serverMessageReader->read((char*)&length, 4);
            char* data = new char[length];
            serverMessageReader->read(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)));
                            short len = *(short*)(data + 6);
                            char* uiml = new char[len + 8];
                            memcpy(uiml, data + 8, len);
                            uiml[len] = 0;
                            ((Game*)(Menu*)menuRegister->get("game"))
                                ->setTargetUIML(uiml);
                            delete[] uiml;
                            break;
                        }
                    case 2:
                        {
                            setTarget(zBlockAt(Vec3<int>(*(int*)(data + 2),
                                *(int*)(data + 6),
                                *(int*)(data + 10))));
                            Direction side = *(Direction*)(data + 14);
                            short len = *(short*)(data + 15);
                            char* uiml = new char[len + 1];
                            memcpy(uiml, data + 17, 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->read((char*)&ownEntityId, 4);
            int dimensionId = 0;
            serverMessageReader->read((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);
    if (!background)
    {
        Entity* player = getCurrentPlayerEntity();
        if (player)
        {
            currentDimension->removeDistantChunks(
                {(int)player->getPos().x, (int)player->getPos().y});
            Point 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(Point)> requestChunk =
                        [this](Point center) {
                            Chunk* zC = currentDimension->zChunk(center);
                            if (!zC)
                            {
                                char msg[1];
                                msg[0]
                                    = 0; // add observer and request chaunk data
                                Point pos = center;
                                subLock.lockRead();
                                bool found = 0;
                                for (Point p : subscriptions)
                                {
                                    if (p == pos)
                                    {
                                        found = 1;
                                        break;
                                    }
                                }
                                if (!found)
                                {
                                    client->chunkAPIRequest(pos, msg, 1);
                                    subLock.lockWrite();
                                    subscriptions.add(pos);
                                    subLock.unlockWrite();
                                }
                                subLock.unlockRead();
                            }
                        };
                    requestChunk(
                        currentChunk + Point(x * CHUNK_SIZE, y * CHUNK_SIZE));
                    if (y > 0)
                        requestChunk(currentChunk
                                     + Point(x * CHUNK_SIZE, -y * CHUNK_SIZE));
                    if (x > 0)
                    {
                        requestChunk(currentChunk
                                     + Point(-x * CHUNK_SIZE, y * CHUNK_SIZE));
                        if (y > 0)
                            requestChunk(
                                currentChunk
                                + Point(-x * CHUNK_SIZE, -y * CHUNK_SIZE));
                    }
                }
            }
        }
    }
    return messageCount;
}

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)
{
    currentDimension->setChunk(chunk, chunk->getCenter());
}

void World::thread()
{
    new AsynchronCall("World Update", [this]() {
        Timer tm;
        tm.measureStart();
        double sum = 0;
        int updates = 0;
        int messages = 0;
        double wait = 0;
        while (client->isConnected())
        {
            if (World::INSTANCE != this)
            {
                return;
            }
            messages += update(0);
            updates++;
            tm.measureEnd();
            sum += tm.getSekunden();
            wait += 10 - tm.getSekunden();
            tm.measureStart();
            if ((int)wait > 0)
            {
                Sleep((int)wait);
            }
            if (sum > 1.0)
            {
                std::cout << "updates: " << updates
                          << ", messages: " << (double)messages
                          << ", seconds: " << sum << "\n";
                sum = 0;
                updates = 0;
                messages = 0;
            }
        }
        std::cout << "foreground connection lost\n";
    });
    while (client->isConnected())
    {
        if (World::INSTANCE != this)
        {
            return;
        }
        update(1);
        Sleep(100);
    }
    Sleep(1000);
    std::cout << "background connection lost\n";
    menuRegister->get("game")->hide();
    menuRegister->get("serverSelection")->show();
    zScreenPtr->postAction([this]() {
        if (World::INSTANCE == this)
        {
            World::INSTANCE = 0;
            release();
        }
    });
}

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

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

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

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

Framework::Point World::getChunkCenter(int x, int y) const
{
    return Point(((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::onChunkAdded(Point pos)
{
    subLock.lockWrite();
    int index = 0;
    for (Point p : subscriptions)
    {
        if (p == pos)
        {
            subscriptions.remove(index);
            break;
        }
        index++;
    }
    subLock.unlockWrite();
}

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(Point center)
{
    return currentDimension->zChunk(center);
}

Lock& World::getChunkReadLock()
{
    return currentDimension->getReadLock();
}

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

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