/*
 * This file is part of SpoutPlugin.
 *
 * Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
 * SpoutPlugin is licensed under the GNU Lesser General Public License.
 *
 * SpoutPlugin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SpoutPlugin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.gmail.nossr50.util.blockmeta.chunkmeta;

import java.io.*;
import java.util.ArrayList;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

public class McMMOSimpleRegionFile {
    private RandomAccessFile file;
    private final int[] dataStart = new int[1024];
    private final int[] dataActualLength = new int[1024];
    private final int[] dataLength = new int[1024];
    private final ArrayList<Boolean> inuse = new ArrayList<>();
    private int segmentSize;
    private int segmentMask;
    private final int rx;
    private final int rz;
    private final int defaultSegmentSize;
    private final File parent;
    @SuppressWarnings("unused")
    private long lastAccessTime = System.currentTimeMillis();
    @SuppressWarnings("unused")
    private static final long TIMEOUT_TIME = 300000; // 5 min

    public McMMOSimpleRegionFile(File f, int rx, int rz) {
        this(f, rx, rz, 10);
    }

    public McMMOSimpleRegionFile(File f, int rx, int rz, int defaultSegmentSize) {
        this.rx = rx;
        this.rz = rz;
        this.defaultSegmentSize = defaultSegmentSize;
        this.parent = f;

        lastAccessTime = System.currentTimeMillis();
        if (file == null) {
            try {
                this.file = new RandomAccessFile(parent, "rw");

                if (file.length() < 4096 * 3) {
                    for (int i = 0; i < 1024 * 3; i++) {
                        file.writeInt(0);
                    }
                    file.seek(4096 * 2);
                    file.writeInt(defaultSegmentSize);
                }

                file.seek(4096 * 2);

                this.segmentSize = file.readInt();
                this.segmentMask = (1 << segmentSize) - 1;

                int reservedSegments = this.sizeToSegments(4096 * 3);

                for (int i = 0; i < reservedSegments; i++) {
                    while (inuse.size() <= i) {
                        inuse.add(false);
                    }
                    inuse.set(i, true);
                }

                file.seek(0);

                for (int i = 0; i < 1024; i++) {
                    dataStart[i] = file.readInt();
                }

                for (int i = 0; i < 1024; i++) {
                    dataActualLength[i] = file.readInt();
                    dataLength[i] = sizeToSegments(dataActualLength[i]);
                    setInUse(i, true);
                }

                extendFile();
            }
            catch (IOException fnfe) {
                throw new RuntimeException(fnfe);
            }
        }
    }

    public synchronized final RandomAccessFile getFile() {
        lastAccessTime = System.currentTimeMillis();
        if (file == null) {
            try {
                this.file = new RandomAccessFile(parent, "rw");

                if (file.length() < 4096 * 3) {
                    for (int i = 0; i < 1024 * 3; i++) {
                        file.writeInt(0);
                    }
                    file.seek(4096 * 2);
                    file.writeInt(defaultSegmentSize);
                }

                file.seek(4096 * 2);

                this.segmentSize = file.readInt();
                this.segmentMask = (1 << segmentSize) - 1;

                int reservedSegments = this.sizeToSegments(4096 * 3);

                for (int i = 0; i < reservedSegments; i++) {
                    while (inuse.size() <= i) {
                        inuse.add(false);
                    }
                    inuse.set(i, true);
                }

                file.seek(0);

                for (int i = 0; i < 1024; i++) {
                    dataStart[i] = file.readInt();
                }

                for (int i = 0; i < 1024; i++) {
                    dataActualLength[i] = file.readInt();
                    dataLength[i] = sizeToSegments(dataActualLength[i]);
                    setInUse(i, true);
                }

                extendFile();
            }
            catch (IOException fnfe) {
                throw new RuntimeException(fnfe);
            }
        }
        return file;
    }

    public synchronized boolean testCloseTimeout() {
        /*
        if (System.currentTimeMillis() - TIMEOUT_TIME > lastAccessTime) {
            close();
            return true;
        }
         */
        return false;
    }

    public synchronized DataOutputStream getOutputStream(int x, int z) {
        int index = getChunkIndex(x, z);
        return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
    }

    public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
        int index = getChunkIndex(x, z);
        int actualLength = dataActualLength[index];

        if (actualLength == 0) {
            return null;
        }

        byte[] data = new byte[actualLength];

        getFile().seek(dataStart[index] << segmentSize);
        getFile().readFully(data);
        return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
    }

    synchronized void write(int index, byte[] buffer, int size) throws IOException {
        int oldStart = setInUse(index, false);
        int start = findSpace(oldStart, size);
        getFile().seek(start << segmentSize);
        getFile().write(buffer, 0, size);
        dataStart[index] = start;
        dataActualLength[index] = size;
        dataLength[index] = sizeToSegments(size);
        setInUse(index, true);
        saveFAT();
    }

    public synchronized void close() {
        try {
            if (file != null) {
                file.seek(4096 * 2);
                file.close();
            }

            file = null;
        }
        catch (IOException ioe) {
            throw new RuntimeException("Unable to close file", ioe);
        }
    }

    private synchronized int setInUse(int index, boolean used) {
        if (dataActualLength[index] == 0) {
            return dataStart[index];
        }

        int start = dataStart[index];
        int end = start + dataLength[index];

        for (int i = start; i < end; i++) {
            while (i > inuse.size() - 1) {
                inuse.add(false);
            }

            Boolean old = inuse.set(i, used);
            if (old != null && old == used) {
                if (old) {
                    throw new IllegalStateException("Attempting to overwrite an in-use segment");
                }

                throw new IllegalStateException("Attempting to delete empty segment");
            }
        }

        return dataStart[index];
    }

    private synchronized void extendFile() throws IOException {
        long extend = (-getFile().length()) & segmentMask;

        getFile().seek(getFile().length());

        while ((extend--) > 0) {
            getFile().write(0);
        }
    }

    private synchronized int findSpace(int oldStart, int size) {
        int segments = sizeToSegments(size);

        boolean oldFree = true;
        for (int i = oldStart; i < inuse.size() && i < oldStart + segments; i++) {
            if (inuse.get(i)) {
                oldFree = false;
                break;
            }
        }

        if (oldFree) {
            return oldStart;
        }

        int start = 0;
        int end = 0;

        while (end < inuse.size()) {
            if (inuse.get(end)) {
                end++;
                start = end;
            }
            else {
                end++;
            }

            if (end - start >= segments) {
                return start;
            }
        }

        return start;
    }

    private synchronized int sizeToSegments(int size) {
        if (size <= 0) {
            return 1;
        }

        return ((size - 1) >> segmentSize) + 1;
    }

    private synchronized Integer getChunkIndex(int x, int z) {
        if (rx != (x >> 5) || rz != (z >> 5)) {
            throw new RuntimeException(x + ", " + z + " not in region " + rx + ", " + rz);
        }

        x = x & 0x1F;
        z = z & 0x1F;

        return (x << 5) + z;
    }

    private synchronized void saveFAT() throws IOException {
        getFile().seek(0);
        for (int i = 0; i < 1024; i++) {
            getFile().writeInt(dataStart[i]);
        }

        for (int i = 0; i < 1024; i++) {
            getFile().writeInt(dataActualLength[i]);
        }
    }
}
