#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;
        }
    }
}

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->copyLightTo(block);
            iterator->release();
            iterator.set(block);
            cs.unlock();
            vcs.lock();
            if (block->isVisible())
            {
                visibleBlocks.add(block);
            }
            vcs.unlock();
            return;
        }
    }
    blocks.add(block);
    cs.unlock();
    vcs.lock();
    if (block->isVisible())
    {
        visibleBlocks.add(block);
    }
    vcs.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();
            vcs.lock();
            if (b->isVisible())
            {
                visibleBlocks.add(b);
            }
            vcs.unlock();
        }
        zReader->lese((char*)&id, 2);
    }
    int index = 0;
    // light
    zReader->lese((char*)&index, 4);
    char lightData[6];
    while (index >= -1)
    {
        if (index == -1)
        {
            int x = 0;
            int y = 0;
            int z = 0;
            zReader->lese((char*)&x, 4);
            zReader->lese((char*)&y, 4);
            zReader->lese((char*)&z, 4);
            zReader->lese(lightData, 6);
            if (x == -1)
            {
                int cacheIndex
                    = y * WORLD_HEIGHT + z;
                Block* zB = blockCache.z((char*)&cacheIndex, 4);
                if (zB)
                {
                    zB->setLightData(WEST, (unsigned char*)lightData);
                }
            }
            else if (y == -1)
            {
                int cacheIndex = (x * CHUNK_SIZE) * WORLD_HEIGHT + z;
                Block* zB = blockCache.z((char*)&cacheIndex, 4);
                if (zB)
                {
                    zB->setLightData(NORTH, (unsigned char*)lightData);
                }
            }
            else if (x == CHUNK_SIZE)
            {
                int cacheIndex = ((CHUNK_SIZE - 1) * CHUNK_SIZE + y) * WORLD_HEIGHT + z;
                Block* zB = blockCache.z((char*)&cacheIndex, 4);
                if (zB)
                {
                    zB->setLightData(EAST, (unsigned char*)lightData);
                }
            }
            else if (y == CHUNK_SIZE)
            {
                int cacheIndex
                    = (x * CHUNK_SIZE + (CHUNK_SIZE - 1)) * WORLD_HEIGHT + z;
                Block* zB = blockCache.z((char*)&cacheIndex, 4);
                if (zB)
                {
                    zB->setLightData(SOUTH, (unsigned char*)lightData);
                }
            }
        }
        else
        {
            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;
}

void Chunk::blockVisibilityChanged(Block* zB)
{
    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();
}

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();
}