#include "common.h"
#include "nu/nusys.h"
#include "ld_addrs.h"
#include "hud_element.h"
#include "sprite.h"
#include "overlay.h"

extern s32 gPauseBackgroundFade;

Vp D_80074200 = {
    .vp = {
        .vscale = {640, 480, 511, 0},
        .vtrans = {640, 480, 511, 0},
    }
};

Gfx D_80074210[] = {
    gsDPSetRenderMode(G_RM_OPA_SURF, G_RM_OPA_SURF2),
    gsDPSetCombineMode(G_CC_SHADE, G_CC_SHADE),
    gsDPSetColorDither(G_CD_BAYER),
    gsSPEndDisplayList(),
};

Gfx D_80074230[] = {
    gsSPViewport(&D_80074200),
    gsSPClearGeometryMode(G_ZBUFFER | G_SHADE | G_CULL_BOTH | G_FOG | G_LIGHTING | G_TEXTURE_GEN |
                          G_TEXTURE_GEN_LINEAR | G_LOD | G_SHADING_SMOOTH | G_CLIPPING | 0x0040F9FA),
    gsSPSetGeometryMode(G_ZBUFFER | G_SHADE | G_CULL_BACK | G_SHADING_SMOOTH),
    gsSPTexture(0, 0, 0, G_TX_RENDERTILE, G_OFF),
    gsSPEndDisplayList(),
};

void gfx_init_state(void) {
    gSPSegment(gMainGfxPos++, 0x00, 0x0);
    gSPDisplayList(gMainGfxPos++, OS_K0_TO_PHYSICAL(D_80074230));
    gSPDisplayList(gMainGfxPos++, OS_K0_TO_PHYSICAL(D_80074210));
}

s32 gfx_frame_filter_pass_0(const u16* frameBuffer0, const u16* frameBuffer1, s32 y, s32 x, Color_RGBA8* out) {
    s32 pixel = SCREEN_WIDTH * y + x;

    out->a = (frameBuffer1[pixel] >> 2) & 0xF;
    out->r = UNPACK_PAL_R(frameBuffer0[pixel]);
    out->g = UNPACK_PAL_G(frameBuffer0[pixel]);
    out->b = UNPACK_PAL_B(frameBuffer0[pixel]);
}

void gfx_frame_filter_pass_1(Color_RGBA8* filterBuf0, Color_RGBA8 filterBuf1, u16* out) {
    #define RGBA_BUF_SIZE 32
    Color_RGBA8 sp0;
    Color_RGBA8 sp8;
    Color_RGBA8 final;
    u8 rs[RGBA_BUF_SIZE];
    u8 gs[RGBA_BUF_SIZE];
    u8 bs[RGBA_BUF_SIZE];
    s32 i;

    sp8 = filterBuf1;
    sp0 = filterBuf1;

    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        rs[i] = 0;
        gs[i] = 0;
        bs[i] = 0;
    }

    for (i = 0; i < 6; i++) {
        if (filterBuf0[i].a == 8) {
            rs[filterBuf0[i].r]++;
            gs[filterBuf0[i].g]++;
            bs[filterBuf0[i].b]++;
        }
    }

    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        if (rs[i] != 0) {
            rs[i]--;
            break;
        }
    }
    for (; i < RGBA_BUF_SIZE; i++) {
        if (rs[i] != 0) {
            break;
        }
    }
    if (sp0.r > i) {
        sp0.r = i;
    }

    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        if (gs[i] != 0) {
            gs[i]--;
            break;
        }
    }
    for (; i < RGBA_BUF_SIZE; i++) {
        if (gs[i] != 0) {
            break;
        }
    }
    if (sp0.g > i) {
        sp0.g = i;
    }

    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        if (bs[i] != 0) {
            bs[i]--;
            break;
        }
    }
    for (; i < RGBA_BUF_SIZE; i++) {
        if (bs[i] != 0) {
            break;
        }
    }
    if (sp0.b > i) {
        sp0.b = i;
    }

    for (i = RGBA_BUF_SIZE - 1; i >= 0; i--) {
        if (rs[i] != 0) {
            break;
        }
    }
    if (sp8.r < i) {
        sp8.r = i;
    }

    for (i = RGBA_BUF_SIZE - 1; i >= 0; i--) {
        if (gs[i] != 0) {
            break;
        }
    }
    if (sp8.g < i) {
        sp8.g = i;
    }

    for (i = RGBA_BUF_SIZE - 1; i >= 0; i--) {
        if (bs[i] != 0) {
            break;
        }
    }
    if (sp8.b < i) {
        sp8.b = i;
    }

    final.r = (sp8.r + sp0.r) - filterBuf1.r;
    final.g = (sp8.g + sp0.g) - filterBuf1.g;
    final.b = (sp8.b + sp0.b) - filterBuf1.b;

    final.r = ((filterBuf1.r * filterBuf1.a) + (final.r * (8 - filterBuf1.a))) >> 3;
    final.g = ((filterBuf1.g * filterBuf1.a) + (final.g * (8 - filterBuf1.a))) >> 3;
    final.b = ((filterBuf1.b * filterBuf1.a) + (final.b * (8 - filterBuf1.a))) >> 3;

    *out = (final.r << 11) + (final.g << 6) + (final.b << 1) + 1;
    #undef RGBA_BUF_SIZE
}

void func_80027600(Color_RGBA8* arg0, s16* out) {
    #define RGBA_BUF_SIZE 32
    Color_RGBA8 final;
    u8 rs[RGBA_BUF_SIZE];
    u8 gs[RGBA_BUF_SIZE];
    u8 bs[RGBA_BUF_SIZE];
    s32 i;
    s32 sum;

    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        rs[i] = 0;
        gs[i] = 0;
        bs[i] = 0;
    }

    for (i = 0; i < 9; i++) {
        rs[arg0[i].r]++;
        gs[arg0[i].g]++;
        bs[arg0[i].b]++;
    }

    sum = 0;
    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        sum += rs[i];
        if (sum >= 5) {
            break;
        }
    }
    final.r = i;

    sum = 0;
    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        sum += gs[i];
        if (sum >= 5) {
            break;
        }
    }
    final.g = i;

    sum = 0;
    for (i = 0; i < RGBA_BUF_SIZE; i++) {
        sum += bs[i];
        if (sum >= 5) {
            break;
        }
    }
    final.b = i;

    *out = (final.r << 11) + (final.g << 6) + (final.b << 1) + 1;
    #undef RGBA_BUF_SIZE
}

void func_80027774(u16* frameBuffer0, u16* frameBuffer1, u16* zBuffer) {
    Color_RGBA8 filterBuf0[9];
    s32 x, y;

    for (y = 1; y < SCREEN_HEIGHT - 1; y++) {
        for (x = 1; x < SCREEN_WIDTH - 1; x++) {

            /*
            The application of gfx_frame_filter_pass_0 is done to the following pixels, where x is the current pixel.
               ...
               .x.
               ...
            */
            if (
                (((frameBuffer1[(SCREEN_WIDTH * y) + x - 1] >> 2) & 0xF) < 8) ||
                (((frameBuffer1[(SCREEN_WIDTH * y) + x + 0] >> 2) & 0xF) < 8) ||
                (((frameBuffer1[(SCREEN_WIDTH * y) + x + 1] >> 2) & 0xF) < 8))
            {
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y - 1, x - 1, &filterBuf0[0]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y - 1, x,     &filterBuf0[1]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y - 1, x + 1, &filterBuf0[2]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y    , x - 1, &filterBuf0[3]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y    , x,     &filterBuf0[4]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y    , x + 1, &filterBuf0[5]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y + 1, x - 1, &filterBuf0[6]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y + 1, x,     &filterBuf0[7]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y + 1, x + 1, &filterBuf0[8]);
                func_80027600(filterBuf0, &zBuffer[(SCREEN_WIDTH * y) + x]);
            } else {
                zBuffer[(SCREEN_WIDTH * y) + x] = frameBuffer0[(SCREEN_WIDTH * y) + x] | 1;
            }
        }
    }
}

// transfers the framebuffer into the depth buffer and applies filters
void gfx_transfer_frame_to_depth(u16* frameBuffer0, u16* frameBuffer1, u16* zBuffer) {
    Color_RGBA8 filterBuf0[6];
    Color_RGBA8 filterBuf1;
    s32 y;
    s32 x;

    for (y = 1; y < SCREEN_HEIGHT - 1; y++) {
        for (x = 2; x < SCREEN_WIDTH - 2; x++) {
            s32 pixel = SCREEN_WIDTH * y + x;

            /*
            The application of gfx_frame_filter_pass_0 is done to the following pixels, where x is the current pixel.
               . .
              . x .
               . .
            */
            if (((frameBuffer1[pixel] >> 2) & 0xF) < 8) {
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y - 1, x - 1, &filterBuf0[0]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y - 1, x + 1, &filterBuf0[1]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y,     x - 2, &filterBuf0[2]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y,     x + 2, &filterBuf0[3]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y + 1, x - 1, &filterBuf0[4]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y + 1, x + 1, &filterBuf0[5]);
                gfx_frame_filter_pass_0(frameBuffer0, frameBuffer1, y,     x,     &filterBuf1);
                gfx_frame_filter_pass_1(filterBuf0, filterBuf1, &zBuffer[pixel]);
            } else {
                // Don't apply any filters to the edges of the screen
                zBuffer[pixel] = frameBuffer0[pixel] | 1;
            }
        }
    }
}

void func_80027BAC(s32 arg0, s32 arg1) {
    s32 i;
    s32 temp = 24; // todo figure out why this is needed and can't be used elsewhere

    gDPPipeSync(gMainGfxPos++);
    gSPTexture(gMainGfxPos++, -1, -1, 0, G_TX_RENDERTILE, G_ON);
    gDPSetColorImage(gMainGfxPos++, G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH, arg1);
    gDPSetCycleType(gMainGfxPos++, G_CYC_COPY);
    gDPSetTexturePersp(gMainGfxPos++, G_TP_NONE);
    gDPSetTextureLUT(gMainGfxPos++, G_TT_NONE);
    gDPSetRenderMode(gMainGfxPos++, G_RM_NOOP, G_RM_NOOP2);
    gDPSetTextureFilter(gMainGfxPos++, G_TF_POINT);

    for (i = 0; i < 40; i++) {
        gDPLoadTextureTile(gMainGfxPos++, arg0 + (0xF00 * i), G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH,
                           SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH - 1, 5, 0, G_TX_NOMIRROR | G_TX_WRAP,
                           G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD);
        gSPTextureRectangle(gMainGfxPos++, 0, i * temp, 0x04FC, (i * 24) + 20, G_TX_RENDERTILE, 0, 0, 0x1000, 0x0400);
        gDPPipeSync(gMainGfxPos++);
    }
}

/// Logic for the drawing the scene background. In normal operation, it draws the regular background.
/// While opening pause menu, it does the following:
///  * Extracts coverage from the current framebuffer and saves it to nuGfxCfb[1] on the first frame.
///  * Copies the current framebuffer to the depth buffer to save it and applies a filter on the
///    saved framebuffer based on the saved coverage values one frame later.
///  * Draws the saved framebuffer to the current framebuffer while the pause screen is opened, fading it in over time.
void gfx_draw_background(void) {
    Camera* camera;
    s32 bgRenderState;
    s32 backgroundMinX;
    s32 backgroundMaxX;
    s32 backgroundMinY;
    s32 backgroundMaxY;
    s32 viewportStartX;
    s32 i;
    s32 a = SCREEN_COPY_TILE_HEIGHT << 2;

    gDPSetScissor(gMainGfxPos++, G_SC_NON_INTERLACE, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

    camera = &gCameras[gCurrentCameraID];
    bgRenderState = gGameStatusPtr->backgroundFlags & BACKGROUND_RENDER_STATE_MASK;

    switch (bgRenderState) {
        case BACKGROUND_RENDER_STATE_BEGIN_PAUSED:
            // Save coverage to nunGfxCfb[1] using the VISCVG render mode
            gDPPipeSync(gMainGfxPos++);
            gDPSetColorImage(gMainGfxPos++, G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH, nuGfxCfb[1]);
            gDPSetCycleType(gMainGfxPos++, G_CYC_1CYCLE);
            gDPSetBlendColor(gMainGfxPos++, 0x80, 0x80, 0x80, 0xFF);
            gDPSetPrimDepth(gMainGfxPos++, 0xFFFF, 0xFFFF);
            gDPSetDepthSource(gMainGfxPos++, G_ZS_PRIM);
            gDPSetRenderMode(gMainGfxPos++, G_RM_VISCVG, G_RM_VISCVG2);
            gDPFillRectangle(gMainGfxPos++, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
            gDPPipeSync(gMainGfxPos++);
            gDPSetDepthSource(gMainGfxPos++, G_ZS_PIXEL);
            gGameStatusPtr->backgroundFlags &= ~BACKGROUND_RENDER_STATE_MASK;
            gGameStatusPtr->backgroundFlags |= BACKGROUND_RENDER_STATE_FILTER_PAUSED;
            break;
        case BACKGROUND_RENDER_STATE_FILTER_PAUSED:
            // Save the framebuffer into the depth buffer and run a filter on it based on the saved coverage values
            gfx_transfer_frame_to_depth(nuGfxCfb[0], nuGfxCfb[1], nuGfxZBuffer); // applies filters to the framebuffer
            gPauseBackgroundFade = 0;
            gGameStatusPtr->backgroundFlags &= ~BACKGROUND_RENDER_STATE_MASK;
            gGameStatusPtr->backgroundFlags |= BACKGROUND_RENDER_STATE_SHOW_PAUSED;
            // fallthrough
        case BACKGROUND_RENDER_STATE_SHOW_PAUSED:
            // Draw the saved framebuffer to the background, fading in at a rate of 16 opacity per frame until reaching 128 opacity
            gPauseBackgroundFade += 16;
            if (gPauseBackgroundFade > 128) {
                gPauseBackgroundFade = 128;
            }

            gDPPipeSync(gMainGfxPos++);
            gDPSetScissor(gMainGfxPos++, G_SC_NON_INTERLACE, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
            gDPSetCycleType(gMainGfxPos++, G_CYC_FILL);
            gDPSetRenderMode(gMainGfxPos++, G_RM_NOOP, G_RM_NOOP2);
            gDPSetColorImage(gMainGfxPos++, G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH, nuGfxCfb_ptr);
            gDPSetFillColor(gMainGfxPos++, PACK_FILL_COLOR(0, 0, 0, 1));
            gDPFillRectangle(gMainGfxPos++, 0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);
            gDPSetCycleType(gMainGfxPos++, G_CYC_1CYCLE);
            gDPSetTexturePersp(gMainGfxPos++, G_TP_NONE);
            gDPSetTextureLUT(gMainGfxPos++, G_TT_NONE);
            gDPSetRenderMode(gMainGfxPos++, G_RM_OPA_SURF, G_RM_OPA_SURF2);
            /// @bug In 1-cycle mode, the two combiner cycles should be identical. Using Texel1 here in the second cycle,
            /// which is the actual cycle of the combiner used on hardware in 1-cycle mode, actually samples the next
            /// pixel's texel value instead of the current pixel's. This results in a one-pixel offset.
            gDPSetCombineMode(gMainGfxPos++, PM_CC_43, PM_CC_44);
            gDPSetPrimColor(gMainGfxPos++, 0, 0, 40, 40, 40, gPauseBackgroundFade);
            gDPSetTextureFilter(gMainGfxPos++, G_TF_POINT);

            for (i = 0; i < 40; i++) {
                gDPLoadTextureTile(gMainGfxPos++, nuGfxZBuffer + (i * SCREEN_WIDTH * SCREEN_COPY_TILE_HEIGHT), G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH,
                                   SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH - 1, SCREEN_COPY_TILE_HEIGHT - 1, 0, G_TX_NOMIRROR | G_TX_WRAP,
                                   G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD);
                /// @bug Due to the previous issue with the incorrect second cycle combiner, the devs added a 1-pixel offset to texture coordinates
                /// in this texrect to compensate for the combiner error.
                gSPTextureRectangle(gMainGfxPos++,
                                    // ulx, uly, lrx, lry
                                    0 << 2, i * a, SCREEN_WIDTH << 2, a + (i * (SCREEN_COPY_TILE_HEIGHT << 2)),
                                    // tile
                                    G_TX_RENDERTILE,
                                    // s, t, dsdx, dtdy
                                    -1 << 5, 0 << 5, 1 << 10, 1 << 10);
                gDPPipeSync(gMainGfxPos++);
            }
            break;
        default:
            // Draw the scene's background as normal
            if (gOverrideFlags & GLOBAL_OVERRIDES_DISABLE_DRAW_FRAME) {
                gDPSetColorImage(gMainGfxPos++, G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH, osVirtualToPhysical(nuGfxCfb_ptr));
                return;
            }

            gDPSetDepthImage(gMainGfxPos++, OS_K0_TO_PHYSICAL(nuGfxZBuffer));
            gDPSetCycleType(gMainGfxPos++, G_CYC_FILL);
            gDPSetRenderMode(gMainGfxPos++, G_RM_NOOP, G_RM_NOOP2);
            gDPSetColorImage(gMainGfxPos++, G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH, OS_K0_TO_PHYSICAL(nuGfxZBuffer));
            gDPSetFillColor(gMainGfxPos++, PACK_FILL_DEPTH(G_MAXFBZ, 0));
            gDPFillRectangle(gMainGfxPos++, 0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);
            gDPPipeSync(gMainGfxPos++);
            gDPSetColorImage(gMainGfxPos++, G_IM_FMT_RGBA, G_IM_SIZ_16b, SCREEN_WIDTH, osVirtualToPhysical(nuGfxCfb_ptr));
            gDPSetFillColor(gMainGfxPos++, PACK_FILL_COLOR(camera->bgColor[0], camera->bgColor[1], camera->bgColor[2], 1));

            backgroundMinX = gGameStatusPtr->backgroundMinX;
            backgroundMaxX = backgroundMinX + gGameStatusPtr->backgroundMaxX;
            backgroundMinY = gGameStatusPtr->backgroundMinY;
            backgroundMaxY = backgroundMinY + gGameStatusPtr->backgroundMaxY;
            viewportStartX = camera->viewportStartX;

            if (backgroundMinX < viewportStartX) {
                backgroundMinX = viewportStartX;
            }

            if (backgroundMinY < camera->viewportStartY) {
                backgroundMinY = camera->viewportStartY;
            }

            if (backgroundMaxX > viewportStartX + camera->viewportW) {
                backgroundMaxX = viewportStartX + camera->viewportW;
            }

            if (backgroundMaxY > camera->viewportStartY + camera->viewportH) {
                backgroundMaxY = camera->viewportStartY + camera->viewportH;
            }

            if (backgroundMinX < 0) {
                backgroundMinX = 0;
            }

            if (backgroundMinY < 0) {
                backgroundMinY = 0;
            }

            if (backgroundMaxX < 1) {
                backgroundMaxX = 1;
            }

            if (backgroundMaxY < 1) {
                backgroundMaxY = 1;
            }

            if (backgroundMinX > SCREEN_WIDTH - 1) {
                backgroundMinX = SCREEN_WIDTH - 1;
            }

            if (backgroundMinY > SCREEN_HEIGHT - 1) {
                backgroundMinY = SCREEN_HEIGHT - 1;
            }

            if (backgroundMaxX > SCREEN_WIDTH) {
                backgroundMaxX = SCREEN_WIDTH;
            }

            if (backgroundMaxY > SCREEN_HEIGHT) {
                backgroundMaxY = SCREEN_HEIGHT;
            }

            if (!(gGameStatusPtr->backgroundFlags & BACKGROUND_FLAG_TEXTURE)) {
                gDPFillRectangle(gMainGfxPos++, backgroundMinX, backgroundMinY, backgroundMaxX - 1, backgroundMaxY - 1);
            } else {
                appendGfx_background_texture();
            }

            gDPPipeSync(gMainGfxPos++);
            gDPSetCycleType(gMainGfxPos++, G_CYC_FILL);
            gDPSetRenderMode(gMainGfxPos++, G_RM_NOOP, G_RM_NOOP2);
            gDPSetFillColor(gMainGfxPos++, PACK_FILL_COLOR(0, 0, 0, 1));
            gDPPipeSync(gMainGfxPos++);

            if (backgroundMinY > 0) {
                gDPFillRectangle(gMainGfxPos++, 0, 0, SCREEN_WIDTH - 1, backgroundMinY - 1);
                gDPNoOp(gMainGfxPos++);
            }

            if (backgroundMinX > 0) {
                gDPFillRectangle(gMainGfxPos++, 0, backgroundMinY, backgroundMinX - 1, backgroundMaxY - 1);
                gDPNoOp(gMainGfxPos++);
            }

            if (backgroundMaxX < SCREEN_WIDTH) {
                gDPFillRectangle(gMainGfxPos++, backgroundMaxX, backgroundMinY, SCREEN_WIDTH - 1, backgroundMaxY - 1);
                gDPNoOp(gMainGfxPos++);
            }

            if (backgroundMaxY < SCREEN_HEIGHT) {
                gDPFillRectangle(gMainGfxPos++, 0, backgroundMaxY, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);
                gDPNoOp(gMainGfxPos++);
            }
            break;
    }
    gDPPipeSync(gMainGfxPos++);
}
