package electrosphere.server.terrain.manager;

import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.scene.RealmDescriptor;
import electrosphere.game.server.world.ServerWorldData;
import electrosphere.server.terrain.diskmap.ChunkDiskMap;
import electrosphere.server.terrain.generation.TestGenerationChunkGenerator;
import electrosphere.server.terrain.generation.interfaces.ChunkGenerator;
import electrosphere.server.terrain.generation.macro.DefaultMacroGenerator;
import electrosphere.server.terrain.generation.macro.HomogenousMacroGenerator;
import electrosphere.server.terrain.generation.macro.MacroGenerator;
import electrosphere.server.terrain.models.TerrainModel;
import electrosphere.server.terrain.models.TerrainModification;
import electrosphere.util.FileUtils;
import electrosphere.util.annotation.Exclude;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
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 terrain
 */
public class ServerTerrainManager {

    /**
     * The number of threads for chunk generation
     */
    public static final int GENERATION_THREAD_POOL_SIZE = 2;

    /**
     * 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;
    
    /**
     * The cache of chunks
     */
    @Exclude
    ServerChunkCache chunkCache = new ServerChunkCache();

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

    //The generation algorithm for this terrain manager
    @Exclude
    ChunkGenerator chunkGenerator;

    /**
     * The threadpool for chunk generation
     */
    @Exclude
    static final ExecutorService chunkExecutorService = Executors.newFixedThreadPool(GENERATION_THREAD_POOL_SIZE);    
    
    /**
     * Constructor
     */
    public ServerTerrainManager(
        ServerWorldData parent,
        long seed,
        ChunkGenerator chunkGenerator
    ){
        this.parent = parent;
        this.seed = seed;
        this.chunkGenerator = chunkGenerator;
    }
    
    ServerTerrainManager(){
        
    }
    
    /**
     * Generates a terrain model for the manager
     */
    public void generate(){
        this.model = TerrainModel.create(this.seed);
        DefaultMacroGenerator generator = new DefaultMacroGenerator();
        generator.generate(this.model);
        this.chunkGenerator.setModel(this.model);
        this.chunkDiskMap = ChunkDiskMap.init();
    }

    /**
     * Generates a terrain model for the manager
     * @param realmDescriptor The realm description to draw parameters from while generating
     */
    public void generate(RealmDescriptor realmDescriptor){
        this.model = TerrainModel.create(this.seed);
        MacroGenerator generator = new DefaultMacroGenerator();
        if(realmDescriptor.getWorldType().equals(RealmDescriptor.PROCEDURAL_TYPE_HOMOGENOUS)){
            generator = new HomogenousMacroGenerator(realmDescriptor.getBiomeType());
        }
        generator.generate(this.model);
        this.chunkGenerator.setModel(this.model);
        this.chunkDiskMap = ChunkDiskMap.init();
    }
    
    /**
     * 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){
            if(model.getDiscreteArrayDimension() < 1){
                throw new Error("Invalid terrain macro data size!");
            }

            //save the model itself
            FileUtils.serializeObjectToSavePath(saveName, "./terrain.json", model);

            //save the elevation array
            ByteBuffer floatBuff = ByteBuffer.allocate(model.getDiscreteArrayDimension() * model.getDiscreteArrayDimension() * 4);
            FloatBuffer floatView = floatBuff.asFloatBuffer();
            for(int x = 0; x < model.getDiscreteArrayDimension(); x++){
                for(int y = 0; y < model.getDiscreteArrayDimension(); y++){
                    floatView.put(model.getElevation()[x][y]);
                }
            }
            if(floatView.position() > 0){
                floatView.flip();
            }
            FileUtils.saveBinaryToSavePath(saveName, "./terrain.dat", floatBuff.array());
            
            //save the biome array
            ByteBuffer shortBuff = ByteBuffer.allocate(model.getDiscreteArrayDimension() * model.getDiscreteArrayDimension() * 2);
            ShortBuffer shortView = shortBuff.asShortBuffer();
            for(int x = 0; x < model.getDiscreteArrayDimension(); x++){
                for(int y = 0; y < model.getDiscreteArrayDimension(); y++){
                    shortView.put(model.getBiome()[x][y]);
                }
            }
            if(shortView.position() > 0){
                shortView.flip();
            }
            FileUtils.saveBinaryToSavePath(saveName, "./biome.dat", shortBuff.array());
        }
        //for each chunk, save via disk map
        for(ServerTerrainChunk chunk : this.chunkCache.getContents()){
            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()){

            //read the model itself
            model = FileUtils.loadObjectFromSavePath(saveName, "./terrain.json", TerrainModel.class);
            chunkGenerator.setModel(model);
            if(model.getDiscreteArrayDimension() < 1){
                throw new Error("Invalid terrain macro data size!");
            }

            //read the elevation data
            float[][] elevation = new float[model.getDiscreteArrayDimension()][model.getDiscreteArrayDimension()];
            if(FileUtils.checkSavePathExists(saveName, "./terrain.dat")){
                byte[] data = FileUtils.loadBinaryFromSavePath(saveName, "./terrain.dat");
                ByteBuffer buffer = ByteBuffer.wrap(data);
                FloatBuffer floatView = buffer.asFloatBuffer();
                for(int x = 0; x < model.getDiscreteArrayDimension(); x++){
                    for(int y = 0; y < model.getDiscreteArrayDimension(); y++){
                        elevation[x][y] = floatView.get();
                    }
                }
            }
            model.setElevationArray(elevation);

            //read the biome data
            short[][] biome = new short[model.getDiscreteArrayDimension()][model.getDiscreteArrayDimension()];
            if(FileUtils.checkSavePathExists(saveName, "./biome.dat")){
                byte[] data = FileUtils.loadBinaryFromSavePath(saveName, "./biome.dat");
                ByteBuffer buffer = ByteBuffer.wrap(data);
                ShortBuffer shortView = buffer.asShortBuffer();
                for(int x = 0; x < model.getDiscreteArrayDimension(); x++){
                    for(int y = 0; y < model.getDiscreteArrayDimension(); y++){
                        biome[x][y] = shortView.get();
                    }
                }
            }
            model.setBiome(biome);
        }
        //load chunk disk map
        chunkDiskMap = ChunkDiskMap.init(saveName);
    }

    /**
     * Generates a test terrain model
     * @param chunkGen The chunk generator
     */
    public void genTestData(TestGenerationChunkGenerator chunkGen){
        this.model = TerrainModel.generateTestModel();
        chunkGen.setModel(model);
    }

    /**
     * Evicts all cached terrain
     */
    public void evictAll(){
        this.chunkCache.clear();
    }
    
    public float getDiscreteValue(int x, int y){
        if(model != null){
            return model.getElevation()[x][y];
        } else {
            return 0;
        }
    }

    /**
     * Gets the terrain model backing this terrain manager
     * @return The terrain model
     */
    public TerrainModel getModel() {
        return model;
    }

    /**
     * Overrides the chunk generator
     * @param generator The new chunk generator
     */
    public void overrideChunkGenerator(ChunkGenerator generator){
        this.chunkGenerator = generator;
    }
    
    /**
     * 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 ServerTerrainChunk
     */
    public ServerTerrainChunk getChunk(int worldX, int worldY, int worldZ){
        Globals.profiler.beginAggregateCpuSample("ServerTerrainManager.getChunk");
        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
        ServerTerrainChunk returnedChunk = null;
        if(chunkCache.containsChunk(worldX,worldY,worldZ,ChunkData.NO_STRIDE)){
            returnedChunk = chunkCache.get(worldX,worldY,worldZ, ChunkData.NO_STRIDE);
        } else {
            //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, ChunkData.NO_STRIDE);
            }
            this.chunkCache.add(worldX, worldY, worldZ, ChunkData.NO_STRIDE, returnedChunk);
        }
        Globals.profiler.endCpuSample();
        return returnedChunk;
    }

    /**
     * Performs logic once a server chunk is available
     * @param worldX The world x position
     * @param worldZ The world z position
     * @param chunkX The chunk x position
     * @param chunkZ THe chunk z position
     * @return The ServerTerrainChunk
     */
    public double getElevation(int worldX, int worldZ, int chunkX, int chunkZ){
        Globals.profiler.beginAggregateCpuSample("ServerTerrainManager.getChunk");
        double elevation = chunkGenerator.getElevation(worldX, worldZ, chunkX, chunkZ);
        Globals.profiler.endCpuSample();
        return elevation;
    }

    /**
     * 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<ServerTerrainChunk> onLoad){
        Globals.profiler.beginAggregateCpuSample("ServerTerrainManager.getChunkAsync");
        chunkExecutorService.submit(new ChunkGenerationThread(chunkDiskMap, chunkCache, chunkGenerator, 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 && chunkCache.containsChunk(position.x, position.y, position.z, ChunkData.NO_STRIDE)){
            chunkDiskMap.saveToDisk(this.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);
        if(chunkCache.containsChunk(worldPos.x,worldPos.y,worldPos.z,ChunkData.NO_STRIDE)){
            ServerTerrainChunk chunk = chunkCache.get(worldPos.x,worldPos.y,worldPos.z, ChunkData.NO_STRIDE);
            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;
    }

    /**
     * Gets the chunk generator of the terrain manager
     * @return The chunk generator
     */
    public ChunkGenerator getChunkGenerator(){
        return chunkGenerator;
    }
    
    /**
     * Closes the generation threadpool
     */
    public void closeThreads(){
        chunkExecutorService.shutdownNow();
    }

}
