package electrosphere.game.server.terrain.manager;

import com.google.gson.Gson;
import electrosphere.game.terrain.processing.TerrainInterpolator;
import electrosphere.main.Globals;
import electrosphere.game.server.terrain.generation.TerrainGen;
import electrosphere.game.server.terrain.models.ModificationList;
import electrosphere.game.server.terrain.models.TerrainModel;
import electrosphere.game.server.terrain.models.TerrainModification;
import electrosphere.util.FileUtils;
import electrosphere.util.Utilities;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;

/**
 *
 * @author satellite
 */
public class ServerTerrainManager {
    
    
    //The size of the world in discrete units * must be multiple of 200
    int worldSizeDiscrete;
    
    //The vertical multiplier applied to the statically generated terrain
    int verticalInterpolationRatio;
    
    int dynamicInterpolationRatio;
    
    float interpolationRandomDampener;
    
    long seed;
    
    TerrainModel model;
    
    
    //Basic idea is we associate string that contains chunk x&y 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 = 50;
    HashMap<String, ServerTerrainChunk> elevationMapCache;
    ArrayList<String> elevationMapCacheContents;
    
    
    
    public ServerTerrainManager(int worldSizeDiscrete, int verticalInterpolationRatio, int dynamicInterpolationRatio, float interpolationRandomDampener, long seed){
        this.worldSizeDiscrete = worldSizeDiscrete;
        this.verticalInterpolationRatio = verticalInterpolationRatio;
        this.dynamicInterpolationRatio = dynamicInterpolationRatio;
        this.elevationMapCache = new HashMap();
        this.elevationMapCacheContents = new ArrayList();
        this.interpolationRandomDampener = interpolationRandomDampener;
        this.seed = seed;
    }
    
    ServerTerrainManager(){
        
    }
    
    public static ServerTerrainManager constructArenaTerrainManager(){
        ServerTerrainManager rVal = new ServerTerrainManager();
        rVal.worldSizeDiscrete = 2;
        rVal.verticalInterpolationRatio = 0;
        rVal.dynamicInterpolationRatio = 100;
        rVal.elevationMapCache = new HashMap();
        rVal.elevationMapCacheContents = new ArrayList();
        rVal.interpolationRandomDampener = 0.0f;
        return rVal;
    }
    
    public void generate(){
        TerrainGen terrainGen = new TerrainGen();
        terrainGen.setInterpolationRatio(worldSizeDiscrete/200);
        terrainGen.setVerticalInterpolationRatio(verticalInterpolationRatio);
        terrainGen.setDynamicInterpolationRatio(dynamicInterpolationRatio);
        terrainGen.setRandomSeed(seed);
        model = terrainGen.generateModel();
        model.setInterpolationRandomDampener(interpolationRandomDampener);
    }
    
    public void save(){
        Gson gson = new Gson();
        String terrainOutRaw = gson.toJson(model);
        try {
            Files.write(new File("./terrain.json").toPath(), terrainOutRaw.getBytes());
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    public void load(String saveName){
        model = FileUtils.loadObjectFromSavePath(saveName, "./terrain.json", TerrainModel.class);
    }
    
    public float[][] getTerrainAtChunk(int x, int y){
        return model.getElevationForChunk(x, y);
    }
    
//    public ServerTerrainChunk getAugmentedTerrainAtChunk(int x, int y){
//        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
//        if(model != null){
//            String key = getKey(x,y);
//            ServerTerrainChunk returnedChunk;
//            if(elevationMapCache.containsKey(key)){
//                elevationMapCacheContents.remove(key);
//                elevationMapCacheContents.add(0, key);
//                returnedChunk = elevationMapCache.get(key);
//                return returnedChunk;
//            } else {
//                float[][] macroValues = model.getRad5MacroValuesAtPosition(x, y);
//                long[][] randomizer = model.getRad5RandomizerValuesAtPosition(x, y);
//                float[][] heightmap = TerrainInterpolator.getBicubicInterpolatedChunk(
//                        macroValues,
//                        randomizer,
//                        model.getDynamicInterpolationRatio(),
//                        model.getRandomDampener()
//                );
//                ModificationList modificationList = model.getModifications(x, y);
//                for(TerrainModification modification : modificationList.getModifications()){
//                    modification.applyToHeightfield(heightmap);
//                }
//                if(elevationMapCacheContents.size() > cacheSize){
//                    String oldChunk = elevationMapCacheContents.remove(elevationMapCacheContents.size() - 1);
//                    elevationMapCache.remove(oldChunk);
//                }
//                returnedChunk = new ServerTerrainChunk(x, y, heightmap, macroValues, randomizer);
//                elevationMapCache.put(key, returnedChunk);
//                elevationMapCacheContents.add(key);
//                return returnedChunk;
//            }
//        } else {
//            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
//            return ServerTerrainChunk.getArenaChunk(dynamicInterpolationRatio + 1);
//        }
//    }
    
    public double getHeightAtPosition(double x, double y){
        //get chunk coordinate space of input x,y
        int chunkX = (int)Math.floor(x / dynamicInterpolationRatio);
        int chunkY = (int)Math.floor(y / dynamicInterpolationRatio);
        //get local coordinate space of input x,y
        double localX = x - chunkX * dynamicInterpolationRatio;
        double localY = y - chunkY * dynamicInterpolationRatio;
        //get chunk elevation map
        float[][] chunkElevationMap = getChunk(chunkX,chunkY).heightMap;
        //floored variants of local values
        int localXf = (int)Math.floor(localX);
        int localYf = (int)Math.floor(localY);
        
        /*
        Average some inner value.
        
        01            11
           0.3 0.4 0.5
           0.1 0.2 0.3
        00            10
        */
        //interp elevation from map
        float elevation00 = chunkElevationMap[(int)localX+0][(int)localY+0];
        float elevation10 = chunkElevationMap[(int)localX+1][(int)localY+0];
        float elevation01 = chunkElevationMap[(int)localX+0][(int)localY+1];
        float elevation11 = chunkElevationMap[(int)localX+1][(int)localY+1];
        
        double rVal = 
                (1-(localX-localXf))*(1-(localY-localYf)) * elevation00 +
                (  (localX-localXf))*(1-(localY-localYf)) * elevation10 +
                (1-(localX-localXf))*(  (localY-localYf)) * elevation01 +
                (  (localX-localXf))*(  (localY-localYf)) * elevation11
                ;
        
        return rVal;
    }
        
    public int getChunkWidth(){
        return dynamicInterpolationRatio;
    }
    
    public int getAugmentedChunkWidth(){
        return dynamicInterpolationRatio + 1;
    }
    
    public int getWorldDiscreteSize(){
        return worldSizeDiscrete;
    }
    
    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;
        }
    }
    
//    public float[][] getMacroValues(int x, int y){
//        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
//        if(model != null){
//            return model.getMacroValuesAtPosition(x, y);
//        } else {
//            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
//            return new float[3][3];
//        }
//    }
    
//    public float[][] getRad5MacroValues(int x, int y){
//        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
//        if(model != null){
//            return model.getRad5MacroValuesAtPosition(x, y);
//        } else {
//            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
//            return new float[5][5];
//        }
//    }
    
//    public long[][] getRandomizer(int x, int y){
//        //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
//        if(model != null){
//            return model.getRad5RandomizerValuesAtPosition(x, y);
//        } else {
//            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
//            return new long[5][5];
//        }
//    }

    public TerrainModel getModel() {
        return model;
    }
    
    public String getKey(int x, int y){
        return x + "-" + y;
    }
    
    public ServerTerrainChunk getChunk(int x, int y){
        if(model != null){
            //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING
            String key = getKey(x,y);
            ServerTerrainChunk returnedChunk;
            if(elevationMapCache.containsKey(key)){
                elevationMapCacheContents.remove(key);
                elevationMapCacheContents.add(0, key);
                returnedChunk = elevationMapCache.get(key);
                return returnedChunk;
            } else {
                float[][] macroValues = model.getRad5MacroValuesAtPosition(x, y);
                long[][] randomizer = model.getRad5RandomizerValuesAtPosition(x, y);
                float[][] heightmap = TerrainInterpolator.getBicubicInterpolatedChunk(
                        macroValues,
                        randomizer,
                        model.getDynamicInterpolationRatio(),
                        model.getRandomDampener()
                );
                ModificationList modificationList = model.getModifications(x, y);
                if(modificationList != null){
                    for(TerrainModification modification : modificationList.getModifications()){
                       heightmap = modification.applyToHeightfield(heightmap);
                    }
                }
                if(elevationMapCacheContents.size() > cacheSize){
                    String oldChunk = elevationMapCacheContents.remove(elevationMapCacheContents.size() - 1);
                    elevationMapCache.remove(oldChunk);
                }
                returnedChunk = new ServerTerrainChunk(x, y, heightmap, macroValues, randomizer);
                elevationMapCache.put(key, returnedChunk);
                elevationMapCacheContents.add(key);
                return returnedChunk;
            }
        } else {
            //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING
            String key = getKey(x,y);
            ServerTerrainChunk returnedChunk;
            if(elevationMapCache.containsKey(key)){
                elevationMapCacheContents.remove(key);
                elevationMapCacheContents.add(0, key);
                returnedChunk = elevationMapCache.get(key);
                return returnedChunk;
            } else {
                returnedChunk = ServerTerrainChunk.getArenaChunk(dynamicInterpolationRatio + 1, x, y);
                elevationMapCache.put(key, returnedChunk);
                elevationMapCacheContents.add(key);
                return returnedChunk;
            }
        }
    }
    
    public void deformTerrainAtLocationToValue(int worldX, int worldY, int locationX, int locationY, float value){
//        System.out.println("Add modification at " + worldX + "," + worldY + " subloc " + locationX + "," + locationY);
        TerrainModification modification = new TerrainModification(worldX,worldY,locationX,locationY,value);
        model.addModification(modification);
        String key = getKey(worldX,worldY);
        if(elevationMapCache.containsKey(key)){
            ServerTerrainChunk chunk = elevationMapCache.get(key);
            chunk.addModification(modification);
            chunk.heightMap = modification.applyToHeightfield(chunk.heightMap);
        }
    }
    
    public long getRandomizerAtPoint(int worldX, int worldY){
        return model.getRad5RandomizerValuesAtPosition(worldX, worldY)[2][2];
    }
    
}
