#include "z64skybox.h"
#include "global.h"
#include "buffers.h"

typedef struct SkyboxFaceParams {
    /* 0x00 */ s32 xStart;
    /* 0x04 */ s32 yStart;
    /* 0x08 */ s32 zStart;
    /* 0x0C */ s32 outerIncrVal;
    /* 0x10 */ s32 innerIncrVal;
} SkyboxFaceParams; // size = 0x14

// Converts texture coordinate values to s10.5 fixed point
#define TC(x) ((s16)((x)*32))

// Texture offsets for each face in the static segment buffer
u32 sSkybox128TexOffsets[] = {
    128 * 64 * 0, 128 * 64 * 1, 128 * 64 * 2, 128 * 64 * 3, 128 * 64 * 4, 128 * 64 * 4 + 128 * 128,
};

// Maps vertex buffer index to coordinate buffer index
u16 sSkybox128VtxBufIndices[] = {
    0, 2, 10, 12, 2, 4, 12, 14, 10, 12, 20, 22, 12, 14, 22, 24, 1, 3, 5, 6, 7, 8, 9, 11, 13, 15, 16, 17, 18, 19, 21, 23,
};

// S coordinates for all faces
s32 sSkybox128TexSCoords[] = {
    TC(62 * 0), TC(62 * 1), TC(62 * 2), TC(62 * 3), TC(62 * 4),
};

// T coordinates for top and bottom faces
s32 sSkybox128TexTCoordsXZ[] = {
    TC(62 * 0), TC(62 * 1), TC(62 * 2), TC(62 * 3), TC(62 * 4),
};

// T coordinates for side faces
s32 sSkybox128TexTCoords[] = {
    TC(62 * 0), TC(62 * 1), TC(62 * 2), TC(62 * 1), TC(62 * 0),
};

// Maps vertex index to vertex buffer index
s16 sSkybox128VtxIndices[] = {
    0,  16, 19, 18, 16, 1,  20, 19, 1,  17, 21, 20, 17, 5,  22, 21, 18, 19, 23, 2,  19, 20,
    3,  23, 20, 21, 24, 3,  21, 22, 7,  24, 2,  23, 26, 25, 23, 3,  27, 26, 3,  24, 28, 27,
    24, 7,  29, 28, 25, 26, 30, 10, 26, 27, 11, 30, 27, 28, 31, 11, 28, 29, 15, 31,
};

/**
 * Build the vertex and display list data for a skybox with 128x128 and 128x64 face textures.
 *
 * While the textures are nominally 128x128 (128x64) the 4x4 (4x2) tiles that cover it are only 31x31,
 * therefore only a 125x125 (125x63) area is ever sampled (125 = 4 * 31 + 1, the additional +1 accounts for bilinear
 * filtering)
 *
 * Each texture dimension is padded to the next power of 2, resulting in a final size of 128x128 (128x64)
 */
s32 Skybox_CalculateFace128(SkyboxContext* skyboxCtx, Vtx* roomVtx, s32 roomVtxStartIndex, s32 xStart, s32 yStart,
                            s32 zStart, s32 innerIncrVal, s32 outerIncrVal, s32 faceNum) {
    s32 i;
    s32 j;
    s32 k;
    s16 uls;
    s16 m;
    s32 outerIncr;
    u16 index;
    s16 ult;
    s16 l;
    s16 vtxIndex;
    s32 innerIncr;
    s32 xPoints[5 * 5];
    s32 yPoints[5 * 5];
    s32 zPoints[5 * 5];
    s32 tcS[5 * 5];
    s32 tcT[5 * 5];
    s32 pad;

    // Collect all vertex positions for this face
    switch (faceNum) {
        case 0: // xy plane
        case 1:
            outerIncr = yStart;

            for (i = 0, k = 0; k < 25; i++) {
                innerIncr = xStart;
                for (j = 0; j < 5; j++, k++) {
                    zPoints[k] = zStart;
                    xPoints[k] = innerIncr;
                    yPoints[k] = outerIncr;
                    tcS[k] = sSkybox128TexSCoords[j];
                    tcT[k] = sSkybox128TexTCoords[i];
                    innerIncr += innerIncrVal;
                }
                outerIncr += outerIncrVal;
            }
            break;

        case 2: // yz plane
        case 3:
            outerIncr = yStart;

            for (i = 0, k = 0; k < 25; i++) {
                innerIncr = zStart;
                for (j = 0; j < 5; j++, k++) {
                    xPoints[k] = xStart;
                    yPoints[k] = outerIncr;
                    zPoints[k] = innerIncr;
                    tcS[k] = sSkybox128TexSCoords[j];
                    tcT[k] = sSkybox128TexTCoords[i];
                    innerIncr += innerIncrVal;
                }
                outerIncr += outerIncrVal;
            }
            break;

        case 4: // xz plane
        case 5:
            outerIncr = zStart;

            for (i = 0, k = 0; k < 25; i++) {
                innerIncr = xStart;
                for (j = 0; j < 5; j++, k++) {
                    yPoints[k] = yStart;
                    xPoints[k] = innerIncr;
                    zPoints[k] = outerIncr;
                    tcS[k] = sSkybox128TexSCoords[j];
                    tcT[k] = sSkybox128TexTCoordsXZ[i];
                    innerIncr += innerIncrVal;
                }
                outerIncr += outerIncrVal;
            }
            break;

        default:
            break;
    }

    // Select gfx buffer
    skyboxCtx->gfx = &skyboxCtx->dListBuf[2 * faceNum][0];

    // Generate and load Vertex structures
    for (i = 0; i < ARRAY_COUNT(sSkybox128VtxBufIndices); i++) {
        index = sSkybox128VtxBufIndices[i];

        roomVtx[roomVtxStartIndex + i].v.ob[0] = xPoints[index];
        roomVtx[roomVtxStartIndex + i].v.ob[1] = yPoints[index];
        roomVtx[roomVtxStartIndex + i].v.ob[2] = zPoints[index];
        roomVtx[roomVtxStartIndex + i].v.flag = 0;
        roomVtx[roomVtxStartIndex + i].v.tc[0] = tcS[index];
        roomVtx[roomVtxStartIndex + i].v.tc[1] = tcT[index];
        roomVtx[roomVtxStartIndex + i].v.cn[1] = 0;
        roomVtx[roomVtxStartIndex + i].v.cn[2] = 0;
        roomVtx[roomVtxStartIndex + i].v.cn[0] = 255;
    }
    gSPVertex(skyboxCtx->gfx++, &roomVtx[roomVtxStartIndex], 32, 0);
    roomVtxStartIndex += i; // += 32

    // Cull the face if not within the viewing volume
    gSPCullDisplayList(skyboxCtx->gfx++, 0, 15);

    // Draw face, load the texture in several tiles to work around TMEM size limitations
    if ((faceNum == 4) || (faceNum == 5)) {
        // top/bottom faces, 128x128 texture
        ult = 0;
        for (vtxIndex = 0, l = 0; l < 4; l++, ult += 31) {
            for (uls = 0, m = 0; m < 4; m++, uls += 31, vtxIndex += 4) {
                gDPLoadMultiTile(skyboxCtx->gfx++,
                                 (uintptr_t)skyboxCtx->staticSegments[0] + sSkybox128TexOffsets[faceNum], 0,
                                 G_TX_RENDERTILE, G_IM_FMT_CI, G_IM_SIZ_8b, 128, 0, uls, ult, uls + 31, ult + 31, 0,
                                 G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOMIRROR | G_TX_WRAP,
                                 G_TX_NOMASK, G_TX_NOLOD);
                gDPLoadMultiTile(
                    skyboxCtx->gfx++, (uintptr_t)skyboxCtx->staticSegments[1] + sSkybox128TexOffsets[faceNum], 0x80, 1,
                    G_IM_FMT_CI, G_IM_SIZ_8b, 128, 0, uls, ult, uls + 31, ult + 31, 0, G_TX_NOMIRROR | G_TX_WRAP,
                    G_TX_NOMASK, G_TX_NOLOD, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOLOD);
                gSP1Quadrangle(skyboxCtx->gfx++, sSkybox128VtxIndices[vtxIndex + 1], sSkybox128VtxIndices[vtxIndex + 2],
                               sSkybox128VtxIndices[vtxIndex + 3], sSkybox128VtxIndices[vtxIndex + 0], 3);
            }
        }
    } else {
        // other faces, 128x64 texture
        ult = 0;
        for (vtxIndex = 0, l = 0; l < 2; l++, ult += 31) {
            for (uls = 0, m = 0; m < 4; m++, uls += 31, vtxIndex += 4) {
                gDPLoadMultiTile(skyboxCtx->gfx++,
                                 (uintptr_t)skyboxCtx->staticSegments[0] + sSkybox128TexOffsets[faceNum], 0,
                                 G_TX_RENDERTILE, G_IM_FMT_CI, G_IM_SIZ_8b, 128, 0, uls, ult, uls + 31, ult + 31, 0,
                                 G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOMIRROR | G_TX_WRAP,
                                 G_TX_NOMASK, G_TX_NOLOD);
                gDPLoadMultiTile(
                    skyboxCtx->gfx++, (uintptr_t)skyboxCtx->staticSegments[1] + sSkybox128TexOffsets[faceNum], 0x80, 1,
                    G_IM_FMT_CI, G_IM_SIZ_8b, 128, 0, uls, ult, uls + 31, ult + 31, 0, G_TX_NOMIRROR | G_TX_WRAP,
                    G_TX_NOMASK, G_TX_NOLOD, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOLOD);
                gSP1Quadrangle(skyboxCtx->gfx++, sSkybox128VtxIndices[vtxIndex + 1], sSkybox128VtxIndices[vtxIndex + 2],
                               sSkybox128VtxIndices[vtxIndex + 3], sSkybox128VtxIndices[vtxIndex + 0], 3);
            }
        }
        ult -= 31;
        for (l = 0; l < 2; l++, ult -= 31) {
            for (uls = 0, m = 0; m < 4; m++, uls += 31, vtxIndex += 4) {
                gDPLoadMultiTile(skyboxCtx->gfx++,
                                 (uintptr_t)skyboxCtx->staticSegments[0] + sSkybox128TexOffsets[faceNum], 0,
                                 G_TX_RENDERTILE, G_IM_FMT_CI, G_IM_SIZ_8b, 128, 0, uls, ult, uls + 31, ult + 31, 0,
                                 G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOMIRROR | G_TX_WRAP,
                                 G_TX_NOMASK, G_TX_NOLOD);
                gDPLoadMultiTile(
                    skyboxCtx->gfx++, (uintptr_t)skyboxCtx->staticSegments[1] + sSkybox128TexOffsets[faceNum], 0x80, 1,
                    G_IM_FMT_CI, G_IM_SIZ_8b, 128, 0, uls, ult, uls + 31, ult + 31, 0, G_TX_NOMIRROR | G_TX_WRAP,
                    G_TX_NOMASK, G_TX_NOLOD, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOLOD);
                gSP1Quadrangle(skyboxCtx->gfx++, sSkybox128VtxIndices[vtxIndex + 1], sSkybox128VtxIndices[vtxIndex + 2],
                               sSkybox128VtxIndices[vtxIndex + 3], sSkybox128VtxIndices[vtxIndex + 0], 3);
            }
        }
    }

    gSPEndDisplayList(skyboxCtx->gfx++);
    return roomVtxStartIndex;
}

SkyboxFaceParams sSkybox128FaceParams[] = {
    { -64, 64, -64, 32, -32 }, { 64, 64, 64, -32, -32 }, { -64, 64, 64, -32, -32 },
    { 64, 64, -64, 32, -32 },  { -64, 64, 64, 32, -32 }, { -64, -64, -64, 32, 32 },
};

/**
 * Computes the display list for a skybox with up to 6 faces, where the sides are 128x64 CI8 textures and the
 * top/bottom faces are 128x128 CI8 textures.
 */
void Skybox_Calculate128(SkyboxContext* skyboxCtx, s32 nFaces) {
    s32 roomVtxStartIndex = 0;
    s32 faceNum;

    for (faceNum = 0; faceNum < nFaces; faceNum++) {
        roomVtxStartIndex = Skybox_CalculateFace128(
            skyboxCtx, skyboxCtx->roomVtx, roomVtxStartIndex, sSkybox128FaceParams[faceNum].xStart,
            sSkybox128FaceParams[faceNum].yStart, sSkybox128FaceParams[faceNum].zStart,
            sSkybox128FaceParams[faceNum].outerIncrVal, sSkybox128FaceParams[faceNum].innerIncrVal, faceNum);
    }
}

void Skybox_Setup(GameState* gameState, SkyboxContext* skyboxCtx, s16 skyboxId) {
    PlayState* play = (PlayState*)gameState;
    size_t size;
    void* segment;

    skyboxCtx->rot.z = 0.0f;

    switch (skyboxId) {
        case SKYBOX_NORMAL_SKY:
            // Send a DMA request for the cloudy sky texture
            skyboxCtx->staticSegments[0] = gLoBuffer.skyboxBuffer;
            size = SEGMENT_ROM_SIZE(d2_cloud_static);
            segment = (void*)ALIGN8((uintptr_t)skyboxCtx->staticSegments[0] + size);
            DmaMgr_RequestSync(skyboxCtx->staticSegments[0], SEGMENT_ROM_START(d2_cloud_static), size);

            // Send a DMA request for the clear sky texture
            skyboxCtx->staticSegments[1] = segment;
            size = SEGMENT_ROM_SIZE(d2_fine_static);
            segment = (void*)ALIGN8((uintptr_t)segment + size);
            DmaMgr_RequestSync(skyboxCtx->staticSegments[1], SEGMENT_ROM_START(d2_fine_static), size);

            // Send a DMA request for the skybox palette
            skyboxCtx->palette = segment;
            size = SEGMENT_ROM_SIZE(d2_fine_pal_static);
            segment = (void*)ALIGN8((uintptr_t)segment + size);
            DmaMgr_RequestSync(skyboxCtx->palette, SEGMENT_ROM_START(d2_fine_pal_static), size);

            skyboxCtx->prim.r = 145;
            skyboxCtx->prim.g = 120;
            skyboxCtx->prim.b = 155;

            skyboxCtx->env.r = 40;
            skyboxCtx->env.g = 0;
            skyboxCtx->env.b = 40;

            // Inverted Stone Tower Temple and Inverted Stone Tower
            if ((play->sceneId == SCENE_F41) || (play->sceneId == SCENE_INISIE_R)) {
                skyboxCtx->rot.z = 3.15f;
            }
            break;

        case SKYBOX_2:
            break;

        default:
            break;
    }
}

void Skybox_Reload(PlayState* play, SkyboxContext* skyboxCtx, s16 skyboxId) {
    size_t size;

    switch (skyboxId) {
        case SKYBOX_NORMAL_SKY:
            osCreateMesgQueue(&skyboxCtx->loadQueue, skyboxCtx->loadMsg, ARRAY_COUNT(skyboxCtx->loadMsg));

            if (play->envCtx.skybox1Index == 0) {
                // Send a DMA request for the clear sky texture
                size = SEGMENT_ROM_SIZE(d2_fine_static);

                DmaMgr_RequestAsync(&skyboxCtx->skybox1DmaRequest, skyboxCtx->staticSegments[0],
                                    SEGMENT_ROM_START(d2_fine_static), size, 0, &skyboxCtx->loadQueue, NULL);
            } else {
                // Send a DMA request for the cloudy sky texture
                size = SEGMENT_ROM_SIZE(d2_cloud_static);

                DmaMgr_RequestAsync(&skyboxCtx->skybox1DmaRequest, skyboxCtx->staticSegments[0],
                                    SEGMENT_ROM_START(d2_cloud_static), size, 0, &skyboxCtx->loadQueue, NULL);
            }

            osRecvMesg(&skyboxCtx->loadQueue, NULL, OS_MESG_BLOCK);
            osCreateMesgQueue(&skyboxCtx->loadQueue, skyboxCtx->loadMsg, ARRAY_COUNT(skyboxCtx->loadMsg));

            if (play->envCtx.skybox2Index == 0) {
                // Send a DMA request for the clear sky texture
                size = SEGMENT_ROM_SIZE(d2_fine_static);

                DmaMgr_RequestAsync(&skyboxCtx->skybox2DmaRequest, skyboxCtx->staticSegments[1],
                                    SEGMENT_ROM_START(d2_fine_static), size, 0, &skyboxCtx->loadQueue, NULL);
            } else {
                // Send a DMA request for the cloudy sky texture
                size = SEGMENT_ROM_SIZE(d2_cloud_static);

                DmaMgr_RequestAsync(&skyboxCtx->skybox2DmaRequest, skyboxCtx->staticSegments[1],
                                    SEGMENT_ROM_START(d2_cloud_static), size, 0, &skyboxCtx->loadQueue, NULL);
            }

            osRecvMesg(&skyboxCtx->loadQueue, NULL, OS_MESG_BLOCK);
            osCreateMesgQueue(&skyboxCtx->loadQueue, skyboxCtx->loadMsg, ARRAY_COUNT(skyboxCtx->loadMsg));

            size = SEGMENT_ROM_SIZE(d2_fine_pal_static);

            // Send a DMA request for the skybox palette
            DmaMgr_RequestAsync(&skyboxCtx->paletteDmaRequest, skyboxCtx->palette,
                                SEGMENT_ROM_START(d2_fine_pal_static), size, 0, &skyboxCtx->loadQueue, NULL);

            osRecvMesg(&skyboxCtx->loadQueue, NULL, OS_MESG_BLOCK);
            break;

        default:
            break;
    }
}

void Skybox_Init(GameState* gameState, SkyboxContext* skyboxCtx, s16 skyboxId) {
    skyboxCtx->shouldDraw = false;
    skyboxCtx->rot.x = skyboxCtx->rot.y = skyboxCtx->rot.z = 0.0f;

    Skybox_Setup(gameState, skyboxCtx, skyboxId);

    if (skyboxId != SKYBOX_NONE) {
        skyboxCtx->dListBuf = THA_AllocTailAlign16(&gameState->tha, 12 * 150 * sizeof(Gfx));

        if (skyboxId == SKYBOX_CUTSCENE_MAP) {
            // Allocate enough space for the vertices for a 6 sided skybox (cube)
            skyboxCtx->roomVtx = THA_AllocTailAlign16(&gameState->tha, 6 * 32 * sizeof(Vtx));
            Skybox_Calculate128(skyboxCtx, 6);
        } else {
            // Allocate enough space for the vertices for a 5 sided skybox (bottom is missing)
            skyboxCtx->roomVtx = THA_AllocTailAlign16(&gameState->tha, 5 * 32 * sizeof(Vtx));
            Skybox_Calculate128(skyboxCtx, 5);
        }
    }
}
