package electrosphere.server.block.manager;

import electrosphere.client.block.BlockChunkCache;
import electrosphere.client.block.BlockChunkData;
import electrosphere.engine.Globals;
import electrosphere.game.server.world.ServerWorldData;
import electrosphere.server.block.diskmap.ServerBlockChunkDiskMap;
import electrosphere.util.annotation.Exclude;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

import org.joml.Vector3i;

/**
 * Provides an interface for the server to query information about block chunks
 */
public class ServerBlockManager {

    /**
     * The number of threads for chunk generation
     */
    public static final int GENERATION_THREAD_POOL_SIZE = 2;
    
    /**
     * The parent world data
     */
    ServerWorldData parent;
    
    /**
     * The cache of chunks
     */
    @Exclude
    BlockChunkCache chunkCache = new BlockChunkCache();

    //The map of chunk position <-> file on disk containing chunk data
    ServerBlockChunkDiskMap chunkDiskMap = null;

    /**
     * The threadpool for chunk generation
     */
    @Exclude
    static final ExecutorService chunkExecutorService = Executors.newFixedThreadPool(GENERATION_THREAD_POOL_SIZE);    
    
    /**
     * Constructor
     */
    public ServerBlockManager(
        ServerWorldData parent
    ){
        this.parent = parent;
    }
    
    ServerBlockManager(){
        
    }
    
    /**
     * Inits the chunk disk map
     */
    public void generate(){
        this.chunkDiskMap = ServerBlockChunkDiskMap.init();
    }
    
    /**
     * Saves the block cache backing this manager to a save file
     * @param saveName The name of the save
     */
    public void save(String saveName){
        //for each chunk, save via disk map
        for(BlockChunkData chunk : this.chunkCache.getContents()){
            chunkDiskMap.saveToDisk(chunk);
        }
        //save disk map itself
        if(chunkDiskMap != null){
            chunkDiskMap.save();
        }
    }
    
    /**
     * Loads a block manager from a save file
     * @param saveName The name of the save
     */
    public void load(String saveName){
        //load chunk disk map
        chunkDiskMap = ServerBlockChunkDiskMap.init(saveName);
    }

    /**
     * Evicts all cached chunks
     */
    public void evictAll(){
        this.chunkCache.clear();
    }
    
    /**
     * Performs logic once a server chunk is available
     * @param worldX The world x position
     * @param worldY The world y position
     * @param worldZ The world z position
     * @return The BlockChunkData
     */
    public BlockChunkData getChunk(int worldX, int worldY, int worldZ){
        Globals.profiler.beginAggregateCpuSample("ServerBlockManager.getChunk");
        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
        BlockChunkData returnedChunk = null;
        if(chunkCache.containsChunk(worldX,worldY,worldZ,BlockChunkData.LOD_FULL_RES)){
            returnedChunk = chunkCache.get(worldX,worldY,worldZ, BlockChunkData.LOD_FULL_RES);
        } else {
            //pull from disk if it exists
            if(chunkDiskMap != null){
                if(chunkDiskMap.containsBlocksAtPosition(worldX, worldY, worldZ)){
                    returnedChunk = chunkDiskMap.getBlockChunk(worldX, worldY, worldZ);
                }
            }
            //generate if it does not exist
            if(returnedChunk == null){
                returnedChunk = BlockChunkData.allocate();
                returnedChunk.setWorldX(worldX);
                returnedChunk.setWorldY(worldY);
                returnedChunk.setWorldZ(worldZ);
                returnedChunk.setHomogenousValue(0);
                // if(worldX == 0 && worldY == 0 && worldZ == 0){
                //     returnedChunk.setHomogenousValue(BlockChunkData.NOT_HOMOGENOUS);
                //     for(int x = 3; x < 16; x++){
                //         for(int y = 8; y < 16; y++){
                //             for(int z = 3; z < 16; z++){
                //                 returnedChunk.setType(x, y, z, 1);
                //             }
                //         }
                //     }
                // }
            }
            this.chunkCache.add(worldX, worldY, worldZ, BlockChunkData.LOD_FULL_RES, returnedChunk);
        }
        Globals.profiler.endCpuSample();
        return returnedChunk;
    }

    /**
     * Performs logic once a server chunk is available
     * @param worldX The world x position
     * @param worldY The world y position
     * @param worldZ The world z position
     * @param stride The stride of the data
     * @param onLoad The logic to run once the chunk is available
     */
    public void getChunkAsync(int worldX, int worldY, int worldZ, int stride, Consumer<BlockChunkData> onLoad){
        Globals.profiler.beginAggregateCpuSample("ServerBlockManager.getChunkAsync");
        chunkExecutorService.submit(new ServerBlockChunkGenerationThread(chunkDiskMap, chunkCache, worldX, worldY, worldZ, stride, onLoad));
        Globals.profiler.endCpuSample();
    }

    /**
     * Saves a given position's chunk to disk. 
     * Uses the current global save name
     * @param position The position to save
     */
    public void savePositionToDisk(Vector3i position){
        if(chunkDiskMap != null){
            chunkDiskMap.saveToDisk(getChunk(position.x, position.y, position.z));
        }
    }
    
    /**
     * Applies an edit to a block at a given location
     * @param worldPos The world coordinates of the chunk to modify
     * @param voxelPos The voxel coordinates of the voxel to modify
     * @param type The type of block
     * @param metadata The metadata of the block
     */
    public void editBlockAtLocationToValue(Vector3i worldPos, Vector3i voxelPos, short type, short metadata){
        if(chunkCache.containsChunk(worldPos.x,worldPos.y,worldPos.z,BlockChunkData.LOD_FULL_RES)){
            BlockChunkData chunk = chunkCache.get(worldPos.x,worldPos.y,worldPos.z, BlockChunkData.LOD_FULL_RES);
            chunk.setType(voxelPos.x, voxelPos.y, voxelPos.z, type);
            chunk.setMetadata(voxelPos.x, voxelPos.y, voxelPos.z, metadata);
            chunk.setHomogenousValue(BlockChunkData.NOT_HOMOGENOUS);
        }
    }

    /**
     * Sets the parent world data of this manager
     * @param serverWorldData The parent world data
     */
    public void setParent(ServerWorldData serverWorldData){
        this.parent = serverWorldData;
    }
    
    /**
     * Closes the generation threadpool
     */
    public void closeThreads(){
        chunkExecutorService.shutdownNow();
    }

}
