package electrosphere.server.terrain.manager;

import electrosphere.game.server.world.ServerWorldData;
import electrosphere.server.terrain.diskmap.ChunkDiskMap;
import electrosphere.server.terrain.generation.continentphase.TerrainGenerator;
import electrosphere.server.terrain.generation.interfaces.ChunkGenerator;
import electrosphere.server.terrain.models.TerrainModel;
import electrosphere.server.terrain.models.TerrainModification;
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 terrain
 */
public class ServerTerrainManager {

    /**
     * Full world discrete size
     */
    public static final int WORLD_SIZE_DISCRETE = 1000;

    /**
     * Vertical interp ratio
     */
    public static final int VERTICAL_INTERPOLATION_RATIO = 50;
    
    //the interpolation width of a server terrain manager is hard coded at the moment to make sure it's divisible by the sub chunk calculations
    public static final int SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO = 128;

    /**
     * The dampening ratio for ground noise
     */
    public static final float SERVER_TERRAIN_MANAGER_DAMPENER = 1.0f;
    
    /**
     * The parent world data
     */
    ServerWorldData parent;

    //the seed for terrain generation
    long seed;
    
    //The model of the terrain this manager is managing
    TerrainModel 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, ServerTerrainChunk> chunkCache;
    List<String> chunkCacheContents;

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

    //The generation algorithm for this terrain manager
    ChunkGenerator chunkGenerator;
    
    
    /**
     * Constructor
     */
    public ServerTerrainManager(
        ServerWorldData parent,
        long seed,
        ChunkGenerator chunkGenerator
    ){
        this.parent = parent;
        this.chunkCache = new ConcurrentHashMap<String, ServerTerrainChunk>();
        this.chunkCacheContents = new CopyOnWriteArrayList<String>();
        this.seed = seed;
        this.chunkGenerator = chunkGenerator;
    }
    
    ServerTerrainManager(){
        
    }
    
    /**
     * Generates a terrain model for the manager
     */
    public void generate(){
        TerrainGenerator terrainGen = new TerrainGenerator();
        terrainGen.setInterpolationRatio(parent.getWorldSizeDiscrete()/200);
        terrainGen.setVerticalInterpolationRatio(parent.getWorldSizeDiscrete());
        terrainGen.setRandomSeed(seed);
        model = terrainGen.generateModel();
        this.chunkGenerator.setModel(model);
        model.setInterpolationRandomDampener(SERVER_TERRAIN_MANAGER_DAMPENER);
        this.chunkDiskMap = new ChunkDiskMap();
    }
    
    /**
     * Saves the terrain 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.serializeObjectToSavePath(saveName, "./terrain.json", model);
            FileUtils.saveBinaryToSavePath(saveName, "./terrain.dat", buffer.array());
        }
        //for each chunk, save via disk map
        for(String chunkKey : chunkCacheContents){
            ServerTerrainChunk chunk = chunkCache.get(chunkKey);
            chunkDiskMap.saveToDisk(chunk);
        }
        //save disk map itself
        if(chunkDiskMap != null){
            chunkDiskMap.save();
        }
    }
    
    /**
     * Loads a terrain manager from a save file
     * @param saveName The name of the save
     */
    public void load(String saveName){
        //load terrain model
        if(FileUtils.getSaveFile(saveName, "./terrain.json").exists()){
            model = FileUtils.loadObjectFromSavePath(saveName, "./terrain.json", TerrainModel.class);
            chunkGenerator.setModel(model);
            byte[] data = FileUtils.loadBinaryFromSavePath(saveName, "./terrain.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 ChunkDiskMap();
        chunkDiskMap.init(saveName);
    }
    
    public float[][] getTerrainAtChunk(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 terrain model backing this terrain manager
     * @return The terrain model
     */
    public TerrainModel 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 terrain chunk
     * @param worldX The world x position
     * @param worldY The world y position
     * @param worldZ The world z position
     * @return The ServerTerrainChunk
     */
    public ServerTerrainChunk getChunk(int worldX, int worldY, int worldZ){
        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
        String key = getKey(worldX,worldY,worldZ);
        ServerTerrainChunk 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.containsTerrainAtPosition(worldX, worldY, worldZ)){
                    returnedChunk = chunkDiskMap.getTerrainChunk(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){
        if(chunkDiskMap != null){
            chunkDiskMap.saveToDisk(getChunk(position.x, position.y, position.z));
        }
    }
    
    /**
     * Applies a deform to terrain 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 deformTerrainAtLocationToValue(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)){
            ServerTerrainChunk chunk = chunkCache.get(key);
            chunk.addModification(modification);
        }
    }

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