package cn.nukkit.level.format.mcregion;

import cn.nukkit.level.format.FullChunk;
import cn.nukkit.level.format.LevelProvider;
import cn.nukkit.level.format.generic.BaseRegionLoader;
import cn.nukkit.utils.*;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;

/**
 * author: MagicDroidX
 * Nukkit Project
 */

public class RegionLoader extends BaseRegionLoader {

    public RegionLoader(LevelProvider level, int regionX, int regionZ) {
        super(level, regionX, regionZ, "mcr");
    }

    @Override
    protected boolean isChunkGenerated(int index) {
        Integer[] array = this.locationTable.get(index);
        return !(array[0] == 0 || array[1] == 0);
    }

    public Chunk readChunk(int x, int z) throws IOException {
        int index = getChunkOffset(x, z);
        if (index < 0 || index >= 4096) {
            return null;
        }

        this.lastUsed = System.currentTimeMillis();

        if (!this.isChunkGenerated(index)) {
            return null;
        }

        Integer[] table = this.locationTable.get(index);
        RandomAccessFile raf = this.getRandomAccessFile();
        raf.seek(table[0] << 12);
        int length = raf.readInt();
        if (length <= 0 || length >= MAX_SECTOR_LENGTH) {
            if (length >= MAX_SECTOR_LENGTH) {
                table[0] = ++this.lastSector;
                table[1] = 1;
                this.locationTable.put(index, table);
                MainLogger.getLogger().error("Corrupted chunk header detected");
            }
            return null;
        }

        byte compression = raf.readByte();
        if (length > (table[1] << 12)) {
            MainLogger.getLogger().error("Corrupted bigger chunk detected");
            table[1] = length >> 12;
            this.locationTable.put(index, table);
            this.writeLocationIndex(index);
        } else if (compression != COMPRESSION_ZLIB && compression != COMPRESSION_GZIP) {
            MainLogger.getLogger().error("Invalid compression type");
            return null;
        }

        byte[] data = new byte[length - 1];
        raf.readFully(data);
        Chunk chunk = this.unserializeChunk(data);
        if (chunk != null) {
            return chunk;
        } else {
            MainLogger.getLogger().error("Corrupted chunk detected");
            return null;
        }
    }

    @Override
    protected Chunk unserializeChunk(byte[] data) {
        return Chunk.fromBinary(data, this.levelProvider);
    }

    @Override
    public boolean chunkExists(int x, int z) {
        return this.isChunkGenerated(getChunkOffset(x, z));
    }

    @Override
    protected void saveChunk(int x, int z, byte[] chunkData) throws IOException {
        int length = chunkData.length + 1;
        if (length + 4 > MAX_SECTOR_LENGTH) {
            throw new ChunkException("Chunk is too big! " + (length + 4) + " > " + MAX_SECTOR_LENGTH);
        }
        int sectors = (int) Math.ceil((length + 4) / 4096d);
        int index = getChunkOffset(x, z);
        boolean indexChanged = false;
        Integer[] table = this.locationTable.get(index);

        if (table[1] < sectors) {
            table[0] = this.lastSector + 1;
            this.locationTable.put(index, table);
            this.lastSector += sectors;
            indexChanged = true;
        } else if (table[1] != sectors) {
            indexChanged = true;
        }

        table[1] = sectors;
        table[2] = (int) (System.currentTimeMillis() / 1000d);

        this.locationTable.put(index, table);

        RandomAccessFile raf = this.getRandomAccessFile();
        raf.seek(table[0] << 12);

        BinaryStream stream = new BinaryStream();
        stream.put(Binary.writeInt(length));
        stream.putByte(COMPRESSION_ZLIB);
        stream.put(chunkData);
        byte[] data = stream.getBuffer();
        if (data.length < sectors << 12) {
            byte[] newData = new byte[sectors << 12];
            System.arraycopy(data, 0, newData, 0, data.length);
            data = newData;
        }

        raf.write(data);

        if (indexChanged) {
            this.writeLocationIndex(index);
        }

    }

    @Override
    public void removeChunk(int x, int z) {
        int index = getChunkOffset(x, z);
        Integer[] table = this.locationTable.get(0);
        table[0] = 0;
        table[1] = 0;
        this.locationTable.put(index, table);
    }

    @Override
    public void writeChunk(FullChunk chunk) throws Exception {
        this.lastUsed = System.currentTimeMillis();
        byte[] chunkData = chunk.toBinary();
        this.saveChunk(chunk.getX() & 0x1f, chunk.getZ() & 0x1f, chunkData);
    }

    protected static int getChunkOffset(int x, int z) {
        return x | (z << 5);
    }

    @Override
    public void close() throws IOException {
        this.writeLocationTable();
        this.levelProvider = null;
        super.close();
    }

    @Override
    public int doSlowCleanUp() throws Exception {
        RandomAccessFile raf = this.getRandomAccessFile();
        for (int i = 0; i < 1024; i++) {
            Integer[] table = this.locationTable.get(i);
            if (table[0] == 0 || table[1] == 0) {
                continue;
            }
            raf.seek(table[0] << 12);
            byte[] chunk = new byte[table[1] << 12];
            raf.readFully(chunk);
            int length = Binary.readInt(Arrays.copyOfRange(chunk, 0, 3));
            if (length <= 1) {
                this.locationTable.put(i, (table = new Integer[]{0, 0, 0}));
            }
            try {
                chunk = Zlib.inflate(Arrays.copyOf(chunk, 5));
            } catch (Exception e) {
                this.locationTable.put(i, new Integer[]{0, 0, 0});
                continue;
            }
            chunk = Zlib.deflate(chunk, 9);
            ByteBuffer buffer = ByteBuffer.allocate(4 + 1 + chunk.length);
            buffer.put(Binary.writeInt(chunk.length + 1));
            buffer.put(COMPRESSION_ZLIB);
            buffer.put(chunk);
            chunk = buffer.array();
            int sectors = (int) Math.ceil(chunk.length / 4096d);
            if (sectors > table[1]) {
                table[0] = this.lastSector + 1;
                this.lastSector += sectors;
                this.locationTable.put(i, table);
            }
            raf.seek(table[0] << 12);
            byte[] bytes = new byte[sectors << 12];
            ByteBuffer buffer1 = ByteBuffer.wrap(bytes);
            buffer1.put(chunk);
            raf.write(buffer1.array());
        }
        this.writeLocationTable();
        int n = this.cleanGarbage();
        this.writeLocationTable();
        return n;
    }

    @Override
    protected void loadLocationTable() throws IOException {
        RandomAccessFile raf = this.getRandomAccessFile();
        raf.seek(0);
        this.lastSector = 1;
        int[] data = new int[1024 * 2]; //1024 records * 2 times
        for (int i = 0; i < 1024 * 2; i++) {
            data[i] = raf.readInt();
        }
        for (int i = 0; i < 1024; ++i) {
            int index = data[i];
            this.locationTable.put(i, new Integer[]{index >> 8, index & 0xff, data[1024 + i]});
            int value = this.locationTable.get(i)[0] + this.locationTable.get(i)[1] - 1;
            if (value > this.lastSector) {
                this.lastSector = value;
            }
        }
    }

    private void writeLocationTable() throws IOException {
        RandomAccessFile raf = this.getRandomAccessFile();
        raf.seek(0);
        for (int i = 0; i < 1024; ++i) {
            Integer[] array = this.locationTable.get(i);
            raf.writeInt((array[0] << 8) | array[1]);
        }
        for (int i = 0; i < 1024; ++i) {
            Integer[] array = this.locationTable.get(i);
            raf.writeInt(array[2]);
        }
    }

    private int cleanGarbage() throws IOException {
        Map<Integer, Integer> sectors = new TreeMap<>();
        for (int index : new ArrayList<>(this.locationTable.keySet())) {
            Integer[] data = this.locationTable.get(index);
            if (data[0] == 0 || data[1] == 0) {
                this.locationTable.put(index, new Integer[]{0, 0, 0});
                continue;
            }
            sectors.put(data[0], index);
        }

        if (sectors.size() == (this.lastSector - 2)) {
            return 0;
        }

        int shift = 0;
        int lastSector = 1;

        RandomAccessFile raf = this.getRandomAccessFile();
        raf.seek(8192);
        int s = 2;
        for (int sector : sectors.keySet()) {
            s = sector;
            int index = sectors.get(sector);
            if ((sector - lastSector) > 1) {
                shift += sector - lastSector - 1;
            }
            if (shift > 0) {
                raf.seek(sector << 12);
                byte[] old = new byte[4096];
                raf.readFully(old);
                raf.seek((sector - shift) << 12);
                raf.write(old);
            }
            Integer[] v = this.locationTable.get(index);
            v[0] -= shift;
            this.locationTable.put(index, v);
            this.lastSector = sector;
        }
        raf.setLength((s + 1) << 12);
        return shift;
    }

    @Override
    protected void writeLocationIndex(int index) throws IOException {
        Integer[] array = this.locationTable.get(index);
        RandomAccessFile raf = this.getRandomAccessFile();

        raf.seek(index << 2);
        raf.writeInt((array[0] << 8) | array[1]);
        raf.seek(4096 + (index << 2));
        raf.writeInt(array[2]);
    }

    @Override
    protected void createBlank() throws IOException {
        RandomAccessFile raf = this.getRandomAccessFile();

        raf.seek(0);
        raf.setLength(0);
        this.lastSector = 1;
        int time = (int) (System.currentTimeMillis() / 1000d);
        for (int i = 0; i < 1024; ++i) {
            this.locationTable.put(i, new Integer[]{0, 0, time});
            raf.writeInt(0);
        }
        for (int i = 0; i < 1024; ++i) {
            raf.writeInt(time);
        }
    }

    @Override
    public int getX() {
        return x;
    }

    @Override
    public int getZ() {
        return z;
    }

}