package systems.crigges.jmpq3;

import systems.crigges.jmpq3.security.MPQEncryption;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;

import static java.nio.file.StandardOpenOption.*;
import static systems.crigges.jmpq3.MpqFile.*;

public class BlockTable {
    private MappedByteBuffer blockMap;
    private int size;

    public BlockTable(ByteBuffer buf) throws IOException {
        this.size = (buf.capacity() / 16);
        
        final ByteBuffer decryptedBuffer = ByteBuffer.allocate(buf.capacity());
        new MPQEncryption(-326913117, true).processFinal(buf, decryptedBuffer);
        byte[] decrypted = decryptedBuffer.array();

        File block = File.createTempFile("block", "jmpq", JMpqEditor.tempDir);
        block.deleteOnExit();

        try (FileOutputStream blockStream = new FileOutputStream(block); FileChannel blockChannel = FileChannel.open(block.toPath(), CREATE, WRITE, READ)) {

            blockStream.write(decrypted);
            blockStream.flush();
            this.blockMap = blockChannel.map(FileChannel.MapMode.READ_WRITE, 0L, blockChannel.size());
            this.blockMap.order(ByteOrder.LITTLE_ENDIAN);
        }
    }

    public static void writeNewBlocktable(ArrayList<Block> blocks, int size, MappedByteBuffer buf) {
        ByteBuffer temp = ByteBuffer.allocate(size * 16);
        temp.order(ByteOrder.LITTLE_ENDIAN);
        for (Block b : blocks) {
            b.writeToBuffer(temp);
        }
        temp.clear();
        if (new MPQEncryption(-326913117, false).processFinal(temp, buf))
            throw new BufferOverflowException(); 
    }

    public Block getBlockAtPos(int pos) throws JMpqException {
        if ((pos < 0) || (pos > this.size)) {
            throw new JMpqException("Invaild block position");
        }
        this.blockMap.position(pos * 16);
        try {
            return new Block(this.blockMap);
        } catch (IOException e) {
            throw new JMpqException(e);
        }
    }

    public ArrayList<Block> getAllVaildBlocks() throws JMpqException {
        ArrayList<Block> list = new ArrayList<Block>();
        for (int i = 0; i < this.size; i++) {
            Block b = getBlockAtPos(i);
            if ((b.getFlags() & 0x80000000) == -2147483648) {
                list.add(b);
            }
        }
        return list;
    }

    public static class Block {
        private long filePos;
        private int compressedSize;
        private int normalSize;
        private int flags;

        public Block(MappedByteBuffer buf) throws IOException {
            this.filePos = buf.getInt();
            this.compressedSize = buf.getInt();
            this.normalSize = buf.getInt();
            this.flags = buf.getInt();
        }

        public Block(long filePos, int compressedSize, int normalSize, int flags) {
            this.filePos = filePos;
            this.compressedSize = compressedSize;
            this.normalSize = normalSize;
            this.flags = flags;
        }

        public void writeToBuffer(ByteBuffer bb) {
            bb.putInt((int) this.filePos);
            bb.putInt(this.compressedSize);
            bb.putInt(this.normalSize);
            bb.putInt(this.flags);
        }

        public int getFilePos() {
            return (int) this.filePos;
        }

        public int getCompressedSize() {
            return this.compressedSize;
        }

        public int getNormalSize() {
            return this.normalSize;
        }

        public int getFlags() {
            return this.flags;
        }

        public void setFilePos(int filePos) {
            this.filePos = filePos;
        }

        public void setCompressedSize(int compressedSize) {
            this.compressedSize = compressedSize;
        }

        public void setNormalSize(int normalSize) {
            this.normalSize = normalSize;
        }

        public void setFlags(int flags) {
            this.flags = flags;
        }

        public boolean hasFlag(int flag) {
            return (flags & flag) == flag;
        }

        public String toString() {
            return "Block [filePos=" + this.filePos + ", compressedSize=" + this.compressedSize + ", normalSize=" + this.normalSize + ", flags=" +
                    printFlags().trim() + "]";
        }

        public String printFlags() {
            return (hasFlag(EXISTS) ? "EXISTS " : "") + (hasFlag(SINGLE_UNIT) ? "SINGLE_UNIT " : "") + (hasFlag(COMPRESSED) ? "COMPRESSED " : "")
                    + (hasFlag(ENCRYPTED) ? "ENCRYPTED " : "") + (hasFlag(ADJUSTED_ENCRYPTED) ? "ADJUSTED " : "") + (hasFlag(DELETED) ? "DELETED " : "");
        }
    }
}