import IMemory from "modloader64_api/IMemory";
import { EventBus } from "./EventHandler";

export let NULLPTR_ID = -1;
let HEAP_ALIGN_BIG = 0x40;
let HEAP_ALIGN_NORM = 0x8;

export interface IHeapBlock {
    id: number;
    address: number;
    size: number;
    isFree: boolean;
    prevId: number;
    nextId: number;
}

export const enum HeapEvents {
    ON_BLOCK_MOVED = "ON_BLOCK_MOVED",
    ON_BLOCK_FREED = "ON_BLOCK_FREED"
}

export class HeapBlock extends EventBus implements IHeapBlock {
    id: number;
    address: number;
    size: number;
    isFree: boolean;
    prevId: number;
    nextId: number;

    constructor(id: number, address: number, size: number, isFree: boolean, prevId: number, nextId: number) {
        super();
        this.id = id;
        this.address = address;
        this.size = size;
        this.isFree = isFree;
        this.prevId = prevId;
        this.nextId = nextId;
    }
}

export class DefragHeap {
    public heapStart: number;
    public heapSize: number;
    private memory: IMemory;
    private uuidCounter: number;
    public heapBlocks: HeapBlock[];

    constructor(heapStart: number, heapSize: number, memory: IMemory) {
        this.heapStart = heapStart;
        this.heapSize = heapSize;
        this.memory = memory;
        this.uuidCounter = 0;
        this.heapBlocks = [];
    }

    public malloc(size: number, bigAlign: boolean = false): HeapBlock | undefined {
        let alignedSize = this.alignSize(size, bigAlign ? HEAP_ALIGN_BIG : HEAP_ALIGN_NORM);
        let blockId = -1;

        // Check if existing free block can be reused
        for (let index = 0; index < this.heapBlocks.length; index++) {
            let block = this.heapBlocks[index];
            if (block.isFree && block.size >= alignedSize) {
                blockId = block.id;
                block.isFree = false;
                block.size = alignedSize;
                break;
            }
        }

        // Create new block if not found
        if (blockId === -1) {
            let lastBlock = this.heapBlocks[this.heapBlocks.length - 1];
            let newBlockAddress = lastBlock ? lastBlock.address + lastBlock.size : this.heapStart;
            let newBlock = new HeapBlock(this.uuidCounter++, newBlockAddress, alignedSize, false, lastBlock ? lastBlock.id : -1, -1);
            this.heapBlocks.push(newBlock);
            blockId = newBlock.id;
        }

        // Create new free block
        let block = this.get_block(blockId);
        if (block !== undefined) {
            if (block.address + block.size < this.heapStart + this.heapSize) {
                let newFreeBlockAddress = block.address + alignedSize;
                let newFreeBlockSize = (this.heapStart + this.heapSize) - (block.address + block.size);
                let newFreeBlockNext = block.nextId;
                if (block.nextId != -1) {
                    try {
                        let nextBlock = this.get_block(block.nextId)
                        if (nextBlock !== undefined) {
                            newFreeBlockSize = nextBlock.address - (block.address + block.size);
                        }
                    } catch (a) {
                        console.error(`[${this.uuidCounter}] -> ${block.nextId};\n${JSON.stringify(this.heapBlocks, null, 2)}}\n${a}`)
                    }
                }
                let newFreeBlock = new HeapBlock(this.uuidCounter++, newFreeBlockAddress, newFreeBlockSize, true, blockId, newFreeBlockNext);
                this.heapBlocks.push(newFreeBlock);
                block.nextId = newFreeBlock.id;
            }
        }

        return block;
    }

    public realloc(size: number, id: number, bigAlign: boolean = false): HeapBlock | undefined {
        let alignedSize = this.alignSize(size, bigAlign ? HEAP_ALIGN_BIG : HEAP_ALIGN_NORM);
        let block = this.get_block(id);
        if (block !== undefined) {
            let blockId = block.id;

            // Move block
            if (block.size < alignedSize) {
                let lastBlock = this.heapBlocks[this.heapBlocks.length - 1];
                let newBlockAddress = lastBlock ? lastBlock.address + lastBlock.size : this.heapStart;
                let newBlock = new HeapBlock(this.uuidCounter++, newBlockAddress, alignedSize, false, lastBlock ? lastBlock.id : -1, -1);
                this.heapBlocks.push(newBlock);
                blockId = newBlock.id;

                // Copy data to the new location
                let buf = this.memory.rdramReadBuffer(block.address, block.size);
                this.memory.rdramWriteBuffer(newBlock.address, buf);
                lastBlock.emit(HeapEvents.ON_BLOCK_MOVED, newBlock);

                this.free(block); // Free the old block
            }
            else if (block.size > alignedSize) {
                block.size = alignedSize; // Resize existing block

                // Create new free block
                let nextBlock = this.heapBlocks[this.heapBlocks.indexOf(block) + 1];
                if (nextBlock) {
                    let newFreeBlockAddress = block.address + alignedSize;
                    let newFreeBlockSize = nextBlock.address - (block.address + block.size);
                    let newFreeBlockNext = nextBlock.id;
                    let newFreeBlock = new HeapBlock(this.uuidCounter++, newFreeBlockAddress, newFreeBlockSize, true, blockId, newFreeBlockNext);
                    this.heapBlocks.splice(this.heapBlocks.indexOf(block) + 1, 0, newFreeBlock);
                    block.nextId = newFreeBlock.id;
                    nextBlock.prevId = newFreeBlock.id;
                }
            }
        }
        return block;
    }

    public free(b: HeapBlock): void {
        let block = this.get_block(b.id);
        if (block !== undefined) {
            block.isFree = true;
            this.memory.rdramWriteBuffer(block.address, Buffer.alloc(block.size));
            block.emit(HeapEvents.ON_BLOCK_FREED, {});
            block.removeAllListeners();
        }
    }

    public get_block(id: number): HeapBlock | undefined {
        let index = this.heapBlocks.findIndex(block => block.id === id);
        if (index === -1) return undefined;
        return this.heapBlocks[index];
    }

    public defrag(): void {
        if (!this.heapBlocks || this.heapBlocks.length === 0) {
            return;
        }

        // Iteration #1
        // Find all free blocks and delete them.
        let deadBlocks: number[] = [];
        for (let i = 0; i < this.heapBlocks.length; i++) {
            if (this.heapBlocks[i].isFree){
                deadBlocks.push(i);
            }
        }
        while (deadBlocks.length > 0) {
            let i = deadBlocks.shift();
            if (i){
                let id = this.heapBlocks.splice(i, 1);
            }
        }
        // Iteration #2
        // Fix linked list.
        for (let i = 0; i < this.heapBlocks.length; i++) {
            let nextBlock = this.get_block(i + 1);
            let prevBlock = this.get_block(i - 1);
            let nextId = nextBlock !== undefined ? nextBlock.id : -1;
            let prevId = prevBlock !== undefined ? prevBlock.id : -1;

            this.heapBlocks[i].prevId = prevId;
            this.heapBlocks[i].nextId = nextId;
        }
        // Iteration #3
        // Move every block to the left as far as possible.
        let curAddr = this.heapStart;
        for (let i = 0; i < this.heapBlocks.length; i++) {
            if (this.heapBlocks[i].address !== curAddr) {
                this.heapBlocks[i].address = curAddr;
                this.heapBlocks[i].emit(HeapEvents.ON_BLOCK_MOVED, this.heapBlocks[i]);
            }
            curAddr += this.heapBlocks[i].size;
        }
    }

    private alignSize(size: number, ALIGN: number = HEAP_ALIGN_NORM): number {
        return size % ALIGN === 0 ? size : size + ALIGN - (size % ALIGN);
    }
}


