#include "Chunk.h"

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

Chunk::Chunk(Framework::Punkt location)
    : ReferenceCounter(),
      location(location),
      isLoading(0)
{}

Chunk::Chunk(Framework::Punkt location, Framework::StreamReader* zReader)
    : Chunk(location)
{
    load(zReader);
}

Chunk::~Chunk()
{
    char msg = 1; // remove observer
    World::INSTANCE->zClient()->chunkAPIRequest(location, &msg, 1);
}

void Chunk::api(char* message)
{
    switch (message[0])
    {
    case 0: // set block
        {
            int index = *(int*)(message + 1);
            int id = *(int*)(message + 5);
            Framework::Vec3<int> location((index / WORLD_HEIGHT) / CHUNK_SIZE,
                (index / WORLD_HEIGHT) % CHUNK_SIZE,
                index % WORLD_HEIGHT);
            location.x += this->location.x - CHUNK_SIZE / 2;
            location.y += this->location.y - CHUNK_SIZE / 2;
            if (blockTypes[id]->doesNeedInstance())
            {
                Block* zB = blockTypes[id]->createBlock(location);
                setBlock(zB);
            }
            else
            {
                Block* zB = zBlockAt(location);
                if (zB) removeBlock(zB);
            }
            break;
        }
    case 1: // update light
        {
            int index = *(int*)(message + 1);
            Framework::Vec3<int> location((index / WORLD_HEIGHT) / CHUNK_SIZE,
                (index / WORLD_HEIGHT) % CHUNK_SIZE,
                index % WORLD_HEIGHT);
            for (int i = 0; i < 6; i++)
            {
                Framework::Vec3<int> pos
                    = location + getDirection(getDirectionFromIndex(i));
                if (pos.z >= 0 && pos.z < WORLD_HEIGHT)
                {
                    if (pos.x >= 0 && pos.x < CHUNK_SIZE && pos.y >= 0
                        && pos.y < CHUNK_SIZE)
                    {
                        pos.x += this->location.x - CHUNK_SIZE / 2;
                        pos.y += this->location.y - CHUNK_SIZE / 2;
                        Block* zB = zBlockAt(pos);
                        if (zB)
                        {
                            bool visible = zB->isVisible();
                            zB->setLightData(
                                getOppositeDirection(getDirectionFromIndex(i)),
                                (unsigned char*)(message + 5));
                            if (zB->isVisible() != visible)
                            {
                                vcs.lock();
                                if (zB->isVisible())
                                    visibleBlocks.add(zB);
                                else
                                {
                                    for (Framework::Iterator<Block*> iterator
                                         = visibleBlocks.begin();
                                         iterator;
                                         iterator++)
                                    {
                                        if (zB == (Block*)iterator)
                                        {
                                            iterator.remove();
                                            break;
                                        }
                                    }
                                }
                                vcs.unlock();
                            }
                        }
                    }
                    else
                    {
                        pos.x += this->location.x - CHUNK_SIZE / 2;
                        pos.y += this->location.y - CHUNK_SIZE / 2;
                        Block* zB = World::INSTANCE->zBlockAt(pos);
                        if (zB)
                        {
                            bool visible = zB->isVisible();
                            zB->setLightData(
                                getOppositeDirection(getDirectionFromIndex(i)),
                                (unsigned char*)(message + 5));
                            if (zB->isVisible() != visible)
                            {
                                Chunk* c = World::INSTANCE->zChunk(
                                    World::INSTANCE->getChunkCenter(
                                        pos.x, pos.y));
                                c->vcs.lock();
                                if (zB->isVisible())
                                    c->visibleBlocks.add(zB);
                                else
                                {
                                    for (Framework::Iterator<Block*> iterator
                                         = c->visibleBlocks.begin();
                                         iterator;
                                         iterator++)
                                    {
                                        if (zB == (Block*)iterator)
                                        {
                                            iterator.remove();
                                            break;
                                        }
                                    }
                                }
                                c->vcs.unlock();
                            }
                        }
                    }
                }
            }
        }
    }
}

Block* Chunk::zBlockAt(Framework::Vec3<int> location)
{
    cs.lock();
    for (Block* b : blocks)
    {
        if (b->getLocation() == location)
        {
            cs.unlock();
            return b;
        }
    }
    cs.unlock();
    return 0;
}

void Chunk::setBlock(Block* block)
{
    cs.lock();
    Framework::Vec3<int> pos = block->getLocation();
    for (Framework::Iterator<Block*> iterator = blocks.begin(); iterator;
         iterator++)
    {
        if (pos == iterator->getLocation())
        {
            vcs.lock();
            for (Framework::Iterator<Block*> vi = visibleBlocks.begin(); vi;
                 vi++)
            {
                if ((Block*)iterator == (Block*)vi)
                {
                    vi.remove();
                    break;
                }
            }
            vcs.unlock();
            iterator->release();
            iterator.set(block);
            cs.unlock();
            return;
        }
    }
    blocks.add(block);
    cs.unlock();
}

void Chunk::removeBlock(Block* zBlock)
{
    cs.lock();
    vcs.lock();
    for (Framework::Iterator<Block*> iterator = visibleBlocks.begin(); iterator;
         iterator++)
    {
        if (zBlock == (Block*)iterator)
        {
            iterator.remove();
            break;
        }
    }
    vcs.unlock();
    for (Framework::Iterator<Block*> iterator = blocks.begin(); iterator;
         iterator++)
    {
        if (zBlock == (Block*)iterator)
        {
            iterator.remove();
            break;
        }
    }
    cs.unlock();
}

void Chunk::load(Framework::StreamReader* zReader)
{
    cs.lock();
    blocks.leeren();
    cs.unlock();
    Trie<Block> blockCache;
    isLoading = 1;
    Framework::Vec3<int> pos = {0, 0, 0};
    unsigned short id;
    zReader->lese((char*)&id, 2);
    while (id)
    {
        int index;
        zReader->lese((char*)&index, 4);
        pos = Vec3<int>((index / WORLD_HEIGHT) / CHUNK_SIZE,
            (index / WORLD_HEIGHT) % CHUNK_SIZE,
            index % WORLD_HEIGHT);
        if (blockTypes[id]->doesNeedInstance())
        {
            cs.lock();
            Block* b = blockTypes[id]->createBlock(
                {pos.x + location.x - CHUNK_SIZE / 2,
                    pos.y + location.y - CHUNK_SIZE / 2,
                    pos.z});
            blocks.add(b);
            blockCache.set((char*)&index, 4, dynamic_cast<Block*>(b->getThis()));
            cs.unlock();
        }
        zReader->lese((char*)&id, 2);
    }
    int index = 0;
    // light
    zReader->lese((char*)&index, 4);
    char lightData[6];
    while (index >= 0)
    {
        zReader->lese(lightData, 6);
        Framework::Vec3<int> location((index / WORLD_HEIGHT) / CHUNK_SIZE,
            (index / WORLD_HEIGHT) % CHUNK_SIZE,
            index % WORLD_HEIGHT);
        for (int i = 0; i < 6; i++)
        {
            Framework::Vec3<int> pos
                = location + getDirection(getDirectionFromIndex(i));
            if (pos.z >= 0 && pos.z < WORLD_HEIGHT)
            {
                if (pos.x >= 0 && pos.x < CHUNK_SIZE && pos.y >= 0
                    && pos.y < CHUNK_SIZE)
                {
                    int cacheIndex
                        = (pos.x * CHUNK_SIZE + pos.y) * WORLD_HEIGHT + pos.z;
                    Block* zB
                        = blockCache.z((char*)&cacheIndex, 4);
                    if (zB)
                    {
                        bool visible = zB->isVisible();
                        zB->setLightData(
                            getOppositeDirection(getDirectionFromIndex(i)),
                            (unsigned char*)lightData);
                        if (zB->isVisible() && !visible)
                        {
                            vcs.lock();
                            visibleBlocks.add(zB);
                            vcs.unlock();
                        }
                    }
                }
                else
                {
                    pos.x += this->location.x - CHUNK_SIZE / 2;
                    pos.y += this->location.y - CHUNK_SIZE / 2;
                    Block* zB = World::INSTANCE->zBlockAt(pos);
                    if (zB)
                    {
                        bool visible = zB->isVisible();
                        zB->setLightData(
                            getOppositeDirection(getDirectionFromIndex(i)),
                            (unsigned char*)lightData);
                        if (zB->isVisible() && !visible)
                        {
                            Chunk* c = World::INSTANCE->zChunk(
                                World::INSTANCE->getChunkCenter(pos.x, pos.y));
                            c->vcs.lock();
                            c->visibleBlocks.add(zB);
                            c->vcs.unlock();
                        }
                    }
                }
            }
        }
        zReader->lese((char*)&index, 4);
    }
    isLoading = 0;
}

Framework::Punkt Chunk::getCenter() const
{
    return location;
}

Framework::Vec3<int> Chunk::getMin() const
{
    return {location.x - CHUNK_SIZE / 2, location.y - CHUNK_SIZE / 2, 0};
}

Framework::Vec3<int> Chunk::getMax() const
{
    return {
        location.x + CHUNK_SIZE / 2, location.y + CHUNK_SIZE / 2, WORLD_HEIGHT};
}

void Chunk::forAll(std::function<void(Model3D*)> f)
{
    vcs.lock();
    for (Block* b : visibleBlocks)
        f(b);
    vcs.unlock();
}