package me.hammerle.supersnuvi.gamelogic;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;

public class LevelData 
{
    private final static int STRING_ID = 1;
    private final static int INTEGER_ID = 2;
    
    // container for all custom data
    private final HashMap<String, Object> data;
    
    // layers up to bIndex (including) are considered background
    // the next layer is the entity spawn layer
    // every layer after that is foreground
    private int bIndex;
    private int width;
    private int height;
    private int layers;
    
    // storage for tiles
    // [layer][width][height]
    private int[][][] tiles;
    
    // the file of the level for saving and loading
    private final File file;
    
    public LevelData(File file)
    {
        this.file = file;
        this.data = new HashMap<>();
        this.bIndex = 0;
        this.width = 0;
        this.height = 0;
        this.layers = 0;
        this.tiles = null;
    }
    
    public LevelData(File file, int bIndex, int layer, int width, int height)
    {
        this.file = file;
        this.data = new HashMap<>();
        this.bIndex = bIndex;
        this.width = width;
        this.height = height;
        this.layers = layer;
        this.tiles = new int[layer][width][height];
    }
    
    // -------------------------------------------------------------------------
    // file writing / reading
    // -------------------------------------------------------------------------
    
    private void writeString(FileOutputStream out, String s, boolean identifier) throws IOException
    {
        // writing type identifier
        if(identifier)
        {
            out.write(STRING_ID);
        }
        // writing length of string
        byte[] b = s.getBytes();
        writeInt(out, b.length, false);
        // writing the bytes of the string
        out.write(b);
    }
    
    private String readString(FileInputStream in) throws IOException
    {
        int length = readInt(in);
        byte[] b = new byte[length];
        in.read(b);
        return new String(b);
    }
    
    private void writeInt(FileOutputStream out, int i, boolean identifier) throws IOException
    {
        // writing type identifier
        if(identifier)
        {
            out.write(INTEGER_ID);
        }
        // writing the int per byte
        out.write(i & 0xFF);
        out.write((i >> 8) & 0xFF);
        out.write((i >> 16) & 0xFF);
        out.write((i >> 24) & 0xFF);
    }
    
    private int readInt(FileInputStream in) throws IOException
    {
        int i = 0;
        i |= in.read();
        i |= in.read() << 8;
        i |= in.read() << 16;
        i |= in.read() << 24;
        return i;
    }
    
    public boolean save()
    {
        try
        {
            try(FileOutputStream out = new FileOutputStream(file)) 
            {
                // writing number of data fields
                writeInt(out, data.size(), false);
                // writing keys and data, data varies, key are always strings
                for(Entry<String, Object> e : data.entrySet())
                {
                    writeString(out, e.getKey(), false);
                    if(e.getValue() instanceof Integer)
                    {
                        writeInt(out, (Integer) e.getValue(),true);
                    }
                    else
                    {
                        writeString(out, e.getValue().toString(), true);
                    }
                }
                
                // write background index
                writeInt(out, bIndex, false);
                // write number of layers
                writeInt(out, layers, false);
                // write layer width
                writeInt(out, width, false);
                // write layer height
                writeInt(out, height, false);
                
                // write tiles
                for(int x = 0; x < layers; x++)
                {
                    for(int y = 0; y < width; y++)
                    {
                        for(int z = 0; z < height; z++)
                        {
                            writeInt(out, tiles[x][y][z], false);
                        }
                    }
                }
                
                out.flush();
                out.close();
            }
            return true;
        }
        catch(IOException ex)
        {
            ex.printStackTrace();
            return false;
        }
    }
    
    public boolean load()
    {
        try
        {
            try (FileInputStream in = new FileInputStream(file)) 
            {
                // getting the number of data fields
                int fields = readInt(in);
                // getting keys and data, data varies, key are always strings
                for(; fields > 0; fields--)
                {
                    String key = readString(in);
                    int id = in.read();
                    switch(id)
                    {
                        case STRING_ID:
                            data.put(key, readString(in));
                            break;
                        case INTEGER_ID:
                            data.put(key, readInt(in));
                            break;
                        default:
                            System.err.println("fucked up level data");
                            return false;
                    }
                }
                //data.forEach((k, v) -> System.out.println(k + " " + v + " " + v.getClass()));
                
                // getting background index
                bIndex = readInt(in);
                // getting number of layers
                layers = readInt(in);
                // getting layer width
                width = readInt(in);
                // getting layer height
                height = readInt(in);
                
                //System.out.println(layers + " " + width + " " + height);
                
                // init tiles
                tiles = new int[layers][width][height];
                // getting tiles
                for(int x = 0; x < layers; x++)
                {
                    for(int y = 0; y < width; y++)
                    {
                        for(int z = 0; z < height; z++)
                        {
                            tiles[x][y][z] = readInt(in);
                        }
                    }
                }
                
                in.close();
            }
            return true;
        }
        catch(IOException ex)
        {
            ex.printStackTrace();
            return false;
        }
    }
    
    // -------------------------------------------------------------------------
    // variable level data
    // -------------------------------------------------------------------------
    
    public void setData(String name, int value)
    {
        data.put(name, value);
    }
    
    public void setData(String name, String value)
    {
        data.put(name, value);
    }
    
    public int getInt(String name, int error)
    {
        Object o = data.get(name);
        if(o == null || !(o instanceof Integer))
        {
            return error;
        }
        return (Integer) o;   
    }
    
    public String getString(String name, String error)
    {
        Object o = data.get(name);
        if(o == null || !(o instanceof String))
        {
            return error;
        }
        return (String) o;   
    }
    
    // -------------------------------------------------------------------------
    // level data getter / setter
    // -------------------------------------------------------------------------

    public int getBackgroundIndex() 
    {
        return bIndex;
    }
    
    public void setBackgroundIndex(int bIndex) 
    {
        this.bIndex = bIndex;
    }
    
    public int getLayers() 
    {
        return layers;
    }
    
    public int getWidth() 
    {
        return width;
    }

    public int getHeight() 
    {
        return height;
    }
    
    // -------------------------------------------------------------------------
    // layer stuff
    // -------------------------------------------------------------------------
    
    public void clearLayer(int index)
    {
        if(index >= 0 && index < layers)
        {
            for(int x = 0; x < width; x++)
            {
                for(int y = 0; y < height; y++)
                {
                    tiles[index][x][y] = -1;
                }
            }
        }
    }
    
    public void copyLayer(int fromIndex, int toIndex)
    {
        if(fromIndex >= 0 && fromIndex < layers && toIndex >= 0 && toIndex < layers)
        { 
            for(int x = 0; x < width; x++)
            {
                System.arraycopy(tiles[fromIndex][x], 0, tiles[toIndex][x], 0, height);
            }
        }
    }
    
    public void addLayer()
    {
        int[][][] newLayers = new int[layers + 1][][];
        System.arraycopy(tiles, 0, newLayers, 0, layers);
        newLayers[layers] = new int[width][height];
        layers++;
        tiles = newLayers;
    }
    
    public void addLayer(int index)
    {
        if(index >= layers)
        {
            addLayer();
            return;
        }
        int[][][] newLayers = new int[layers + 1][][];
        System.arraycopy(tiles, 0, newLayers, 0, index);
        newLayers[index] = new int[width][height];
        System.arraycopy(tiles, index, newLayers, index + 1, layers - index);
        layers++;
        tiles = newLayers;
    }
    
    public void removeLayer(int index)
    {
        if(layers <= 1)
        {
            layers = 0;
            tiles = new int[0][0][0];
            return;
        }
        int[][][] newLayers = new int[layers - 1][][];
        if(index > 0)
        {
            System.arraycopy(tiles, 0, newLayers, 0, index);
        }
        int left = layers - index - 1;
        if(left > 0)
        {
            System.arraycopy(tiles, index + 1, newLayers, index, left);
        }
        layers--;
        tiles = newLayers;
    }
    
    public void setLayerSize(int w, int h)
    {
        int[][][] newLayers = new int[layers][w][h];
        
        int cw = Math.min(w, width);
        int ch = Math.min(h, height);
        for(int l = 0; l < layers; l++)
        {
            for(int x = 0; x < cw; x++)
            {
                System.arraycopy(tiles[l][x], 0, newLayers[l][x], 0, ch);
            }
        }
        
        width = w;
        height = h;
        tiles = newLayers;
    }
    
    // -------------------------------------------------------------------------
    // tile getter / setter
    // -------------------------------------------------------------------------
    
    public int getInteractionTile(int x, int y)
    {
        if(x < 0 || y < 0 || x >= width || y >= height || bIndex >= layers)
        {
            return -1;
        }
        return tiles[bIndex][x][y];
    }
    
    public int getTile(int layer, int x, int y)
    {
        return tiles[layer][x][y];
    }
    
    public void setTile(int layer, int x, int y, int value)
    {
        tiles[layer][x][y] = value;
    }
    
    // -------------------------------------------------------------------------
    // tile iterators
    // -------------------------------------------------------------------------
    
    public void forEachBackground(TileConsumer c, int sx, int ex, int sy, int ey)
    {
        int max = Math.min(layers - 1, bIndex);
        
        sx = Math.max(0, sx);
        sy = Math.max(0, sy);
        ex = Math.min(width, ex);
        ey = Math.min(height, ey);
        
        for(int l = 0; l <= max; l++)
        {
            for(int x = sx; x < ex; x++)
            {
                for(int y = sy; y < ey; y++)
                {
                    c.consume(x, y, tiles[l][x][y]);
                }
            }
        }
    }   
    
    public void forEachInteractTile(TileConsumer c)
    {
        if(bIndex < layers)
        {
            for(int y = 0; y < width; y++)
            {
                for(int z = 0; z < height; z++)
                {
                    c.consume(y, z, tiles[bIndex][y][z]);
                }
            }
        }
    }
    
    public void forEachEntity(TileConsumer c, int sx, int ex, int sy, int ey)
    {
        int l = bIndex + 1;
        if(l < layers)
        {
            sx = Math.max(0, sx);
            sy = Math.max(0, sy);
            ex = Math.min(width, ex);
            ey = Math.min(height, ey);
            
            for(int x = sx; x < ex; x++)
            {
                for(int y = sy; y < ey; y++)
                {
                    c.consume(x, y, tiles[l][x][y]);
                }
            }
        }
    }
    
    public void forEachForeground(TileConsumer c, int sx, int ex, int sy, int ey)
    {
        sx = Math.max(0, sx);
        sy = Math.max(0, sy);
        ex = Math.min(width, ex);
        ey = Math.min(height, ey);
        
        for(int l = bIndex + 2; l < layers; l++)
        {
            for(int x = sx; x < ex; x++)
            {
                for(int y = sy; y < ey; y++)
                {
                    c.consume(x, y, tiles[l][x][y]);
                }
            }
        }
    }
}
