package electrosphere.server.fluid.manager;


import electrosphere.game.server.world.ServerWorldData;
import electrosphere.server.fluid.diskmap.FluidDiskMap;
import electrosphere.server.fluid.generation.FluidGenerator;
import electrosphere.server.fluid.models.FluidModel;
import electrosphere.server.fluid.simulator.ServerFluidSimulator;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
import electrosphere.server.terrain.manager.ServerTerrainManager;
import electrosphere.util.FileUtils;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.joml.Vector3i;

/**
 * Provides an interface for the server to query information about fluid
 */
public class ServerFluidManager {
        
    //the seed for the water    
    long seed;
    
    //The model of the fluid this manager is managing
    FluidModel model;
    
    
    //In memory cache of chunk data
    //Basic idea is we associate string that contains chunk x&y&z with elevation
    //While we incur a penalty with converting ints -> string, think this will
    //offset regenerating the array every time we want a new one
    int cacheSize = 500;
    Map<String, ServerFluidChunk> chunkCache;
    List<String> chunkCacheContents;

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

    //The generation algorithm for this fluid manager
    FluidGenerator chunkGenerator;

    //the fluid simulator
    ServerFluidSimulator serverFluidSimulator;

    //the terrain manager associated
    ServerTerrainManager serverTerrainManager;

    //controls whether fluid simulation should actually happen or not
    boolean simulate = true;

    /**
     * The parent world data
     */
    ServerWorldData parent;
    
    
    /**
     * Constructor
     */
    public ServerFluidManager(
        ServerWorldData parent,
        ServerTerrainManager serverTerrainManager,
        long seed,
        FluidGenerator chunkGenerator
    ){
        this.parent = parent;
        this.serverTerrainManager = serverTerrainManager;
        this.chunkCache = new ConcurrentHashMap<String, ServerFluidChunk>();
        this.chunkCacheContents = new CopyOnWriteArrayList<String>();
        this.seed = seed;
        this.chunkGenerator = chunkGenerator;
    }
    
    ServerFluidManager(){
        
    }
    
    /**
     * Generates a fluid model for the manager
     */
    public void generate(){
        // FluidGenerator terrainGen = new FluidGenerator();
        // terrainGen.setInterpolationRatio(worldSizeDiscrete/200);
        // terrainGen.setVerticalInterpolationRatio(verticalInterpolationRatio);
        // terrainGen.setRandomSeed(seed);
        // model = terrainGen.generateModel();
        // this.chunkGenerator.setModel(model);
        // model.setInterpolationRandomDampener(interpolationRandomDampener);
        // this.chunkDiskMap = new ChunkDiskMap();
    }
    
    /**
     * Saves the fluid model backing this manager to a save file
     * @param saveName The name of the save
     */
    public void save(String saveName){
        if(model != null){
            ByteBuffer buffer = ByteBuffer.allocate(model.getElevation().length * model.getElevation()[0].length * 4);
            FloatBuffer floatView = buffer.asFloatBuffer();
            for(int x = 0; x < model.getElevation().length; x++){
                floatView.put(model.getElevation()[x]);
            }
            floatView.flip();
            FileUtils.saveBinaryToSavePath(saveName, "./fluid.dat", buffer.array());
            FileUtils.serializeObjectToSavePath(saveName, "./fluid.json", model);
        }
        //for each chunk, save via disk map
        for(String chunkKey : chunkCacheContents){
            ServerFluidChunk chunk = chunkCache.get(chunkKey);
            chunkDiskMap.saveToDisk(chunk);
        }
        //save disk map itself
        if(chunkDiskMap != null){
            chunkDiskMap.save();
        }
    }
    
    /**
     * Loads a fluid manager from a save file
     * @param saveName The name of the save
     */
    public void load(String saveName){
        //load fluid model
        model = FileUtils.loadObjectFromSavePath(saveName, "./fluid.json", FluidModel.class);
        chunkGenerator.setModel(model);
        byte[] data = FileUtils.loadBinaryFromSavePath(saveName, "./fluid.dat");
        ByteBuffer buffer = ByteBuffer.wrap(data);
        FloatBuffer floatView = buffer.asFloatBuffer();
        float[][] elevation = new float[parent.getWorldSizeDiscrete()][parent.getWorldSizeDiscrete()];
        for(int x = 0; x < parent.getWorldSizeDiscrete(); x++){
            for(int y = 0; y < parent.getWorldSizeDiscrete(); y++){
                elevation[x][y] = floatView.get();
            }
        }
        model.setElevationArray(elevation);
        //load chunk disk map
        chunkDiskMap = new FluidDiskMap();
        chunkDiskMap.init(saveName);
    }
    
    public float[][] getFluidAtChunk(int x, int y){
        return model.getElevationForChunk(x, y);
    }
    
    public double getHeightAtPosition(double x, double y, double z){
        return y;
    }
    
    public float getDiscreteValue(int x, int y){
        if(model != null){
            return model.getElevation()[x][y];
        } else {
            return 0;
        }
    }
    
    public int getDynamicInterpolationRatio(){
        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
        if(model != null){
            return model.getDynamicInterpolationRatio();
        } else {
            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
            return 0;
        }
    }
    
    public float getRandomDampener(){
        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
        if(model != null){
            return model.getRandomDampener();
        } else {
            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
            return 0.0f;
        }
    }

    /**
     * Gets the fluid model backing this fluid manager
     * @return The fluid model
     */
    public FluidModel getModel() {
        return model;
    }
    
    /**
     * Gets the key for a given world position
     * @param worldX The x component
     * @param worldY The y component
     * @param worldZ The z component
     * @return The key
     */
    public String getKey(int worldX, int worldY, int worldZ){
        return worldX + "_" + worldY + "_" + worldZ;
    }
    
    /**
     * Gets a server fluid chunk
     * @param worldX The world x position
     * @param worldY The world y position
     * @param worldZ The world z position
     * @return The ServerFluidChunk
     */
    public ServerFluidChunk getChunk(int worldX, int worldY, int worldZ){
        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
        String key = getKey(worldX,worldY,worldZ);
        ServerFluidChunk returnedChunk = null;
        if(chunkCache.containsKey(key)){
            chunkCacheContents.remove(key);
            chunkCacheContents.add(0, key);
            returnedChunk = chunkCache.get(key);
            return returnedChunk;
        } else {
            if(chunkCacheContents.size() > cacheSize){
                String oldChunk = chunkCacheContents.remove(chunkCacheContents.size() - 1);
                chunkCache.remove(oldChunk);
            }
            //pull from disk if it exists
            if(chunkDiskMap != null){
                if(chunkDiskMap.containsFluidAtPosition(worldX, worldY, worldZ)){
                    returnedChunk = chunkDiskMap.getFluidChunk(worldX, worldY, worldZ);
                }
            }
            //generate if it does not exist
            if(returnedChunk == null){
                returnedChunk = chunkGenerator.generateChunk(worldX, worldY, worldZ);
            }
            chunkCache.put(key, returnedChunk);
            chunkCacheContents.add(key);
            return returnedChunk;
        }
    }

    /**
     * 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){
        chunkDiskMap.saveToDisk(getChunk(position.x, position.y, position.z));
    }
    
    /**
     * Applies a deform to fluid 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 weight The weight to set it to
     * @param value The value to set it to
     */
    public void deformFluidAtLocationToValue(Vector3i worldPos, Vector3i voxelPos, float weight, int value){
        // TerrainModification modification = new TerrainModification(worldPos,voxelPos,weight,value);
        // //could be null if, for instance, arena mode
        // if(model != null){
        //     model.addModification(modification);
        // }
        // String key = getKey(worldPos.x,worldPos.y,worldPos.z);
        // if(chunkCache.containsKey(key)){
        //     ServerFluidChunk chunk = chunkCache.get(key);
        //     chunk.addModification(modification);
        // }
    }

    /**
     * Simulates a chunk
     */
    public boolean simulate(int worldX, int worldY, int worldZ){
        if(simulate){
            ServerFluidChunk fluidChunk = this.getChunk(worldX, worldY, worldZ);
            ServerTerrainChunk terrainChunk = this.serverTerrainManager.getChunk(worldX, worldY, worldZ);
            if(fluidChunk != null && terrainChunk != null && this.serverFluidSimulator != null){
                return this.serverFluidSimulator.simulate(fluidChunk,terrainChunk,worldX,worldY,worldZ);
            }
        }
        return false;
    }

    //getter for simulate
    public boolean getSimulate(){
        return simulate;
    }

    //setter for simulate
    public void setSimulate(boolean simulate){
        this.simulate = simulate;
    }

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

    
}
