package electrosphere.server.block.diskmap;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterOutputStream;

import electrosphere.client.block.BlockChunkData;
import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.util.FileUtils;
import electrosphere.util.annotation.Exclude;
import electrosphere.util.math.HashUtils;

/**
 * An interface for accessing the disk map of chunk information
 */
public class ServerBlockChunkDiskMap {

    /**
     * The map of world position+chunk type to the file that actually houses that information
     */
    Map<Long,String> worldPosFileMap;

    /**
     * Locks the chunk disk map for thread safety
     */
    @Exclude
    ReentrantLock lock = new ReentrantLock();

    /**
     * Constructor
     */
    private ServerBlockChunkDiskMap(){
        worldPosFileMap = new HashMap<Long,String>();
    }

    /**
     * Gets a key for a given chunk file based on a world coordinate
     * @param worldX The x component
     * @param worldY The y component
     * @param worldZ The z component
     * @return The key
     */
    private static long getBlockChunkKey(int worldX, int worldY, int worldZ){
        return HashUtils.hashIVec(worldX, worldY, worldZ);
    }

    /**
     * Initializes a diskmap based on a given save name
     * @param saveName The save name
     */
    public static ServerBlockChunkDiskMap init(String saveName){
        ServerBlockChunkDiskMap rVal = null;
        LoggerInterface.loggerEngine.DEBUG("INIT CHUNK MAP " + saveName);
        if(FileUtils.getSaveFile(saveName, "chunk.map").exists()){
            rVal = FileUtils.loadObjectFromSavePath(saveName, "chunk.map", ServerBlockChunkDiskMap.class);
            LoggerInterface.loggerEngine.DEBUG("POS FILE MAP: " + rVal.worldPosFileMap.keySet());
        } else {
            rVal = new ServerBlockChunkDiskMap();
        }
        return rVal;
    }

    /**
     * Initializes a diskmap based on a given save name
     * @param saveName The save name
     */
    public static ServerBlockChunkDiskMap init(){
        return new ServerBlockChunkDiskMap();
    }

    /**
     * Saves the disk map to disk
     */
    public void save(){
        FileUtils.serializeObjectToSavePath(Globals.currentSave.getName(), "blockchunk.map", worldPosFileMap);
    }

    /**
     * Checks if the map contains a given chunk position
     * @param worldX The x component
     * @param worldY The y component
     * @param worldZ The z component
     * @return True if the map contains the chunk, false otherwise
     */
    public boolean containsBlocksAtPosition(int worldX, int worldY, int worldZ){
        lock.lock();
        boolean rVal = worldPosFileMap.containsKey(getBlockChunkKey(worldX, worldY, worldZ));
        lock.unlock();
        return rVal;
    }


    /**
     * Gets the block data chunk from disk if it exists, otherwise returns null
     * @param worldX The x coordinate
     * @param worldY The y coordinate
     * @param worldZ The z coordinate
     * @return The block data chunk if it exists, null otherwise
     */
    public BlockChunkData getBlockChunk(int worldX, int worldY, int worldZ){
        lock.lock();
        LoggerInterface.loggerEngine.INFO("Load chunk " + worldX + " " + worldY + " " + worldZ);
        BlockChunkData rVal = null;
        if(this.containsBlocksAtPosition(worldX, worldY, worldZ)){
            //read file
            String fileName = worldPosFileMap.get(getBlockChunkKey(worldX, worldY, worldZ));
            byte[] rawDataCompressed = FileUtils.loadBinaryFromSavePath(Globals.currentSave.getName(), fileName);
            //decompress
            byte[] rawData = null;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            InflaterOutputStream inflaterInputStream = new InflaterOutputStream(out);
            try {
                inflaterInputStream.write(rawDataCompressed);
                inflaterInputStream.flush();
                inflaterInputStream.close();
                rawData = out.toByteArray();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            //parse
            if(rawData != null){
                ByteBuffer buffer = ByteBuffer.wrap(rawData);
                ShortBuffer shortView = buffer.asShortBuffer();
                short[] type = new short[BlockChunkData.TOTAL_DATA_WIDTH];
                short[] metadata = new short[BlockChunkData.TOTAL_DATA_WIDTH];
                short firstType = -1;
                boolean homogenous = true;
                for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){
                    type[i] = shortView.get();
                    if(firstType == -1){
                        firstType = type[i];
                    } else if(homogenous && firstType == type[i]){
                        homogenous = false;
                    }
                }
                for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){
                    metadata[i] = shortView.get();
                }
                rVal = new BlockChunkData();
                rVal.setType(type);
                rVal.setMetadata(metadata);
                rVal.setHomogenousValue(homogenous ? firstType : BlockChunkData.NOT_HOMOGENOUS);
                rVal.setWorldX(worldX);
                rVal.setWorldY(worldY);
                rVal.setWorldZ(worldZ);
                rVal.setLod(BlockChunkData.LOD_FULL_RES);
            }
        }
        lock.unlock();
        return rVal;
    }

    /**
     * Saves a block data chunk to disk
     * @param chunkData The block data chunk
     */
    public void saveToDisk(BlockChunkData chunkData){
        lock.lock();
        LoggerInterface.loggerEngine.DEBUG("Save to disk: " + chunkData.getWorldX() + " " + chunkData.getWorldY() + " " + chunkData.getWorldZ());
        //get the file name for this chunk
        String fileName = null;
        Long chunkKey = getBlockChunkKey(chunkData.getWorldX(),chunkData.getWorldY(),chunkData.getWorldZ());
        if(worldPosFileMap.containsKey(chunkKey)){
            fileName = worldPosFileMap.get(chunkKey);
        } else {
            fileName = chunkKey + "b.dat";
        }
        //generate binary for the file
        short[] type = chunkData.getType();
        short[] metadata = chunkData.getMetadata();
        ByteBuffer buffer = ByteBuffer.allocate(BlockChunkData.TOTAL_DATA_WIDTH * 2 * 2);
        ShortBuffer shortView = buffer.asShortBuffer();
        for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){
            shortView.put(type[i]);
        }
        for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){
            shortView.put(metadata[i]);
        }
        //compress
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DeflaterOutputStream deflaterInputStream = new DeflaterOutputStream(out);
        try {
            deflaterInputStream.write(buffer.array());
            deflaterInputStream.flush();
            deflaterInputStream.close();
            //write to disk
            FileUtils.saveBinaryToSavePath(Globals.currentSave.getName(), fileName, out.toByteArray());
            //save to the map of filenames
            worldPosFileMap.put(chunkKey,fileName);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        lock.unlock();
    }

}
