package electrosphere.game.server.world;

import electrosphere.client.block.BlockChunkData;
import electrosphere.server.block.manager.ServerBlockManager;
import electrosphere.server.fluid.generation.DefaultFluidGenerator;
import electrosphere.server.fluid.manager.ServerFluidManager;
import electrosphere.server.terrain.generation.DefaultChunkGenerator;
import electrosphere.server.terrain.generation.TestGenerationChunkGenerator;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
import electrosphere.server.terrain.manager.ServerTerrainManager;
import electrosphere.server.terrain.models.TerrainModel;
import electrosphere.util.FileUtils;

import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector3i;

/**
 * Server data about the world
 */
public class ServerWorldData {
    
    /**
     * The size of the procedural world
     */
    public static final int PROCEDURAL_WORLD_SIZE = TerrainModel.MAX_WORLD_SIZE_DISCRETE;

    public static enum WorldType {
        GAME_WORLD,
        ARENA_WORLD,
        LEVEL,
    }
    
    WorldType type;
    
    
    /*
    
                       world max
    +---------------------+
    |                     |
    |                     |
    |                     |
    |                     |
    |                     |
    +---------------------+
 world min
    
    
    basically we're saying what the maximum and minimum x and z something can occupy are
    
    FOR THE TIME BEING DOES NOT ACCOUNT FOR Y
    */
    Vector3f worldMinPoint;
    Vector3f worldMaxPoint;
    
    int worldSizeDiscrete;
    int worldSizeDiscreteVertical;
    int dynamicInterpolationRatio;
    float randomDampener;
    boolean isArena = false;

    
    //terrain data
    private ServerTerrainManager serverTerrainManager;

    //fluid data
    private ServerFluidManager serverFluidManager;

    /**
     * The block manager
     */
    private ServerBlockManager serverBlockManager;
    

    /**
     * Creates a server world data object based on a discrete world size
     * @param discreteWorldSize The discrete world size
     * @return The server world data object
     */
    public static ServerWorldData createGriddedRealmWorldData(int discreteWorldSize){
        ServerWorldData rVal = new ServerWorldData();
        rVal.type = WorldType.LEVEL;

        //min and max real points
        rVal.worldMinPoint = new Vector3f(0,0,0);
        int worldDim = discreteWorldSize * ServerTerrainChunk.CHUNK_DIMENSION;
        rVal.worldMaxPoint = new Vector3f(worldDim,worldDim, worldDim);

        //misc values
        rVal.dynamicInterpolationRatio = 1;
        rVal.worldSizeDiscrete = discreteWorldSize;
        rVal.worldSizeDiscreteVertical = discreteWorldSize;
        rVal.randomDampener = ServerTerrainManager.SERVER_TERRAIN_MANAGER_DAMPENER;

        return rVal;
    }

    /**
     * Creates a server world data object with a fixed size
     * @param minPoint The minimum point of the world
     * @param maxPoint The maximum point of the world
     * @return The server world data object
     */
    public static ServerWorldData createFixedWorldData(Vector3d minPoint, Vector3d maxPoint){
        ServerWorldData rVal = new ServerWorldData();
        rVal.type = WorldType.LEVEL;

        //min and max real points
        rVal.worldMinPoint = new Vector3f((float)minPoint.x,(float)minPoint.y,(float)minPoint.z);
        rVal.worldMaxPoint = new Vector3f((float)maxPoint.x,(float)maxPoint.y,(float)maxPoint.z);

        //misc values
        rVal.dynamicInterpolationRatio = 1;
        rVal.worldSizeDiscrete = 1;
        rVal.worldSizeDiscreteVertical = 1;
        rVal.randomDampener = ServerTerrainManager.SERVER_TERRAIN_MANAGER_DAMPENER;

        return rVal;
    }

    /**
     * Loads world data from a scene or a save
     * @param sceneOrSaveName The name of the scene or save
     * @param isScene true if loading from a scene, false if loading from a save
     * @return The server world data
     */
    public static ServerWorldData loadWorldData(String sceneOrSaveName, boolean isScene){
        //
        //Read world data if it exists
        //
        ServerWorldData serverWorldData = null;
        ServerTerrainManager serverTerrainManager = null;
        ServerFluidManager serverFluidManager = null;
        ServerBlockManager serverBlockManager = null;
        if(isScene){
            serverWorldData = FileUtils.loadObjectFromSavePath(sceneOrSaveName, "world.json", ServerWorldData.class);
            serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, new DefaultChunkGenerator());
            serverTerrainManager.load(sceneOrSaveName);
            serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator());
            serverBlockManager = new ServerBlockManager(serverWorldData);
        } else {
            //TODO: Allow loading procedurally generated terrain from disk (the chunk generator is always default currently)
            serverWorldData = FileUtils.loadObjectFromSavePath(sceneOrSaveName, "world.json", ServerWorldData.class);
            serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, new TestGenerationChunkGenerator(serverWorldData, false));
            serverTerrainManager.load(sceneOrSaveName);
            serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator());
            serverBlockManager = new ServerBlockManager(serverWorldData);
        }
        serverWorldData.setManagers(serverTerrainManager, serverFluidManager, serverBlockManager);
        return serverWorldData;
    }

    /**
     * Creates world data for testing generation
     * @return The server world data
     */
    public static ServerWorldData createGenerationTestWorldData(){
        //
        //Read world data if it exists
        //
        ServerWorldData serverWorldData = null;
        ServerTerrainManager serverTerrainManager = null;
        ServerFluidManager serverFluidManager = null;
        ServerBlockManager serverBlockManager = null;
        //TODO: Allow loading procedurally generated terrain from disk (the chunk generator is always default currently)
        serverWorldData = ServerWorldData.createFixedWorldData(new Vector3d(0),new Vector3d(TestGenerationChunkGenerator.GENERATOR_REALM_SIZE * ServerTerrainChunk.CHUNK_DIMENSION));
        serverWorldData.worldSizeDiscrete = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE;
        serverWorldData.worldSizeDiscreteVertical = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE;

        //test terrain gen
        {
            TestGenerationChunkGenerator chunkGen = new TestGenerationChunkGenerator(serverWorldData, TestGenerationChunkGenerator.DEFAULT_USE_JAVASCRIPT);
            serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, chunkGen);
            serverTerrainManager.genTestData(chunkGen);
        }
        serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator());
        serverBlockManager = new ServerBlockManager(serverWorldData);
        serverWorldData.setManagers(serverTerrainManager, serverFluidManager, serverBlockManager);
        return serverWorldData;
    }
    
    
    public Vector3f getWorldBoundMin(){
        return worldMinPoint;
    }
    
    public Vector3f getWorldBoundMax(){
        return worldMaxPoint;
    }

    /**
     * Gets the discrete size of the world (in chunks)
     * @return The discrete size of the world (in chunks)
     */
    public int getWorldSizeDiscrete() {
        return worldSizeDiscrete;
    }

    public int getDynamicInterpolationRatio() {
        return dynamicInterpolationRatio;
    }

    public int convertRealToChunkSpace(double real){
        return (int)Math.floor(real / ServerTerrainChunk.CHUNK_DIMENSION);
    }

    public float convertChunkToRealSpace(int chunk){
        return chunk * ServerTerrainChunk.CHUNK_DIMENSION;
    }

    /**
     * Converts a real position to a local block grid position
     * @param real The real position
     * @return The local block grid position
     */
    public int convertRealToLocalBlockSpace(double real){
        return (int)Math.floor(real * BlockChunkData.BLOCKS_PER_UNIT_DISTANCE % BlockChunkData.CHUNK_DATA_WIDTH);
    }

    /**
     * Converts a chunk space coordinate to a real space coordinate
     * @param chunk The position within the chunk
     * @param worldPos The world pos of the chunk
     * @return The real pos
     */
    public double convertVoxelToRealSpace(int chunk, int worldPos){
        return chunk + this.convertWorldToReal(worldPos);
    }
    
    public double getRelativeLocation(double real, int world){
        return real - (world * dynamicInterpolationRatio);
    }

    public boolean isArena() {
        return isArena;
    }
    

    public int convertRealToWorld(double real){
        return convertRealToChunkSpace(real);
    }

    public double convertWorldToReal(int world){
        return convertChunkToRealSpace(world);
    }

    public Vector3i convertRealToChunkSpace(Vector3d position){
        return new Vector3i(
            convertRealToChunkSpace(position.x),
            convertRealToChunkSpace(position.y),
            convertRealToChunkSpace(position.z)
        );
    }

    /**
     * Converts a world space vector to a real space vector
     * @param position The world space vector
     * @return The real space vector
     */
    public Vector3d convertWorldToRealSpace(Vector3i position){
        return new Vector3d(
            convertWorldToReal(position.x),
            convertWorldToReal(position.y),
            convertWorldToReal(position.z)
        );
    }

    public Vector3i convertRealToVoxelSpace(Vector3d position){
        return new Vector3i(
            (int)Math.floor(position.x - convertChunkToRealSpace(convertRealToChunkSpace(position.x))),
            (int)Math.floor(position.y - convertChunkToRealSpace(convertRealToChunkSpace(position.y))),
            (int)Math.floor(position.z - convertChunkToRealSpace(convertRealToChunkSpace(position.z)))
        );
    }

    /**
     * Converts a real coordinate to a world space coordinate
     * @param position The real coordinate
     * @return The world space coordinate
     */
    public Vector3i convertRealToWorldSpace(Vector3d position){
        return new Vector3i(
            convertRealToChunkSpace(position.x),
            convertRealToChunkSpace(position.y),
            convertRealToChunkSpace(position.z)
        );
    }

    /**
     * Converts a real coordinate to a local block grid space coordinate
     * @param position The real coordinate
     * @return The local block grid space coordinate
     */
    public Vector3i convertRealToLocalBlockSpace(Vector3d position){
        return new Vector3i(
            convertRealToLocalBlockSpace(position.x),
            convertRealToLocalBlockSpace(position.y),
            convertRealToLocalBlockSpace(position.z)
        );
    }

    /**
     * Converts a world coordinate to a macro scale coordinate
     * @param worldPos The world position
     * @return The macro scale position
     */
    public Vector3i convertWorldToMacroScale(Vector3i worldPos){
        return new Vector3i(
            worldPos.x / this.serverTerrainManager.getModel().getMacroDataScale(),
            worldPos.y / this.serverTerrainManager.getModel().getMacroDataScale(),
            worldPos.z / this.serverTerrainManager.getModel().getMacroDataScale()
        );
    }

    /**
     * Converts a world coordinate to a macro scale coordinate
     * @param worldPos The world position
     * @return The macro scale position
     */
    public int convertWorldToMacroScale(int worldPos){
        return worldPos / this.serverTerrainManager.getModel().getMacroDataScale();
    }

    /**
     * Clamps the world position to the floored macro value in world pos
     * @param worldPos The world position
     * @return The floor macro value in world pos
     */
    public int clampWorldToMacro(int worldPos){
        return (worldPos / this.serverTerrainManager.getModel().getMacroDataScale()) * this.serverTerrainManager.getModel().getMacroDataScale();
    }
    
    /**
     * Gets the terrain manager for this world
     * @return The terrain manager if it exists, null otherwise
     */
    public ServerTerrainManager getServerTerrainManager(){
        return this.serverTerrainManager;
    }

    /**
     * Gets the fluid manager for this world
     * @return The fluid manager if it exists, null otherwise
     */
    public ServerFluidManager getServerFluidManager(){
        return this.serverFluidManager;
    }

    /**
     * Gets the block manager for this world
     * @return The block manager if it exists, null otherwise
     */
    public ServerBlockManager getServerBlockManager(){
        return this.serverBlockManager;
    }

    /**
     * Sets the chunk managers
     * @param serverTerrainManager The terrain manager
     * @param serverFluidManager The fluid manager
     * @param serverBlockManager The server block manager
     */
    public void setManagers(ServerTerrainManager serverTerrainManager, ServerFluidManager serverFluidManager, ServerBlockManager serverBlockManager){
        this.serverTerrainManager = serverTerrainManager;
        this.serverFluidManager = serverFluidManager;
        this.serverBlockManager = serverBlockManager;
        this.serverTerrainManager.setParent(this);
        this.serverFluidManager.setParent(this);
        if(this.serverTerrainManager == null || this.serverFluidManager == null){
            throw new IllegalStateException("Setting world data managers to a null manager " + this.serverTerrainManager + " " + this.serverFluidManager);
        }
    }

}
