package electrosphere.client.foliagemanager;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector3i;
import org.lwjgl.BufferUtils;

import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityCreationUtils;
import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.foliage.AmbientFoliage;
import electrosphere.game.data.foliage.type.FoliageType;
import electrosphere.renderer.actor.instance.TextureInstancedActor;
import electrosphere.renderer.buffer.ShaderAttribute;
import electrosphere.renderer.buffer.HomogenousUniformBuffer.HomogenousBufferTypes;
import electrosphere.renderer.texture.Texture;

/**
 * Manages ambient foliage (grass, small plants, etc) that should be shown, typically instanced
 */
public class ClientFoliageManager {

    //Random for finding new positions for foliage
    Random placementRandomizer = new Random();

    //Used to prevent concurrent usage of grassEntities set
    boolean ready = false;

    //The list of voxel type ids that should have grass generated on top of them
    static final List<Integer> grassGeneratingVoxelIds = new ArrayList<Integer>();

    //FoliageCells that are active and have foliage that is being drawn
    Set<FoliageCell> activeCells = new HashSet<FoliageCell>();
    //map of position-based key to foliage cell at the position
    Map<String,FoliageCell> locationCellMap = new HashMap<String,FoliageCell>();
    //The maximum distance a cell can be away from the player before being destroyed
    static final float CELL_DISTANCE_MAX = 15f;
    
    //The maximum number of foliage cells

    static final int CELL_COUNT_MAX = 1000;
    //the interval to space along
    static final int TARGET_FOLIAGE_SPACING = 50;
    //The target number of foliage to place per cell
    static final int TARGET_FOLIAGE_PER_CELL = TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING;
    //size of a single item of foliage in the texture buffer
    /*
     * A lot of these are x 4 to account for size of float
     * 3 x 4 for position
     * 2 x 4 for euler rotation
     * 
     * 
     * eventually:
     * grass type
     * color
     * wind characteristics?
     */
    static final int SINGLE_FOLIAGE_DATA_SIZE_BYTES = 3 * 4 + 2 * 4;
    //Stores a list of all locations that are currently invalid which map to
    //the amount of frames that must pass before they are considered valid to evaluate
    Map<String,Integer> locationEvaluationCooldownMap = new ConcurrentHashMap<String,Integer>();
    //The number of frames that must pass before a cell can be reevaluated for foliage placement
    static final int EVALUATION_COOLDOWN = 100;




    //The map of all attributes for instanced foliage
    static final Map<ShaderAttribute,HomogenousBufferTypes> attributes = new HashMap<ShaderAttribute,HomogenousBufferTypes>();

    //model matrix shader attribute
    static ShaderAttribute modelMatrixAttribute;
    
    //set attributes
    static {
        int[] attributeIndices = new int[]{
            5,6,7,8
        };
        modelMatrixAttribute = new ShaderAttribute(attributeIndices);
        attributes.put(modelMatrixAttribute,HomogenousBufferTypes.MAT4F);

        //set grass generating voxel ids
        grassGeneratingVoxelIds.add(2);
    }

    //shader paths
    static final String vertexPath = "Shaders/foliage/foliage.vs";
    static final String fragmentPath = "Shaders/foliage/foliage.fs";












    /**
     * Starts up the foliage manager
     */
    public void start(){
        //queue ambient foliage models
        for(FoliageType foliageType : Globals.gameConfigCurrent.getFoliageMap().getFoliageList()){
            if(foliageType.getTokens().contains(FoliageType.TOKEN_AMBIENT)){
                Globals.assetManager.addModelPathToQueue(foliageType.getModelPath());
                Globals.assetManager.addShaderToQueue(vertexPath, fragmentPath);
            }
        }
        ready = true;
    }

    /**
     * Updates all grass entities
     */
    public void update(){
        Globals.profiler.beginCpuSample("ClientFoliageManager.update");
        if(ready){
            Vector3d playerPosition = null;
            if(Globals.playerEntity != null){
                playerPosition = EntityUtils.getPosition(Globals.playerEntity);
            }
            Globals.profiler.beginCpuSample("ClientFoliageManager.update (talley invalid cells)");
            //flip through all valid cells and see if you can invalidate any
            List<FoliageCell> invalidationList = new LinkedList<FoliageCell>();
            for(FoliageCell cell : activeCells){
                if(
                    playerPosition != null && 
                    playerPosition.distance(cell.realPosition) > CELL_DISTANCE_MAX
                    ){
                    invalidationList.add(cell);
                }
            }
            Globals.profiler.endCpuSample();
            Globals.profiler.beginCpuSample("ClientFoliageManager.update (actually invalidate cells)");
            for(FoliageCell cell : invalidationList){
                invalidateCell(cell);
            }
            Globals.profiler.endCpuSample();
            invalidationList.clear();
            //for each invalid cell, see if can be revalidated
            Globals.profiler.beginCpuSample("ClientFoliageManager.update (revalidate cells)");
            for(String key : locationEvaluationCooldownMap.keySet()){
                int cooldownTime = locationEvaluationCooldownMap.get(key);
                cooldownTime--;
                if(cooldownTime <= 0){
                    String split[] = key.split("_");
                    Vector3i worldPos = new Vector3i(Integer.parseInt(split[0]),Integer.parseInt(split[1]),Integer.parseInt(split[2]));
                    Vector3i voxelPos = new Vector3i(Integer.parseInt(split[3]),Integer.parseInt(split[4]),Integer.parseInt(split[5]));
                    Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(voxelPos.x,voxelPos.y,voxelPos.z);
                    ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
                    //evaluate
                    if(
                        data != null &&
                        data.getWeight(voxelPos) > 0 && 
                        data.getWeight(new Vector3i(voxelPos.x,voxelPos.y + 1,voxelPos.z)) < 0 &&
                        typeSupportsFoliage(data.getType(voxelPos)) &&
                        playerPosition.distance(realPos) < CELL_DISTANCE_MAX
                    ){
                        //create foliage cell
                        createFoliageCell(worldPos,voxelPos,1);
                        locationEvaluationCooldownMap.remove(key);
                    } else {
                        locationEvaluationCooldownMap.put(key, EVALUATION_COOLDOWN);
                    }
                } else {
                    locationEvaluationCooldownMap.put(key, cooldownTime);
                }
            }
            Globals.profiler.endCpuSample();
            //invalidate foliage cells that have had their voxel changed
            invalidateModifiedPositions();
        }
        Globals.profiler.endCpuSample();
    }

    /**
     * Gets a good position to put a new blade of grass
     * @param centerPosition The player's position
     * @return The new position for the blade of grass
     */
    protected Vector3d getNewPosition(Vector3d centerPosition){
        double angle = placementRandomizer.nextDouble() * Math.PI * 2;
        double radius = placementRandomizer.nextDouble();
        return new Vector3d(
            centerPosition.x + Math.cos(angle) * radius,
            centerPosition.y,
            centerPosition.z + Math.sin(angle) * radius
        );
    }

    /**
     * Gets a new rotation for a blade of grass
     * @return The rotation
     */
    protected Quaterniond getNewRotation(){
        return new Quaterniond().rotationX(-Math.PI / 2.0f).rotateLocalY(Math.PI * placementRandomizer.nextFloat()).normalize();
    }

    /**
     * Gets a key for a foliage cell in the localCellMap
     * @param worldPosition The world position of the cell
     * @param voxelPosition The voxel position of the cell
     * @return The key for the cell
     */
    private String getFoliageCellKey(Vector3i worldPosition, Vector3i voxelPosition){
        return worldPosition.x + "_" + worldPosition.y + "_" + worldPosition.z + "_" + voxelPosition.x + "_" + voxelPosition.y + "_" + voxelPosition.z;
    }


    /**
     * Makes an already created entity a drawable, instanced entity (client only) by backing it with an InstancedActor
     * @param entity The entity
     * @param modelPath The model path for the model to back the instanced actor
     * @param capacity The capacity of the instanced actor to draw
     */
    public static void makeEntityTextureInstancedFoliage(Entity entity, String modelPath, int capacity){
        entity.putData(EntityDataStrings.INSTANCED_ACTOR, Globals.clientInstanceManager.createInstancedActor(modelPath, vertexPath, fragmentPath, attributes, capacity));
        entity.putData(EntityDataStrings.DATA_STRING_POSITION, new Vector3d(0,0,0));
        entity.putData(EntityDataStrings.DATA_STRING_ROTATION, new Quaterniond().identity());
        entity.putData(EntityDataStrings.DATA_STRING_SCALE, new Vector3f(1,1,1));
        entity.putData(EntityDataStrings.DRAW_SOLID_PASS, true);
        Globals.clientScene.registerEntity(entity);
        Globals.clientScene.registerEntityToTag(entity, EntityTags.DRAW_INSTANCED_MANAGED);
    }

    /**
     * Evaluates a chunk to see where foliage cells should be created or updated
     * @param worldPos The world position of the chunk
     */
    public void evaluateChunk(Vector3i worldPos){
        ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
        for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
            //can't go to very top 'cause otherwise there would be no room to put grass
            for(int y = 0; y < ChunkData.CHUNK_SIZE - 1; y++){
                for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
                    Vector3i currentPos = new Vector3i(x,y,z);
                    String key = getFoliageCellKey(worldPos, currentPos);
                    if(locationCellMap.get(key) != null){
                        //destroy if there's no longer ground or 
                        //if the cell above is now occupied or
                        //if the lower cell is no longer supporting foliage
                        if(
                            data.getWeight(currentPos) <= 0 ||
                            data.getWeight(new Vector3i(x,y + 1,z)) > 0 ||
                            !typeSupportsFoliage(data.getType(currentPos))
                        ){
                            //destroy
                            FoliageCell toDestroy = locationCellMap.get(key);
                            toDestroy.destroy();
                            activeCells.remove(toDestroy);
                            locationCellMap.remove(key);
                        } else {
                            //TODO: evaluate if foliage is placed well
                        }
                    } else {
                        //create if current is ground and above is air
                        if(
                            !locationEvaluationCooldownMap.containsKey(key) &&
                            data.getWeight(currentPos) > 0 && 
                            data.getWeight(new Vector3i(x,y + 1,z)) < 0 &&
                            typeSupportsFoliage(data.getType(currentPos)) &&
                            activeCells.size() < CELL_COUNT_MAX
                        ){
                            //create foliage cell
                            createFoliageCell(worldPos,currentPos,1);
                        }
                    }
                }
            }
        }
        //evaluate top cells if chunk above this one exists
        ChunkData aboveData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0));
        if(aboveData != null){
            for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
                for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
                    Vector3i currentPos = new Vector3i(x,ChunkData.CHUNK_SIZE-1,z);
                    String key = getFoliageCellKey(worldPos, currentPos);
                    if(locationCellMap.get(key) != null){
                        //destroy if there's no longer ground or 
                        //if the cell above is now occupied or
                        //if the lower cell is no longer supporting foliage
                        if(
                            data.getWeight(currentPos) <= 0 ||
                            aboveData.getWeight(new Vector3i(x,0,z)) > 0 ||
                            !typeSupportsFoliage(data.getType(currentPos))
                        ){
                            //destroy
                            FoliageCell toDestroy = locationCellMap.get(key);
                            toDestroy.destroy();
                            activeCells.remove(toDestroy);
                            locationCellMap.remove(key);
                        } else {
                            //TODO: evaluate if foliage is placed well
                        }
                    } else {
                        //create if current is ground and above is air
                        if(
                            data.getWeight(currentPos) > 0 && 
                            aboveData.getWeight(new Vector3i(x,0,z)) < 0 &&
                            typeSupportsFoliage(data.getType(currentPos)) &&
                            activeCells.size() < CELL_COUNT_MAX
                        ){
                            //create foliage cell
                            createFoliageCell(worldPos,currentPos,1);
                        }
                    }
                }
            }
        }
    }

    //the length of the ray to ground test with
    static final float RAY_LENGTH = 2.0f;

    //the height above the chunk to start from when sampling downwards
    static final float SAMPLE_START_HEIGHT = 1.0f;

    /**
     * Creates a foliage cell at a given position
     * @param worldPos The world position
     * @param voxelPos The voxel position
     */
    private void createFoliageCell(Vector3i worldPos, Vector3i voxelPos, float initialGrowthLevel){
        //get foliage types supported
        ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
        List<String> foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPos)).getAmbientFoliage();
        if(foliageTypesSupported != null){
            Vector3d realPos = new Vector3d(
                worldPos.x * ChunkData.CHUNK_SIZE + voxelPos.x,
                worldPos.y * ChunkData.CHUNK_SIZE + voxelPos.y,
                worldPos.z * ChunkData.CHUNK_SIZE + voxelPos.z
            );

            //get type
            String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size());
            FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName);

            //create cell and buffer
            FoliageCell cell = new FoliageCell(worldPos, voxelPos, realPos);
            ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
            FloatBuffer floatBufferView = buffer.asFloatBuffer();
            //construct simple grid to place foliage on
            Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT,   0), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(   0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(   0,SAMPLE_START_HEIGHT,   0), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(   0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT,   0), new Vector3d(0,-1,0), RAY_LENGTH);
            Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
            //get the heights of each sample
            float height_11 = (float)(sample_11 != null ? sample_11.y : 0);
            float height_00 = (float)(sample_00 != null ? sample_00.y : height_11);
            float height_01 = (float)(sample_01 != null ? sample_01.y : height_11);
            float height_02 = (float)(sample_02 != null ? sample_02.y : height_11);
            float height_10 = (float)(sample_10 != null ? sample_10.y : height_11);
            float height_12 = (float)(sample_12 != null ? sample_12.y : height_11);
            float height_20 = (float)(sample_20 != null ? sample_20.y : height_11);
            float height_21 = (float)(sample_21 != null ? sample_21.y : height_11);
            float height_22 = (float)(sample_22 != null ? sample_22.y : height_11);
            //each height is in real world coordinates that are absolute
            //when rendering, there's already a y offset for the center of the field of grass (based on the model matrix)
            //so when offseting the position of the blade of grass RELATIVE to the overall instance being drawn, need to subtract the real world coordinates of the overall instance
            //in other words realPos SPECIFICALLY for the y dimension, for x and z you don't need to worry about it
            
            //if we don't find data for the center sample, can't place grass so don't create entity
            if(sample_11 != null){
                //generate positions to place
                int drawCount = 0;
                for(int x = 0; x < TARGET_FOLIAGE_SPACING; x++){
                    for(int z = 0; z < TARGET_FOLIAGE_SPACING; z++){
                        //get position to place
                        double rand1 = placementRandomizer.nextDouble();
                        double rand2 = placementRandomizer.nextDouble();
                        double relativePositionOnGridX = x /  (1.0 * TARGET_FOLIAGE_SPACING) + rand1 / TARGET_FOLIAGE_SPACING;
                        double relativePositionOnGridZ = z /  (1.0 * TARGET_FOLIAGE_SPACING) + rand2 / TARGET_FOLIAGE_SPACING;
                        double offsetX = relativePositionOnGridX - 0.5;
                        double offsetZ = relativePositionOnGridZ - 0.5;
                        //determine quadrant we're placing in
                        double offsetY = 0;
                        boolean addBlade = false;
                        if(relativePositionOnGridX >=0.5){
                            if(relativePositionOnGridZ >= 0.5){
                                relativePositionOnGridX = relativePositionOnGridX - 0.5;
                                relativePositionOnGridZ = relativePositionOnGridZ - 0.5;
                                relativePositionOnGridX /= 0.5;
                                relativePositionOnGridZ /= 0.5;
                                // System.out.println(relativePositionOnGridX + " " + relativePositionOnGridZ);
                                //if we have heights for all four surrounding spots, interpolate for y value
                                if(sample_11 != null && sample_12 != null && sample_21 != null && sample_22 != null){
                                    offsetY = 
                                        height_11 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_12 * (1-relativePositionOnGridX) * (  relativePositionOnGridZ) + 
                                        height_21 * (  relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_22 * (  relativePositionOnGridX) * (  relativePositionOnGridZ);
                                    addBlade = true;
                                }
                            } else {
                                relativePositionOnGridX = relativePositionOnGridX - 0.5;
                                relativePositionOnGridX /= 0.5;
                                relativePositionOnGridZ /= 0.5;
                                //if we have heights for all four surrounding spots, interpolate for y value
                                if(sample_10 != null && sample_11 != null && sample_20 != null && sample_21 != null){
                                    offsetY = 
                                        height_10 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_11 * (1-relativePositionOnGridX) * (  relativePositionOnGridZ) + 
                                        height_20 * (  relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_21 * (  relativePositionOnGridX) * (  relativePositionOnGridZ);
                                    addBlade = true;
                                }
                            }
                        } else {
                            if(relativePositionOnGridZ >= 0.5){
                                relativePositionOnGridZ = relativePositionOnGridZ - 0.5;
                                relativePositionOnGridX /= 0.5;
                                relativePositionOnGridZ /= 0.5;
                                //if we have heights for all four surrounding spots, interpolate for y value
                                if(sample_01 != null && sample_02 != null && sample_11 != null && sample_12 != null){
                                    offsetY = 
                                        height_01 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_02 * (1-relativePositionOnGridX) * (  relativePositionOnGridZ) + 
                                        height_11 * (  relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_12 * (  relativePositionOnGridX) * (  relativePositionOnGridZ);
                                    addBlade = true;
                                }
                            } else {
                                relativePositionOnGridX /= 0.5;
                                relativePositionOnGridZ /= 0.5;
                                //if we have heights for all four surrounding spots, interpolate for y value
                                if(sample_00 != null && sample_01 != null && sample_10 != null && sample_11 != null){
                                    offsetY = 
                                        height_00 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_01 * (1-relativePositionOnGridX) * (  relativePositionOnGridZ) + 
                                        height_10 * (  relativePositionOnGridX) * (1-relativePositionOnGridZ) + 
                                        height_11 * (  relativePositionOnGridX) * (  relativePositionOnGridZ);
                                    addBlade = true;
                                }
                            }
                        }
                        if(addBlade){
                            //convert y to relative to chunk
                            offsetY = offsetY - realPos.y;
                            double rotVar = placementRandomizer.nextDouble() * Math.PI * 2;
                            double rotVar2 = placementRandomizer.nextDouble();
                            floatBufferView.put((float)offsetX);
                            floatBufferView.put((float)offsetY);
                            floatBufferView.put((float)offsetZ);
                            floatBufferView.put((float)rotVar);
                            floatBufferView.put((float)rotVar2);
                            drawCount++;
                        }
                    }
                }
                buffer.position(0);
                buffer.limit(TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
                //construct data texture
                Texture dataTexture = new Texture(buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING);
                
                //create entity
                Entity grassEntity = EntityCreationUtils.createClientSpatialEntity();

                TextureInstancedActor.attachTextureInstancedActor(grassEntity, foliageType.getModelPath(), vertexPath, fragmentPath, dataTexture, drawCount);
                EntityUtils.getPosition(grassEntity).set(realPos);
                EntityUtils.getRotation(grassEntity).set(0,0,0,1);
                EntityUtils.getScale(grassEntity).set(1,1,1);
                //add ambient foliage behavior tree
                AmbientFoliage.attachAmbientFoliageTree(grassEntity, initialGrowthLevel, foliageType.getGrowthModel().getGrowthRate());
                cell.addEntity(grassEntity);


                activeCells.add(cell);
                locationCellMap.put(getFoliageCellKey(worldPos, voxelPos),cell);
            }
        }
    }

    /**
     * Gets whether the voxel type supports foliage or not
     * @param type
     * @return
     */
    private boolean typeSupportsFoliage(int type){
        if(Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type) != null){
            return Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type).getAmbientFoliage() != null;
        }
        return false;
    }

    /**
     * Invalidates a foliage cell at a position, destroying all foliage in the cell and unregistering it.
     * Furthermore, it adds it to a cooldown queue to wait until it can recreate foliage
     * @param worldPosition The world position of the cell
     * @param voxelPosition The voxel position of the cell
     */
    private void invalidateCell(Vector3i worldPosition, Vector3i voxelPosition){
        String key = getFoliageCellKey(worldPosition, voxelPosition);
        if(!locationEvaluationCooldownMap.containsKey(key)){
            locationEvaluationCooldownMap.put(key,EVALUATION_COOLDOWN);
            FoliageCell cell = locationCellMap.get(key);
            if(cell != null){
                //destroy
                FoliageCell toDestroy = locationCellMap.get(key);
                toDestroy.destroy();
                activeCells.remove(toDestroy);
                locationCellMap.remove(key);
            }
        }
    }

    /**
     * Invalidates a cell by direct reference
     * @param cell The cell to invalidate
     */
    private void invalidateCell(FoliageCell cell){
        String key = getFoliageCellKey(cell.worldPosition, cell.voxelPosition);
        if(!locationEvaluationCooldownMap.containsKey(key)){
            locationEvaluationCooldownMap.put(key,EVALUATION_COOLDOWN);
        }
        //destroy
        FoliageCell toDestroy = locationCellMap.get(key);
        toDestroy.destroy();
        activeCells.remove(toDestroy);
        locationCellMap.remove(key);
    }

    /**
     * Invalidates the foliage cells for all modified chunks
     */
    private void invalidateModifiedPositions(){
        for(ChunkData chunk : Globals.clientTerrainManager.getAllChunks()){
            if(chunk.getModifiedPositions().size() > 0){
                for(Vector3i position : chunk.getModifiedPositions()){
                    Globals.clientFoliageManager.invalidateCell(Globals.clientTerrainManager.getPositionOfChunk(chunk), position);
                }
                chunk.resetModifiedPositions();
            }
        }
    }

    /**
     * Draws all foliage in the foliage manager
     */
    public void draw(){
        for(FoliageCell cell : activeCells){
            cell.draw(modelMatrixAttribute);
        }
    }



}
