package electrosphere.client.terrain.manager;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import org.joml.Vector3i;

import electrosphere.client.scene.ClientWorldData;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.client.terrain.cache.ClientTerrainCache;
import electrosphere.client.terrain.cells.VoxelTextureAtlas;
import electrosphere.engine.Globals;
import electrosphere.entity.types.terrain.TerrainChunkData;
import electrosphere.logger.LoggerInterface;
import electrosphere.net.parser.net.message.TerrainMessage;
import electrosphere.renderer.meshgen.TerrainChunkModelGeneration;
import electrosphere.renderer.model.Model;
import electrosphere.server.terrain.manager.ServerTerrainManager;

/**
 * Manages terrain storage and access on the client
 */
public class ClientTerrainManager {
    
    //queues messages from server
    List<TerrainMessage> messageQueue = new CopyOnWriteArrayList<TerrainMessage>();
    
    //The interpolation ratio of terrain
    public static final int INTERPOLATION_RATIO = ServerTerrainManager.SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO;
    
    //caches chunks from server
    static final int CACHE_SIZE = 500;
    
    //used for caching the macro values
    ClientTerrainCache terrainCache;
    
    //The world data for the client
    ClientWorldData clientWorldData;

    //The queue of terrain chunk data to be buffered to gpu
    static List<TerrainChunkGenQueueItem> terrainChunkGenerationQueue = new CopyOnWriteArrayList<TerrainChunkGenQueueItem>();
    
    /**
     * Constructor
     */
    public ClientTerrainManager(){
        terrainCache = new ClientTerrainCache(CACHE_SIZE);
    }
    
    
    /**
     * Handles messages that have been received from the server
     */
    public void handleMessages(){
        List<TerrainMessage> bouncedMessages = new LinkedList<TerrainMessage>();
        for(TerrainMessage message : messageQueue){
            messageQueue.remove(message);
            switch(message.getMessageSubtype()){
                case SENDCHUNKDATA: {
                    int[][][] values = new int[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE];
                    float[][][] weights = new float[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE];
                    ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData());
                    FloatBuffer floatBuffer = buffer.asFloatBuffer();
                    for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
                        for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){
                            for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
                                weights[x][y][z] = floatBuffer.get();
                            }
                        }
                    }
                    IntBuffer intView = buffer.asIntBuffer();
                    intView.position(floatBuffer.position());
                    for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
                        for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){
                            for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
                                values[x][y][z] = intView.get();
                            }
                        }
                    }
                    ChunkData data = new ChunkData();
                    data.setVoxelType(values);
                    data.setVoxelWeight(weights);
                    terrainCache.addChunkDataToCache(
                        message.getworldX(), message.getworldY(), message.getworldZ(), 
                        data
                    );
                } break;
                default:
                    LoggerInterface.loggerEngine.WARNING("ClientTerrainManager: unhandled network message of type" + message.getMessageSubtype());
                    break;
            }
        }
        for(TerrainMessage message : bouncedMessages){
            messageQueue.add(message);
        }
    }
    
    /**
     * Attaches a terrain message to the queue of messages that this manager needs to process
     * @param message The message
     */
    public void attachTerrainMessage(TerrainMessage message){
        messageQueue.add(message);
    }
    
    /**
     * Checks if the terrain cache contains chunk data at a given world position
     * @param worldX the x position
     * @param worldY the y position
     * @param worldZ the z position
     * @return true if the data exists, false otherwise
     */
    public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){
        return terrainCache.containsChunkDataAtWorldPoint(worldX, worldY, worldZ);
    }

    /**
     * Checks if the terrain cache contains chunk data at a given world position
     * @param worldPos The vector containing the world-space position
     * @return true if the data exists, false otherwise
     */
    public boolean containsChunkDataAtWorldPoint(Vector3i worldPos){
        return terrainCache.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z);
    }
    
    /**
     * Checks that the cache contains chunk data at a real-space coordinate
     * @param x the x coordinate
     * @param y the y coordinate
     * @param z the z coordinate
     * @return true if the cache contains the chunk data at the coordinate, false otherwise
     */
    public boolean containsChunkDataAtRealPoint(double x, double y, double z){
        assert clientWorldData != null;
        return terrainCache.containsChunkDataAtWorldPoint(
            clientWorldData.convertRealToChunkSpace(x), 
            clientWorldData.convertRealToChunkSpace(y), 
            clientWorldData.convertRealToChunkSpace(z)
        );
    }
    
    /**
     * Gets the chunk data at a given world position
     * @param worldX The x component of the world coordinate
     * @param worldY The y component of the world coordinate
     * @param worldZ The z component of the world coordinate
     * @return The chunk data if it exists, otherwise null
     */
    public ChunkData getChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){
        return terrainCache.getSubChunkDataAtPoint(worldX, worldY, worldZ);
    }

    /**
     * Gets the chunk data at a given world position
     * @param worldPos The world position as a joml vector
     * @return The chunk data if it exists, otherwise null
     */
    public ChunkData getChunkDataAtWorldPoint(Vector3i worldPos){
        return terrainCache.getSubChunkDataAtPoint(worldPos.x, worldPos.y, worldPos.z);
    }
    
    

    /**
     * Queues a terrain chunk to be pushed to GPU based on chunk data
     * @param data The chunk data (triangles, normals, etc)
     * @return The model path that is promised to eventually reflect the terrain model when it makes it to gpu
     */
    public static String queueTerrainGridGeneration(TerrainChunkData data, VoxelTextureAtlas atlas){
        String promisedHash = "";
        UUID newUUID = UUID.randomUUID();
        promisedHash = newUUID.toString();
        TerrainChunkGenQueueItem queueItem = new TerrainChunkGenQueueItem(data, promisedHash, atlas);
        terrainChunkGenerationQueue.add(queueItem);
        return promisedHash;
    }

    /**
     * Pushes all terrain data in queue to the gpu and registers the resulting models
     */
    public static void generateTerrainChunkGeometry(){
        Globals.profiler.beginCpuSample("generateTerrainChunkGeometry");
        for(TerrainChunkGenQueueItem queueItem : terrainChunkGenerationQueue){
            Model terrainModel = TerrainChunkModelGeneration.generateTerrainModel(queueItem.getData(), queueItem.getAtlas());
            Globals.assetManager.registerModelToSpecificString(terrainModel, queueItem.getPromisedHash());
        }
        terrainChunkGenerationQueue.clear();
        Globals.profiler.endCpuSample();
    }

    /**
     * Gets all chunks in the terrain cache
     * @return The collection of all chunk data objects
     */
    public Collection<ChunkData> getAllChunks(){
        return terrainCache.getAllChunks();
    }

    /**
     * Gets the world position of a given chunk
     * @param chunk The chunk
     * @return The world position of the chunk
     */
    public Vector3i getPositionOfChunk(ChunkData chunk){
        return terrainCache.getChunkPosition(chunk);
    }
    
}
