#include "Dimension.h"
#include "Constants.h"
#include "Datei.h"
#include "Game.h"

using namespace Framework;


Dimension::Dimension( int id )
    : dimensionId( id ),
    gravity( 9.8f ),
    chunks( new Trie<Chunk>() ),
    entities( new RCArray<Entity>() )
{}

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

void Dimension::api( Framework::StreamReader* zRequest, NetworkResponse* zResponse )
{
    // TODO: switch type chunck, block, entity
}

void Dimension::tickEntities()
{
    for( auto entity : *entities )
    {
        if( zChunk( Punkt( (int)entity->getPosition().x, (int)entity->getPosition().y ) ) )
            entity->prepareTick( this );
    }
    int index = 0;
    Array<int> removed;
    for( auto entity : *entities )
    {
        if( zChunk( Punkt( (int)entity->getPosition().x, (int)entity->getPosition().y ) ) )
            entity->tick( this );
        if( entity->isRemoved() )
            removed.add( index, 0 );
        index++;
    }
    for( int i : removed )
        entities->remove( i );
}

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

void Dimension::getAddrOfWorld( Punkt 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 );
}

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

Framework::Either<Block*, int> Dimension::zBlock( Vec3<int> location )
{
    Chunk* c = zChunk( Game::INSTANCE->getChunkCenter( location.x, location.y ) );
    if( c )
    {
        int x = location.x % CHUNK_SIZE;
        int y = location.y % CHUNK_SIZE;
        if( x < 0 )
            x += CHUNK_SIZE;
        if( y < 0 )
            y += CHUNK_SIZE;
        return c->zBlockAt( Vec3<int>( x, y, location.z ) );
    }
    return 0;
}

Block* Dimension::zRealBlockInstance( Framework::Vec3<int> location )
{
    Chunk* c = zChunk( Game::INSTANCE->getChunkCenter( location.x, location.y ) );
    if( c )
    {
        int x = location.x % CHUNK_SIZE;
        int y = location.y % CHUNK_SIZE;
        if( x < 0 )
            x += CHUNK_SIZE;
        if( y < 0 )
            y += CHUNK_SIZE;
        c->instantiateBlock( Vec3<int>( x, y, location.z ) );
        return c->zBlockAt( Vec3<int>( x, y, location.z ) );
    }
    return 0;
}

void Dimension::placeBlock( Framework::Vec3<int> location, Framework::Either<Block*, int> block )
{
    Chunk* c = zChunk( Game::getChunkCenter( location.x, location.y ) );
    if( c )
    {
        int x = location.x % CHUNK_SIZE;
        int y = location.y % CHUNK_SIZE;
        if( x < 0 )
            x += CHUNK_SIZE;
        if( y < 0 )
            y += CHUNK_SIZE;
        if( block.isA() )
            c->putBlockAt( location, block );
        else
        {
            c->putBlockAt( location, 0 );
            c->putBlockTypeAt( location, block );
        }
    }
    else if( block.isA() )
        block.getA()->release();
}

void Dimension::addEntity( Entity* entity )
{
    entities->add( entity );
}

void Dimension::setChunk( Chunk* chunk, Punkt center )
{
    char addr[ 8 ];
    getAddrOfWorld( center, addr );
    Chunk* old = chunks->z( addr, 8 );
    if( old )
    {
        for( int i = 0; i < chunkList.getEintragAnzahl(); i++ )
        {
            if( chunkList.get( i ) == old )
            {
                chunkList.remove( i );
                break;
            }
        }
    }
    chunks->set( addr, 8, chunk );
    if( chunk )
    {
        chunkList.add( chunk );
        chunk->setAdded();
    }
    getAddrOfWorld( center + Punkt( CHUNK_SIZE, 0 ), addr );
    Chunk* zChunk = chunks->z( addr, 8 );
    if( zChunk )
    {
        zChunk->setNeighbor( WEST, chunk );
        if( chunk )
            chunk->setNeighbor( EAST, zChunk );
    }
    getAddrOfWorld( center + Punkt( -CHUNK_SIZE, 0 ), addr );
    zChunk = chunks->z( addr, 8 );
    if( zChunk )
    {
        zChunk->setNeighbor( EAST, chunk );
        if( chunk )
            chunk->setNeighbor( WEST, zChunk );
    }
    getAddrOfWorld( center + Punkt( 0, CHUNK_SIZE ), addr );
    zChunk = chunks->z( addr, 8 );
    if( zChunk )
    {
        zChunk->setNeighbor( NORTH, chunk );
        if( chunk )
            chunk->setNeighbor( SOUTH, zChunk );
    }
    getAddrOfWorld( center + Punkt( 0, -CHUNK_SIZE ), addr );
    zChunk = chunks->z( addr, 8 );
    if( zChunk )
    {
        zChunk->setNeighbor( SOUTH, chunk );
        if( chunk )
            chunk->setNeighbor( NORTH, zChunk );
    }
}

void Dimension::save( Text worldDir ) const
{
    for( auto chunk = chunks->getIterator(); chunk; chunk++ )
    {
        if( !chunk._ )
            continue;
        Datei* file = new Datei();
        Text filePath = worldDir + "/dim/" + dimensionId + "/";
        filePath.appendHex( chunk->getCenter().x );
        filePath += "_";
        filePath.appendHex( chunk->getCenter().y );
        filePath += ".chunk";
        file->setDatei( filePath );
        if( file->open( Datei::Style::schreiben ) )
            chunk->save( file, StreamTarget::FULL );
        file->close();
        file->release();
    }
    Text filePath = worldDir + "/dim/" + dimensionId + "/entities";
    Datei* file = new Datei();
    file->setDatei( filePath );
    if( file->open( Datei::Style::schreiben ) )
    {
        for( Entity* entity : *entities )
        {
            if( entity->zType()->getId() != PlayerEntityType::ID )
            {
                if( !entity->isRemoved() )
                {
                    int type = entity->zType()->getId();
                    file->schreibe( (char*)&type, 4 );
                    StaticRegistry<EntityType>::INSTANCE.zElement( type )->saveEntity( entity, file );
                }
            }
            else
            {
                Datei pFile;
                pFile.setDatei( worldDir + "/player/" + ((Player*)entity)->getName() );
                if( pFile.open( Datei::Style::schreiben ) )
                    PlayerEntityType::INSTANCE->saveEntity( entity, &pFile );
            }
        }
        file->close();
    }
}

int Dimension::getDimensionId() const
{
    return dimensionId;
}

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

float Dimension::getGravity() const
{
    return gravity;
}

void Dimension::removeOldChunks()
{
    Array<int> removed;
    int index = 0;
    for( Chunk* chunk : chunkList )
    {
        if( !chunk->hasViews() )
            removed.add( index, 0 );
        index++;
    }
    for( int i : removed )
    {
        Chunk* chunk = chunkList.get( i );
        // TODO: save chunk
        chunk->prepareRemove();
        setChunk( 0, chunk->getCenter() );
    }
}