#include "egg/gfx/eggTextureBuffer.h"

#include "common.h"
#include "egg/gfx/eggCpuTexture.h"
#include "egg/gfx/eggStateGX.h"
#include "rvl/GX/GXTexture.h"
#include "rvl/GX/GXTypes.h"

namespace EGG {

TextureBuffer *TextureBuffer::spBufferTable[NUM_BUFFERS];

TextureBuffer::TextureBuffer() : mSize(0), mState(STATE_FREE), mpNext(nullptr), mpPrev(nullptr) {}

void TextureBuffer::configure() {
    CapTexture::configure();
    setClearColor(StateGX::getClearEfb());
    setWrap(GX_CLAMP, GX_CLAMP);
    setFilt(GX_LINEAR, GX_LINEAR);
    setPixModeSync(true);
}

// Allow buffer to be reclaimed
void TextureBuffer::free() {
    mState = STATE_FREE;
    sBufferSize += mSize;

    // Check if previous buffer is reclaimable
    if (mpPrev != nullptr && mpPrev->mState == STATE_FREE) {
        // Merge with previous buffer
        mSize += mpPrev->mSize;
        setBuffer((u8 *)getBuffer() - mpPrev->mSize);

        // Append reusable buffer
        TextureBuffer *beforeAppend = mpPrev->mpPrev;
        append(mpPrev);

        // Repair mpPrev link
        mpPrev = beforeAppend;
        if (beforeAppend != nullptr) {
            beforeAppend->mpNext = this;
        } else {
            spHead = this;
        }
    }

    // Check if next buffer is reclaimable
    if (mpNext != nullptr && mpNext->mState == STATE_FREE) {
        // Merge with next buffer
        mSize += mpNext->mSize;

        // Append reusable buffer
        TextureBuffer *afterAppend = mpNext->mpNext;
        append(mpNext);

        // Repair mpNext link
        mpNext = afterAppend;
        if (afterAppend != nullptr) {
            afterAppend->mpPrev = this;
        }
    }
}

// Alloc list buffer
void TextureBuffer::initialize(u32 size, Heap *heap_) {
    Heap *heap = heap_ == nullptr ? Heap::getCurrentHeap() : heap_;
    if (includesHeader()) {
        size += 0x20;
    }

    sBufferAllSize = size;
    spBufferAll = new (heap, 32) char[size];

    if (includesHeader()) {
        spBufferAll += 0x20;
    }

    sBufferSize = size;
    sBufferRestSizeMin = size;
    spTailNotJoin = nullptr;
    for (int i = 0; i < NUM_BUFFERS; i++) {
        spBufferTable[i] = new TextureBuffer();
        spBufferTable[i]->configure();
        append(spBufferTable[i]);
    }

    // Initialize head
    spHead = getNotJoin();
    spHead->mState = STATE_FREE;
    spHead->mSize = size;
    spHead->setBuffer(spBufferAll);
}

// Remove and return buffer at list end
TextureBuffer *TextureBuffer::getNotJoin() {
    TextureBuffer *p_buffer = spTailNotJoin;

    // Move list tail back
    spTailNotJoin = spTailNotJoin->mpPrev;
    if (spTailNotJoin != nullptr) {
        spTailNotJoin->mpNext = nullptr;
    }

    // Pop old tail
    p_buffer->mpPrev = nullptr;
    return p_buffer;
}

// Attempt to alloc buffer of specified size
void TextureBuffer::alloc(u32 size) {
    size = ROUND_UP(size, 0x20);

    sBufferSize -= size;
    if (sBufferSize < sBufferRestSizeMin) {
        sBufferRestSizeMin = sBufferSize;
    }

    // Try to find a free buffer with at least the requested size
    for (TextureBuffer *p_list = spHead; p_list != nullptr; p_list = p_list->mpNext) {
        if (p_list->mState == STATE_FREE && p_list->mSize >= size) {
            // Assign free buffer data to new buffer
            mState = STATE_ALLOCED;
            mSize = size;

            u8 *buffer = (u8 *)p_list->getBuffer();
            if (includesHeader()) {
                setBuffer(buffer);
                if (buffer != nullptr) {
                    setFlag(HAS_HEADER);
                }
            } else {
                setBuffer(buffer);
            }

            // Prepend new buffer before old buffer
            mpNext = p_list;
            TextureBuffer *oldPrev = p_list->mpPrev;
            mpPrev = oldPrev;

            // Fix this->prev link
            if (oldPrev != nullptr) {
                oldPrev->mpNext = this;
            } else {
                // nullptr prev buffer must be list head

                spHead = this;
            }

            // Remove requested size from old buffer
            p_list->mSize -= size;

            // Free old buffer if now empty
            if (p_list->mSize == 0) {
                if (p_list->mpNext != nullptr) {
                    p_list->mpNext->mpPrev = this;
                }
                mpNext = p_list->mpNext;
                append(p_list);
            }
            // Adjust old buffer by requested size
            else {
                p_list->setBuffer(buffer + size);
                p_list->mpPrev = this;
            }

            return;
        }
    }
}

TextureBuffer *TextureBuffer::alloc(u16 width, u16 height, GXTexFmt fmt) {
    if (width == 0 || height == 0) {
        return nullptr;
    }
    TextureBuffer *tex = getNotJoin();
    tex->setWidth(width);
    tex->setHeight(height);
    tex->setFormat(fmt);
    tex->configure();
    tex->alloc(GXGetTexBufferSize(tex->getWidth(), tex->getHeight(), tex->getFormat(), false, 1));
    return tex;
}

void TextureBuffer::setIncludesHeader(bool b) {
    sIncludesHeader = b;
}

bool TextureBuffer::includesHeader() {
    return sIncludesHeader;
}

TextureBuffer *TextureBuffer::spHead;
TextureBuffer *TextureBuffer::spTailNotJoin;
char *TextureBuffer::spBufferAll;
u32 TextureBuffer::sBufferAllSize;
u32 TextureBuffer::sBufferSize;
u32 TextureBuffer::sBufferRestSizeMin;
bool TextureBuffer::sIncludesHeader;

} // namespace EGG
