#include "Dimension.h"

#include <File.h>

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

using namespace Framework;

Dimension::Dimension()
    : id(-1),
      chunks(new RCTrie<Chunk>()),
      entities(new RCArray<Entity>())
{}

Dimension::~Dimension()
{
    entities->release();
    chunks->release();
}

void Dimension::forAll(std::function<void(Model3D*)> f)
{
    // handled by render and tick function
}

void Dimension::render(std::function<void(Model3D*)> f)
{
    for (Chunk* chunk : chunkList)
    {
        chunk->renderSolid(f);
    }
    for (Chunk* chunk : chunkList)
    {
        chunk->renderTransparent(f);
    }
}

bool Dimension::tick(std::function<void(Model3D*)> f, double time)
{
    bool res = 0;
    for (Chunk* chunk : chunkList)
    {
        res |= chunk->tick(f, time);
    }
    return res;
}

void Dimension::setId(int id)
{
    this->id = id;
}

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

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

void Dimension::api(char* message)
{
    switch (message[0])
    {
    case 1: // chunck
        {
            int cX = *(int*)(message + 1);
            int cY = *(int*)(message + 5);
            lock.lockRead();
            Chunk* ch = zChunk(Point(cX, cY));
            if (ch) ch->api(message + 9);
            lock.unlockRead();
            break;
        }
    case 2: // entity
        {
            int eId = *(int*)(message + 1);
            lock.lockRead();
            Entity* e = zEntity(eId);
            if (e) e->api(message + 5);
            lock.unlockRead();
            break;
        }
    case 3: // block
        {
            int px = *(int*)(message + 1);
            int py = *(int*)(message + 5);
            int pz = *(int*)(message + 9);
            lock.lockRead();
            Block* b = zBlock(Framework::Vec3<int>(px, py, pz));
            if (b) b->api(message + 13);
            lock.unlockRead();
            break;
        }
    case 4: // add new chunck
        {
            Point center;
            center.x = *(int*)(message + 1);
            center.y = *(int*)(message + 5);
            ByteArrayReader reader(message + 9, INT_MAX, 0);
            std::cout << "downloading chunk " << center.x << ", " << center.y
                      << "\n";
            Timer zm;
            zm.measureStart();
            Chunk* chunk = new Chunk(center, &reader);
            zm.measureEnd();
            std::cout << "chunk loading took " << zm.getSekunden()
                      << " seconds\n";
            setChunk(chunk, center);
            World::INSTANCE->onChunkAdded(center);
            World::INSTANCE->zClient()->chunkAPIRequest(center, "\2", 1);
            break;
        }
    case 5: // light update
        {
            int x = *(int*)(message + 1);
            int y = *(int*)(message + 5);
            int z = *(int*)(message + 9);
            Framework::Vec3<int> location(x, y, z);
            for (int i = 0; i < 6; i++)
            {
                Framework::Vec3<int> pos
                    = location + getDirection(getDirectionFromIndex(i));
                if (pos.z >= 0 && pos.z < WORLD_HEIGHT)
                {
                    lock.lockRead();
                    Chunk* c
                        = zChunk(World::INSTANCE->getChunkCenter(pos.x, pos.y));
                    Block* zB = c ? c->zBlockAt(pos) : 0;
                    if (zB)
                    {
                        bool visible = zB->isVisible();
                        zB->setLightData(
                            getOppositeDirection(getDirectionFromIndex(i)),
                            (unsigned char*)(message + 13),
                            c);
                        if (zB->isVisible() != visible)
                        {
                            zChunk(
                                World::INSTANCE->getChunkCenter(pos.x, pos.y))
                                ->blockVisibilityChanged(zB);
                        }
                    }
                    lock.unlockRead();
                }
            }
            break;
        }
    case 7: // add entity
        {
            int type = *(int*)(message + 1);
            ByteArrayReader reader(message + 5, INT_MAX, 0);
            Entity* entity = entityTypes[type]->loadEntity(&reader);
            addEntity(entity);
            break;
        }
    case 8: // remove entity
        {
            int id = *(int*)(message + 1);
            removeEntity(id);
            break;
        }
    }
}

Chunk* Dimension::zChunk(Point wPos) const
{
    char addr[8];
    getAddrOfWorld(wPos, addr);
    return chunks->z(addr, 8);
}

Block* Dimension::zBlock(Vec3<int> location)
{
    Chunk* c = zChunk(World::INSTANCE->getChunkCenter(location.x, location.y));
    if (c) return c->zBlockAt(location);
    return 0;
}

Block* Dimension::getBlock(Vec3<int> location)
{
    lock.lockRead();
    Chunk* c = zChunk(World::INSTANCE->getChunkCenter(location.x, location.y));
    if (c)
    {
        Block* b = c->zBlockAt(location);
        b = b ? dynamic_cast<Block*>(b->getThis()) : 0;
        lock.unlockRead();
        return b;
    }
    lock.unlockRead();
    return 0;
}

void Dimension::addEntity(Entity* entity)
{
    entities->add(entity);
    World::INSTANCE->setVisibility(entity, 1);
}

void Dimension::setChunk(Chunk* chunk, Point center)
{
    char addr[8];
    getAddrOfWorld(center, addr);
    lock.lockWrite();
    Chunk* old = chunks->z(addr, 8);
    if (old)
    {
        int index = 0;
        for (auto iterator = chunkList.begin(); iterator; ++iterator, ++index)
        {
            if ((Chunk*)iterator == old)
            {
                if (chunk)
                    iterator.set(chunk);
                else
                    chunkList.remove(index);
                break;
            }
        }
    }
    else if (chunk)
        chunkList.add(chunk);
    chunks->set(addr, 8, chunk);
    lock.unlockWrite();
}

bool Dimension::hasChunck(int x, int y) const
{
    return zChunk(Point(x, y));
}

void Dimension::removeDistantChunks(Point wPos)
{
    Array<int> removed;
    int index = 0;
    lock.lockRead();
    for (Chunk* chunk : chunkList)
    {
        if (abs(chunk->getCenter().x - wPos.x) > MAX_VIEW_DISTANCE + CHUNK_SIZE
            || abs(chunk->getCenter().y - wPos.y)
                   > MAX_VIEW_DISTANCE + CHUNK_SIZE)
            removed.add(index, 0);
        index++;
    }
    lock.unlockRead();
    for (int i : removed)
    {
        lock.lockWrite();
        Chunk* chunk = chunkList.get(i);
        chunk->destroy();
        setChunk(0, chunk->getCenter());
        lock.unlockWrite();
    }
}

void Dimension::setBlock(Block* block)
{
    lock.lockWrite();
    Chunk* c = zChunk(World::INSTANCE->getChunkCenter(
        (int)floor(block->getPos().x), (int)floor(block->getPos().y)));
    if (c)
        c->setBlock(block);
    else
        block->release();
    lock.unlockWrite();
}

void Dimension::removeBlock(Block* zBlock)
{
    lock.lockWrite();
    Chunk* c = zChunk(World::INSTANCE->getChunkCenter(
        (int)floor(zBlock->getPos().x), (int)floor(zBlock->getPos().y)));
    if (c) c->removeBlock(zBlock);
    lock.unlockWrite();
}

Entity* Dimension::zEntity(int id)
{
    lock.lockRead();
    for (Entity* e : *entities)
    {
        if (e->getId() == id)
        {
            lock.unlockRead();
            return e;
        }
    }
    lock.unlockRead();
    return 0;
}

Entity* Dimension::getEntity(int id)
{
    lock.lockRead();
    for (Entity* e : *entities)
    {
        if (e->getId() == id)
        {
            Entity* result = dynamic_cast<Entity*>(e->getThis());
            lock.unlockRead();
            return result;
        }
    }
    lock.unlockRead();
    return 0;
}

void Dimension::removeEntity(int id)
{
    lock.lockWrite();
    int index = 0;
    for (Entity* e : *entities)
    {
        if (e->getId() == id)
        {
            World::INSTANCE->setVisibility(e, 0);
            entities->remove(index);
            lock.unlockWrite();
            return;
        }
        index++;
    }
    lock.unlockWrite();
}

int Dimension::getId() const
{
    return id;
}

Lock& Dimension::getReadLock() const
{
    return lock.getReadLock();
}
