#include "egg/core/eggAsyncDisplay.h"

#include "egg/core/eggXfbManager.h"
#include "egg/math/eggMatrix.h"
#include "rvl/GX/GXTexture.h"

// TODO: Find the proper place for this symbol? - Ghidra has it literally unused
extern "C" static EGG::Display *spSelector;

namespace {
static GXTexObj clear_z_tobj;
// clang-format off
u8 clear_z_TX[64] ALIGN_DECL(32) = {
    0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
    0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
// clang-format on
} // namespace

namespace EGG {

AsyncDisplay::AsyncDisplay(u8 maxRetrace) : Display(maxRetrace) {
    this->field_0x98 = 0;
    this->field_0x9C = 1.0f;
    this->field_0xA4 = 0;
    this->field_0xA8 = 0;
    this->field_0xAC = 0;
    this->field_0xB8 = 0;
    spSelector = this;
    OSInitThreadQueue(&this->mThreadQueue);
    OSInitMessageQueue(&this->mMesgQueue, this->mMesgBuf, 4);
}

// Random function that pretends to do an int to float
// cast here, for float ordering issues
f64 cast() {
    return 4503599627370496.0;
}

f32 AsyncDisplay::calcS() {
    f32 tmp = this->field_0x9C + this->field_0xA0;
    this->field_0x9C = tmp;
    if (tmp > 1.0f) {
        if (this->mScreenStateFlag.onBit(4)) {
            this->field_0x9C = tmp - 1.0f;
            this->mScreenStateFlag.resetBit(4);
        } else {
            this->field_0x9C = tmp - this->field_0xA0;
            this->mScreenStateFlag.setBit(4);
        }
    }
    return tmp;
}

inline void AsyncDisplay::waitForMsg(bool b) {
    // This is really ugly but I couldn't find a different way
    // to make the compiler use the registers
    XfbManager *xfb;
    while (
        !((xfb = BaseSystem::mConfigData->getXfbMgr(), 1 < xfb->mNumXfbs - this->field_0xAC) &&
          (BaseSystem::mConfigData->getXfbMgr()->mToCopyXfb != nullptr || b))
    ) {
        OSReceiveMessage(&this->mMesgQueue, this->mMesgBuf, 1);

        while (OSReceiveMessage(&this->mMesgQueue, this->mMesgBuf, 0)) {}
    }
}

void AsyncDisplay::beginFrame() {
    BaseSystem::mConfigData->getXfbMgr();
    GXDrawDone();

    if (this->mFlag.onBit(1)) {
        OSSleepThread(&this->mThreadQueue);
    }
    bool b = false;
    if (BaseSystem::mConfigData->getXfbMgr()->mNumXfbs == 1) {
        s32 tick = OSGetTick();
        s32 lastTick = this->mLastTick;
        if (tick - lastTick > Display::sTickPeriod) {
            VIWaitForRetrace();
        }
    } else {
        b = false;
        if (this->field_0x98 == 1 && this->mScreenStateFlag.onBit(6)) {
            b = true;
        }

        this->waitForMsg(b);
    }

    if (b) {
        this->clearEFB();
        this->mScreenStateFlag.resetBit(6);
        GXDrawDone();
    } else {
        this->copyEFBtoXFB();
    }

    if (this->field_0x98 == 1) {
        this->calcS();
    }
    this->field_0xAC++;
    this->mFrameCount++;
    this->calcFrequency();
}

void AsyncDisplay::endFrame() {
    if (this->mScreenStateFlag.onBit(4)) {
        this->mScreenStateFlag.setBit(5);
    }
}

void AsyncDisplay::endRender() {
    if (this->mScreenStateFlag.onBit(5)) {
        this->mScreenStateFlag.setBit(6);
        this->mScreenStateFlag.resetBit(5);
    }
}

void AsyncDisplay::preVRetrace() {
    s32 tick = OSGetTick();
    this->mLastTick = tick;
    if (this->field_0x98 == 0) {
        u32 a8 = ++this->field_0xA8;
        if (a8 - this->field_0xA4 >= this->mMaxRetraces) {
            this->field_0xA4 = a8;
            this->mScreenStateFlag.setBit(3);
            OSSendMessage(&this->mMesgQueue, this->mMesgBuf, 0);
            if (this->field_0xAC > 0) {
                this->field_0xAC--;
            }
        }
        OSWakeupThread(&this->mThreadQueue);
    }

    if (this->mScreenStateFlag.onBit(3)) {
        s32 count = VIGetRetraceCount();
        if (count - this->mRetraceCount >= this->mMaxRetraces) {
            if (this->mScreenStateFlag.onBit(0)) {
                Video *video = BaseSystem::mConfigData->getVideo();
                VISetBlack(video->mFlag.offBit(0));
                if (video->mFlag.offBit(0)) {
                    video->mFlag.setBit(0);
                } else {
                    video->mFlag.resetBit(0);
                }
                this->mScreenStateFlag.resetBit(0);
            }
            XfbManager *xfb = BaseSystem::mConfigData->getXfbMgr();
            xfb->setNextFrameBuffer();
            s32 newCount = VIGetRetraceCount();
            this->mRetraceCount = newCount;
            this->mScreenStateFlag.resetBit(3);
        }
    }
}

void AsyncDisplay::clearEFB() {
    Video *video = BaseSystem::mConfigData->getVideo();
    GXRenderModeObj const *renderMode = video->pRenderMode;
    this->clearEFB(
        renderMode->fbWidth, renderMode->efbHeight, 0, 0, renderMode->fbWidth, renderMode->efbHeight, this->mClearColor
    );
}

u32 AsyncDisplay::getTickPerFrame() {
    if (this->field_0x98 == 1) {
        return Video::getTickPerVRetrace(0);
    } else {
        return Video::getTickPerVRetrace();
    }
}

void AsyncDisplay::clearEFB(u16 fbWidth, u16 fbHeight, u16 x, u16 y, u16 width, u16 height, nw4r::ut::Color color) {
    GXInitTexObj(&clear_z_tobj, &clear_z_TX, 4, 4, GX_TF_Z24X8, GX_REPEAT, GX_REPEAT, 0);
    GXInitTexObjLOD(&clear_z_tobj, GX_NEAR, GX_NEAR, 0.0, 0.0, 0.0, 0, 0, GX_ANISO_1);
    Mtx44 mat;
    C_MTXOrtho(mat, 0.0, fbHeight, 0.0, fbWidth, 0.0, 1.0f);
    GXSetProjection(mat, GX_ORTHOGRAPHIC);
    GXSetViewport(0.0, 0.0, fbWidth, fbHeight, 0.0, 1.0f);
    GXSetScissor(0, 0, fbWidth, fbHeight);
    Matrix34f::ident.loadPosMtx(0);
    GXSetCurrentMtx(0);
    GXClearVtxDesc();
    GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
    GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT);
    GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGB, GX_RGBX8, 0);
    GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_CLR_RGBA, GX_RGB565, 0);
    GXSetNumChans(0);
    GXSetChanCtrl(GX_COLOR0A0, false, GX_SRC_REG, GX_SRC_REG, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE);
    GXSetChanCtrl(GX_COLOR1A1, false, GX_SRC_REG, GX_SRC_REG, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE);
    GXSetNumTexGens(1);
    GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, 0x3c, false, 0x7d);
    GXLoadTexObj(&clear_z_tobj, GX_TEXMAP0);
    GXSetNumTevStages(1);
    GXSetTevColor(GX_TEVREG0, color);
    GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR_NULL);
    GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0);
    GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV);
    GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_A0);
    GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, 1, GX_TEVPREV);
    GXSetAlphaCompare(GX_ALWAYS, 0, GX_AOP_OR, GX_ALWAYS, 0);
    GXSetZTexture(2, GX_TF_Z24X8, 0);
    GXSetZCompLoc(false);
    GXSetBlendMode(GX_BM_NONE, GX_BL_ZERO, GX_BL_ZERO, GX_LO_NOOP);
    GXSetAlphaUpdate(true);
    GXSetDstAlpha(true, color.a);
    GXSetZMode(true, GX_ALWAYS, true);
    GXSetCullMode(GX_CULL_BACK);
    GXBegin(GX_QUADS, GX_VTXFMT0, 4);

    GXPosition2u16(x, y);
    GXPosition2u8(0, 0);
    u16 tmp = x + width;
    GXPosition2u16(tmp, y);
    GXPosition2u8(1, 0);
    u16 tmp2 = y + height;
    GXPosition2u16(tmp, tmp2);
    GXPosition2u8(1, 1);
    GXPosition2u16(x, tmp2);
    GXPosition2u8(0, 1);

    // GXEnd();

    GXSetZTexture(0, GX_TF_Z24X8, 0);
    GXSetZCompLoc(true);
    GXSetDstAlpha(false, color.a);
}

} // namespace EGG
