#ifdef __WIIU__

#include "../../Window.h"

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <malloc.h>

#include <map>

#ifndef _LANGUAGE_C
#define _LANGUAGE_C
#endif
#include "PR/ultra64/gbi.h"

#include "gfx_cc.h"
#include "gfx_rendering_api.h"
#include "gfx_pc.h"
#include "gfx_wiiu.h"

#include <gx2/texture.h>
#include <gx2/draw.h>
#include <gx2/clear.h>
#include <gx2/state.h>
#include <gx2/swap.h>
#include <gx2/event.h>
#include <gx2/utils.h>
#include <gx2/mem.h>
#include <gx2/registers.h>
#include <gx2/display.h>
#include "gx2_shader_gen.h"

#include <proc_ui/procui.h>
#include <coreinit/memory.h>

#include "../ImGui/backends/wiiu/imgui_impl_gx2.h"

#define ALIGN(x, align) (((x) + ((align) -1)) & ~((align) -1))

struct ShaderProgram {
    struct ShaderGroup group;
    uint8_t num_inputs;
    bool used_textures[2];
    bool used_noise;
    uint32_t window_params_offset;
    uint32_t samplers_location[2];
};

struct Texture {
    GX2Texture texture;
    bool texture_uploaded;

    GX2Sampler sampler;
    bool sampler_set;

    // For ImGui rendering
    ImGui_ImplGX2_Texture imtex;
};

struct Framebuffer {
    GX2ColorBuffer color_buffer;
    bool colorBufferMem1;
    GX2DepthBuffer depth_buffer;
    bool depthBufferMem1;

    GX2Texture texture;
    GX2Sampler sampler;

    // For ImGui rendering
    ImGui_ImplGX2_Texture imtex;
};

static struct Framebuffer main_framebuffer;
static GX2DepthBuffer depthReadBuffer;
static struct Framebuffer *current_framebuffer;

static std::map<std::pair<uint64_t, uint32_t>, struct ShaderProgram> shader_program_pool;
static struct ShaderProgram *current_shader_program;

static struct Texture *current_texture;
static int current_tile;

// 96 Mb (should be more than enough to draw everything without waiting for the GPU)
#define DRAW_BUFFER_SIZE 0x6000000
static uint8_t *draw_buffer = nullptr;
static uint8_t *draw_ptr = nullptr;

static uint32_t frame_count;
static float current_noise_scale;
static FilteringMode current_filter_mode = FILTER_LINEAR;

static BOOL current_depth_test = TRUE;
static BOOL current_depth_write = TRUE;
static GX2CompareFunction current_depth_compare_function = GX2_COMPARE_FUNC_LESS;

static float current_viewport_x = 0.0f;
static float current_viewport_y = 0.0f;
static float current_viewport_width = WIIU_DEFAULT_FB_WIDTH;
static float current_viewport_height = WIIU_DEFAULT_FB_HEIGHT;

static uint32_t current_scissor_x = 0;
static uint32_t current_scissor_y = 0;
static uint32_t current_scissor_width = WIIU_DEFAULT_FB_WIDTH;
static uint32_t current_scissor_height = WIIU_DEFAULT_FB_HEIGHT;

static bool current_zmode_decal = false;
static bool current_use_alpha = false;

static inline GX2SamplerVar *GX2GetPixelSamplerVar(const GX2PixelShader *shader, const char *name)
{
    for (uint32_t i = 0; i < shader->samplerVarCount; ++i) {
        if (strcmp(name, shader->samplerVars[i].name) == 0) {
            return &shader->samplerVars[i];
        }
    }

    return nullptr;
}

static inline int32_t GX2GetPixelSamplerVarLocation(const GX2PixelShader *shader, const char *name)
{
    GX2SamplerVar *sampler = GX2GetPixelSamplerVar(shader, name);
    return sampler ? sampler->location : -1;
}

static inline int32_t GX2GetPixelUniformVarOffset(const GX2PixelShader *shader, const char *name)
{
    GX2UniformVar *uniform = GX2GetPixelUniformVar(shader, name);
    return uniform ? uniform->offset : -1;
}

static void gfx_gx2_init_framebuffer(struct Framebuffer *buffer, uint32_t width, uint32_t height) {
    memset(&buffer->color_buffer, 0, sizeof(GX2ColorBuffer));
    buffer->color_buffer.surface.use = GX2_SURFACE_USE_TEXTURE_COLOR_BUFFER_TV;
    buffer->color_buffer.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D;
    buffer->color_buffer.surface.width = width;
    buffer->color_buffer.surface.height = height;
    buffer->color_buffer.surface.depth = 1;
    buffer->color_buffer.surface.mipLevels = 1;
    buffer->color_buffer.surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8;
    buffer->color_buffer.surface.aa = GX2_AA_MODE1X;
    buffer->color_buffer.surface.tileMode = GX2_TILE_MODE_DEFAULT;
    buffer->color_buffer.viewNumSlices = 1;

    memset(&buffer->depth_buffer, 0, sizeof(GX2DepthBuffer));
    buffer->depth_buffer.surface.use = GX2_SURFACE_USE_DEPTH_BUFFER | GX2_SURFACE_USE_TEXTURE;
    buffer->depth_buffer.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D;
    buffer->depth_buffer.surface.width = width;
    buffer->depth_buffer.surface.height = height;
    buffer->depth_buffer.surface.depth = 1;
    buffer->depth_buffer.surface.mipLevels = 1;
    buffer->depth_buffer.surface.format = GX2_SURFACE_FORMAT_FLOAT_R32;
    buffer->depth_buffer.surface.aa = GX2_AA_MODE1X;
    buffer->depth_buffer.surface.tileMode = GX2_TILE_MODE_DEFAULT;
    buffer->depth_buffer.viewNumSlices = 1;
    buffer->depth_buffer.depthClear = 1.0f;
}

static struct GfxClipParameters gfx_gx2_get_clip_parameters(void) {
    return { false, false };
}

static void gfx_gx2_set_uniforms(struct ShaderProgram *prg) {
    if (prg->used_noise) {
        float window_params_array[2] = { current_noise_scale, (float) frame_count };

        GX2SetPixelUniformReg(prg->window_params_offset, 2, window_params_array);
    }
}

static void gfx_gx2_unload_shader(struct ShaderProgram *old_prg) {
    current_shader_program = nullptr;
}

static void gfx_gx2_load_shader(struct ShaderProgram *new_prg) {
    current_shader_program = new_prg;

    GX2SetFetchShader(&new_prg->group.fetchShader);
    GX2SetVertexShader(&new_prg->group.vertexShader);
    GX2SetPixelShader(&new_prg->group.pixelShader);

    gfx_gx2_set_uniforms(new_prg);
}

static struct ShaderProgram* gfx_gx2_create_and_load_new_shader(uint64_t shader_id0, uint32_t shader_id1) {
    struct CCFeatures cc_features;
    gfx_cc_get_features(shader_id0, shader_id1, &cc_features);

    struct ShaderProgram* prg = &shader_program_pool[std::make_pair(shader_id0, shader_id1)];

    printf("Generating shader: %016llx-%08x\n", shader_id0, shader_id1);
    if (gx2GenerateShaderGroup(&prg->group, &cc_features) != 0) {
        printf("Failed to generate shader\n");
        current_shader_program = nullptr;
        return nullptr;
    }

    prg->num_inputs = cc_features.num_inputs;
    prg->used_textures[0] = cc_features.used_textures[0];
    prg->used_textures[1] = cc_features.used_textures[1];

    gfx_gx2_load_shader(prg);

    prg->window_params_offset = GX2GetPixelUniformVarOffset(&prg->group.pixelShader, "window_params");
    prg->samplers_location[0] = GX2GetPixelSamplerVarLocation(&prg->group.pixelShader, "uTex0");
    prg->samplers_location[1] = GX2GetPixelSamplerVarLocation(&prg->group.pixelShader, "uTex1");

    prg->used_noise = cc_features.opt_alpha && cc_features.opt_noise;

    printf("Generated and loaded shader\n");

    return prg;
}

static struct ShaderProgram *gfx_gx2_lookup_shader(uint64_t shader_id0, uint32_t shader_id1) {
    auto it = shader_program_pool.find(std::make_pair(shader_id0, shader_id1));
    return it == shader_program_pool.end() ? nullptr : &it->second;
}

static void gfx_gx2_shader_get_info(struct ShaderProgram *prg, uint8_t *num_inputs, bool used_textures[2]) {
    *num_inputs = prg->num_inputs;
    used_textures[0] = prg->used_textures[0];
    used_textures[1] = prg->used_textures[1];
}

static uint32_t gfx_gx2_new_texture(void) {
    // some 32-bit trickery :P
    struct Texture *tex = (struct Texture *) calloc(1, sizeof(struct Texture));

    tex->imtex.Texture = &tex->texture;
    tex->imtex.Sampler = &tex->sampler;

    return (uint32_t) tex;
}

static void gfx_gx2_delete_texture(uint32_t texture_id) {
    struct Texture *tex = (struct Texture *) texture_id;

    if (tex->texture.surface.image) {
        free(tex->texture.surface.image);
    }

    free((void *) tex);
}

static void gfx_gx2_select_texture(int tile, uint32_t texture_id) {
    struct Texture *tex = (struct Texture *) texture_id;
    current_texture = tex;
    current_tile = tile;

    if (current_shader_program) {
        uint32_t sampler_location = current_shader_program->samplers_location[tile];

        if (tex->texture_uploaded) {
            GX2SetPixelTexture(&tex->texture, sampler_location);
        }

        if (tex->sampler_set) {
            GX2SetPixelSampler(&tex->sampler, sampler_location);
        }
    }
}

static void gfx_gx2_upload_texture(const uint8_t *rgba32_buf, uint32_t width, uint32_t height) {
    struct Texture *tex = current_texture;
    assert(tex);

    if ((tex->texture.surface.width != width) ||
        (tex->texture.surface.height != height) || 
        !tex->texture.surface.image) {

        if (tex->texture.surface.image) {
            free(tex->texture.surface.image);
            tex->texture.surface.image = nullptr;
        }

        memset(&tex->texture, 0, sizeof(GX2Texture));
        tex->texture.surface.use = GX2_SURFACE_USE_TEXTURE;
        tex->texture.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D;
        tex->texture.surface.width = width;
        tex->texture.surface.height = height;
        tex->texture.surface.depth = 1;
        tex->texture.surface.mipLevels = 1;
        tex->texture.surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8;
        tex->texture.surface.aa = GX2_AA_MODE1X;
        tex->texture.surface.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED;
        tex->texture.viewFirstMip = 0;
        tex->texture.viewNumMips = 1;
        tex->texture.viewFirstSlice = 0;
        tex->texture.viewNumSlices = 1;
        tex->texture.compMap = GX2_COMP_MAP(GX2_SQ_SEL_R, GX2_SQ_SEL_G, GX2_SQ_SEL_B, GX2_SQ_SEL_A);

        GX2CalcSurfaceSizeAndAlignment(&tex->texture.surface);
        GX2InitTextureRegs(&tex->texture);

        tex->texture.surface.image = memalign(tex->texture.surface.alignment, tex->texture.surface.imageSize);
    }

    uint8_t* buf = (uint8_t *) tex->texture.surface.image;
    assert(buf);

    for (uint32_t y = 0; y < height; ++y) {
        memcpy(buf + (y * tex->texture.surface.pitch * 4), rgba32_buf + (y * width * 4), width * 4);
    }

    GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, tex->texture.surface.image, tex->texture.surface.imageSize);

    if (current_shader_program) {
        GX2SetPixelTexture(&tex->texture, current_shader_program->samplers_location[current_tile]);
    }

    tex->texture_uploaded = true;
}

static GX2TexClampMode gfx_cm_to_gx2(uint32_t val) {
    switch (val) {
        case G_TX_NOMIRROR | G_TX_CLAMP:
            return GX2_TEX_CLAMP_MODE_CLAMP;
        case G_TX_MIRROR | G_TX_WRAP:
            return GX2_TEX_CLAMP_MODE_MIRROR;
        case G_TX_MIRROR | G_TX_CLAMP:
            return GX2_TEX_CLAMP_MODE_MIRROR_ONCE;
        case G_TX_NOMIRROR | G_TX_WRAP:
            return GX2_TEX_CLAMP_MODE_WRAP;
    }

    return GX2_TEX_CLAMP_MODE_WRAP;
}

static void gfx_gx2_set_sampler_parameters(int tile, bool linear_filter, uint32_t cms, uint32_t cmt) {
    struct Texture *tex = current_texture;
    assert(tex);

    current_tile = tile;

    GX2InitSampler(&tex->sampler, GX2_TEX_CLAMP_MODE_CLAMP,
        (linear_filter && current_filter_mode == FILTER_LINEAR) ? 
        GX2_TEX_XY_FILTER_MODE_LINEAR : GX2_TEX_XY_FILTER_MODE_POINT);

    GX2InitSamplerClamping(&tex->sampler, gfx_cm_to_gx2(cms), gfx_cm_to_gx2(cmt), GX2_TEX_CLAMP_MODE_WRAP);

    if (current_shader_program) {
        GX2SetPixelSampler(&tex->sampler, current_shader_program->samplers_location[tile]);
    }

    tex->sampler_set = true;
}

static void gfx_gx2_set_depth_test_and_mask(bool depth_test, bool z_upd) {
    current_depth_test = depth_test || z_upd;
    current_depth_write = z_upd;
    current_depth_compare_function = depth_test ? GX2_COMPARE_FUNC_LEQUAL : GX2_COMPARE_FUNC_ALWAYS;

    GX2SetDepthOnlyControl(current_depth_test, current_depth_write, current_depth_compare_function);
}

static void gfx_gx2_set_zmode_decal(bool zmode_decal) {
    current_zmode_decal = zmode_decal;
    if (zmode_decal) {
        GX2SetPolygonControl(GX2_FRONT_FACE_CCW, FALSE, FALSE, TRUE,
                             GX2_POLYGON_MODE_TRIANGLE, GX2_POLYGON_MODE_TRIANGLE,
                             TRUE, TRUE, FALSE);
        GX2SetPolygonOffset(-2.0f, -2.0f, -2.0f, -2.0f, 0.0f);
    } else {
        GX2SetPolygonControl(GX2_FRONT_FACE_CCW, FALSE, FALSE, FALSE,
                             GX2_POLYGON_MODE_TRIANGLE, GX2_POLYGON_MODE_TRIANGLE,
                             FALSE, FALSE, FALSE);
        GX2SetPolygonOffset(0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
    }
}

static void gfx_gx2_set_viewport(int x, int y, int width, int height) {
    uint32_t buffer_height = current_framebuffer->color_buffer.surface.height;

    current_viewport_x = x;
    current_viewport_y = buffer_height - y - height;
    current_viewport_width = width;
    current_viewport_height = height;

    GX2SetViewport(current_viewport_x, current_viewport_y, current_viewport_width, current_viewport_height, 0.0f, 1.0f);
}

static void gfx_gx2_set_scissor(int x, int y, int width, int height) {
    uint32_t buffer_height = current_framebuffer->color_buffer.surface.height;
    uint32_t buffer_width = current_framebuffer->color_buffer.surface.width;

    current_scissor_x = std::min((uint32_t) width, (uint32_t) x);
    current_scissor_y = std::min((uint32_t) height, buffer_height - y - height);
    current_scissor_width = std::min((uint32_t) width, buffer_width);
    current_scissor_height = std::min((uint32_t) height, buffer_height);

    GX2SetScissor(current_scissor_x, current_scissor_y, current_scissor_width, current_scissor_height);
}

static void gfx_gx2_set_use_alpha(bool use_alpha) {
    current_use_alpha = use_alpha;
    GX2SetColorControl(GX2_LOGIC_OP_COPY, use_alpha ? 0xff : 0, FALSE, TRUE);
}

static void gfx_gx2_draw_triangles(float buf_vbo[], size_t buf_vbo_len, size_t buf_vbo_num_tris) {
    if (!current_shader_program) {
        return;
    }

    size_t vbo_len = sizeof(float) * buf_vbo_len;

    if (draw_ptr + vbo_len >= draw_buffer + DRAW_BUFFER_SIZE) {
        printf("Waiting on GPU!!!\n");
        GX2DrawDone();
        draw_ptr = draw_buffer;
    }

    float* new_vbo = (float *) draw_ptr;
    draw_ptr += ALIGN(vbo_len, GX2_VERTEX_BUFFER_ALIGNMENT);

    OSBlockMove(new_vbo, buf_vbo, vbo_len, FALSE);
    GX2Invalidate(GX2_INVALIDATE_MODE_CPU_ATTRIBUTE_BUFFER, new_vbo, vbo_len);

    GX2SetAttribBuffer(0, vbo_len, current_shader_program->group.stride, new_vbo);
    GX2DrawEx(GX2_PRIMITIVE_MODE_TRIANGLES, 3 * buf_vbo_num_tris, 0, 1);
}

static void gfx_gx2_init(void) {
    // Init the default framebuffer
    gfx_gx2_init_framebuffer(&main_framebuffer, WIIU_DEFAULT_FB_WIDTH, WIIU_DEFAULT_FB_HEIGHT);

    GX2CalcSurfaceSizeAndAlignment(&main_framebuffer.color_buffer.surface);
    GX2InitColorBufferRegs(&main_framebuffer.color_buffer);

    main_framebuffer.color_buffer.surface.image = gfx_wiiu_alloc_mem1(main_framebuffer.color_buffer.surface.imageSize, main_framebuffer.color_buffer.surface.alignment);
    assert(main_framebuffer.color_buffer.surface.image);

    GX2CalcSurfaceSizeAndAlignment(&main_framebuffer.depth_buffer.surface);
    GX2InitDepthBufferRegs(&main_framebuffer.depth_buffer);

    main_framebuffer.depth_buffer.surface.image = gfx_wiiu_alloc_mem1(main_framebuffer.depth_buffer.surface.imageSize, main_framebuffer.depth_buffer.surface.alignment);
    assert(main_framebuffer.depth_buffer.surface.image);

    main_framebuffer.imtex.Texture = &main_framebuffer.texture;
    main_framebuffer.imtex.Sampler = &main_framebuffer.sampler;

    // create a linear aligned copy of the depth buffer to read pixels to
    memcpy(&depthReadBuffer, &main_framebuffer.depth_buffer, sizeof(GX2DepthBuffer));

    depthReadBuffer.surface.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED;
    depthReadBuffer.surface.width = 1;
    depthReadBuffer.surface.height = 1;

    GX2CalcSurfaceSizeAndAlignment(&depthReadBuffer.surface);

    depthReadBuffer.surface.image = gfx_wiiu_alloc_mem1(depthReadBuffer.surface.alignment, depthReadBuffer.surface.imageSize);
    assert(depthReadBuffer.surface.image);
    GX2Invalidate(GX2_INVALIDATE_MODE_CPU | GX2_INVALIDATE_MODE_DEPTH_BUFFER, depthReadBuffer.surface.image, depthReadBuffer.surface.imageSize);

    GX2SetColorBuffer(&main_framebuffer.color_buffer, GX2_RENDER_TARGET_0);
    GX2SetDepthBuffer(&main_framebuffer.depth_buffer);

    current_framebuffer = &main_framebuffer;

    // allocate draw buffer
    draw_buffer = (uint8_t *) memalign(GX2_VERTEX_BUFFER_ALIGNMENT, DRAW_BUFFER_SIZE);
    assert(draw_buffer);
    draw_ptr = draw_buffer;

    GX2SetRasterizerClipControl(TRUE, FALSE);

    GX2SetBlendControl(GX2_RENDER_TARGET_0,
                       GX2_BLEND_MODE_SRC_ALPHA,
                       GX2_BLEND_MODE_INV_SRC_ALPHA,
                       GX2_BLEND_COMBINE_MODE_ADD,
                       FALSE,
                       GX2_BLEND_MODE_ZERO,
                       GX2_BLEND_MODE_ZERO,
                       GX2_BLEND_COMBINE_MODE_ADD);
}

void gfx_gx2_shutdown(void) {
    if (has_foreground) {
        GX2DrawDone();

        if (depthReadBuffer.surface.image) {
            gfx_wiiu_free_mem1(depthReadBuffer.surface.image);
            depthReadBuffer.surface.image = nullptr;
        }

        if (main_framebuffer.color_buffer.surface.image) {
            gfx_wiiu_free_mem1(main_framebuffer.color_buffer.surface.image);
            main_framebuffer.color_buffer.surface.image = nullptr;
        }

        if (main_framebuffer.depth_buffer.surface.image) {
            gfx_wiiu_free_mem1(main_framebuffer.depth_buffer.surface.image);
            main_framebuffer.depth_buffer.surface.image = nullptr;
        }
    }

    if (draw_buffer) {
        free(draw_buffer);
        draw_buffer = nullptr;
        draw_ptr = nullptr;
    }
}

static void gfx_gx2_on_resize(void) {
}

static void gfx_gx2_start_frame(void) {
    // Restore state since ImGui modified it when rendering
    GX2SetViewport(current_viewport_x, current_viewport_y, current_viewport_width, current_viewport_height, 0.0f, 1.0f);
    GX2SetScissor(current_scissor_x, current_scissor_y, current_scissor_width, current_scissor_height);

    GX2SetColorControl(GX2_LOGIC_OP_COPY, current_use_alpha ? 0xff : 0, FALSE, TRUE);

    GX2SetBlendControl(GX2_RENDER_TARGET_0,
                       GX2_BLEND_MODE_SRC_ALPHA,
                       GX2_BLEND_MODE_INV_SRC_ALPHA,
                       GX2_BLEND_COMBINE_MODE_ADD,
                       FALSE,
                       GX2_BLEND_MODE_ZERO,
                       GX2_BLEND_MODE_ZERO,
                       GX2_BLEND_COMBINE_MODE_ADD);

    GX2SetDepthOnlyControl(current_depth_test, current_depth_write, current_depth_compare_function);

    if (current_zmode_decal) {
        GX2SetPolygonControl(GX2_FRONT_FACE_CCW, FALSE, FALSE, TRUE,
                             GX2_POLYGON_MODE_TRIANGLE, GX2_POLYGON_MODE_TRIANGLE,
                             TRUE, TRUE, FALSE);
        GX2SetPolygonOffset(-2.0f, -2.0f, -2.0f, -2.0f, 0.0f);
    } else {
        GX2SetPolygonControl(GX2_FRONT_FACE_CCW, FALSE, FALSE, FALSE,
                             GX2_POLYGON_MODE_TRIANGLE, GX2_POLYGON_MODE_TRIANGLE,
                             FALSE, FALSE, FALSE);
        GX2SetPolygonOffset(0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
    }

    frame_count++;
}

static void gfx_gx2_end_frame(void) {
    draw_ptr = draw_buffer;

    GX2CopyColorBufferToScanBuffer(&main_framebuffer.color_buffer, GX2_SCAN_TARGET_TV);
    GX2CopyColorBufferToScanBuffer(&main_framebuffer.color_buffer, GX2_SCAN_TARGET_DRC);
}

static void gfx_gx2_finish_render(void) {
}

static int gfx_gx2_create_framebuffer(void) {
    struct Framebuffer *buffer = (struct Framebuffer *) calloc(1, sizeof(struct Framebuffer));
    assert(buffer);

    GX2InitSampler(&buffer->sampler, GX2_TEX_CLAMP_MODE_WRAP, GX2_TEX_XY_FILTER_MODE_LINEAR);

    buffer->imtex.Texture = &buffer->texture;
    buffer->imtex.Sampler = &buffer->sampler;

    // some more 32-bit shenanigans :D
    return (int) buffer;
}

static void gfx_gx2_update_framebuffer_parameters(int fb, uint32_t width, uint32_t height, uint32_t msaa_level, bool opengl_invert_y, bool render_target, bool has_depth_buffer, bool can_extract_depth) {
    struct Framebuffer *buffer = (struct Framebuffer *) fb;

    // we don't support updating the main buffer (fb 0)
    if (!buffer) {
        return;
    }

    if (buffer->texture.surface.width == width &&
        buffer->texture.surface.height == height) {
        return;
    }

    // make sure the GPU no longer writes to the buffer
    GX2DrawDone();

    if (buffer->texture.surface.image) {
        if (buffer->colorBufferMem1) {
            gfx_wiiu_free_mem1(buffer->texture.surface.image);
        } else {
            free(buffer->texture.surface.image);
        }
        buffer->texture.surface.image = nullptr;
    }

    if (buffer->depth_buffer.surface.image) {
        if (buffer->depthBufferMem1) {
            gfx_wiiu_free_mem1(buffer->depth_buffer.surface.image);
        } else {
            free(buffer->depth_buffer.surface.image);
        }
        buffer->depth_buffer.surface.image = nullptr;
    }

    gfx_gx2_init_framebuffer(buffer, width, height);

    GX2CalcSurfaceSizeAndAlignment(&buffer->depth_buffer.surface);
    GX2InitDepthBufferRegs(&buffer->depth_buffer);

    buffer->depth_buffer.surface.image = gfx_wiiu_alloc_mem1(buffer->depth_buffer.surface.imageSize, buffer->depth_buffer.surface.alignment);
    // fall back to mem2
    if (!buffer->depth_buffer.surface.image) {
        buffer->depth_buffer.surface.image = memalign(buffer->depth_buffer.surface.alignment, buffer->depth_buffer.surface.imageSize);
        buffer->depthBufferMem1 = false;
    } else {
        buffer->depthBufferMem1 = true;
    }
    assert(buffer->depth_buffer.surface.image);

    GX2CalcSurfaceSizeAndAlignment(&buffer->color_buffer.surface);
    GX2InitColorBufferRegs(&buffer->color_buffer);

    memset(&buffer->texture, 0, sizeof(GX2Texture));
    buffer->texture.surface.use = GX2_SURFACE_USE_TEXTURE;
    buffer->texture.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D;
    buffer->texture.surface.width = width;
    buffer->texture.surface.height = height;
    buffer->texture.surface.depth = 1;
    buffer->texture.surface.mipLevels = 1;
    buffer->texture.surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8;
    buffer->texture.surface.aa = GX2_AA_MODE1X;
    buffer->texture.surface.tileMode = GX2_TILE_MODE_DEFAULT;
    buffer->texture.viewFirstMip = 0;
    buffer->texture.viewNumMips = 1;
    buffer->texture.viewFirstSlice = 0;
    buffer->texture.viewNumSlices = 1;
    buffer->texture.compMap = GX2_COMP_MAP(GX2_SQ_SEL_R, GX2_SQ_SEL_G, GX2_SQ_SEL_B, GX2_SQ_SEL_A);

    GX2CalcSurfaceSizeAndAlignment(&buffer->texture.surface);
    GX2InitTextureRegs(&buffer->texture);

    // the texture and color buffer share a buffer
    assert(buffer->color_buffer.surface.imageSize == buffer->texture.surface.imageSize);

    buffer->texture.surface.image = gfx_wiiu_alloc_mem1(buffer->texture.surface.imageSize, buffer->texture.surface.alignment);
    // fall back to mem2
    if (!buffer->texture.surface.image) {
        buffer->texture.surface.image = memalign(buffer->texture.surface.alignment, buffer->texture.surface.imageSize);
        buffer->colorBufferMem1 = false;
    } else {
        buffer->colorBufferMem1 = true;
    }
    assert(buffer->texture.surface.image);

    buffer->color_buffer.surface.image = buffer->texture.surface.image;
}

void gfx_gx2_start_draw_to_framebuffer(int fb, float noise_scale) {
    struct Framebuffer *buffer = (struct Framebuffer *) fb;

    // fb 0 = main buffer
    if (!buffer) {
        buffer = &main_framebuffer;
    }

    if (noise_scale != 0.0f) {
        current_noise_scale = 1.0f / noise_scale;
    }

    GX2SetColorBuffer(&buffer->color_buffer, GX2_RENDER_TARGET_0);
    GX2SetDepthBuffer(&buffer->depth_buffer);

    current_framebuffer = buffer;
}

void gfx_gx2_clear_framebuffer(void) {
    struct Framebuffer *buffer = current_framebuffer;

    GX2ClearColor(&buffer->color_buffer, 0.0f, 0.0f, 0.0f, 1.0f);
    GX2ClearDepthStencilEx(&buffer->depth_buffer, 
        buffer->depth_buffer.depthClear,
        buffer->depth_buffer.stencilClear, GX2_CLEAR_FLAGS_BOTH);
    
    gfx_wiiu_set_context_state();
}

void gfx_gx2_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) {
    struct Framebuffer *src_buffer = (struct Framebuffer *) fb_id_source;
    struct Framebuffer *target_buffer = (struct Framebuffer *) fb_id_target;

    // fb 0 = main buffer
    if (!src_buffer) {
        src_buffer = &main_framebuffer;
    }
    if (!target_buffer) {
        target_buffer = &main_framebuffer;
    }

    if (src_buffer->color_buffer.surface.aa == GX2_AA_MODE1X) {
        GX2CopySurface(&src_buffer->color_buffer.surface,
            src_buffer->color_buffer.viewMip,
            src_buffer->color_buffer.viewFirstSlice,
            &target_buffer->color_buffer.surface,
            target_buffer->color_buffer.viewMip,
            target_buffer->color_buffer.viewFirstSlice);
    } else {
        GX2ResolveAAColorBuffer(&src_buffer->color_buffer,
            &target_buffer->color_buffer.surface,
            target_buffer->color_buffer.viewMip,
            target_buffer->color_buffer.viewFirstSlice);
    }
}

void *gfx_gx2_get_framebuffer_texture_id(int fb_id) {
    struct Framebuffer *buffer = (struct Framebuffer *) fb_id;

    // fb 0 = main buffer
    if (!buffer) {
        buffer = &main_framebuffer;
    }

    return &buffer->imtex;
}

void gfx_gx2_select_texture_fb(int fb) {
    struct Framebuffer *buffer = (struct Framebuffer *) fb;
    assert(buffer);

    assert(current_shader_program);
    uint32_t location = current_shader_program->samplers_location[0];
    GX2SetPixelTexture(&buffer->texture, location);
    GX2SetPixelSampler(&buffer->sampler, location);
}

static std::unordered_map<std::pair<float, float>, uint16_t, hash_pair_ff> gfx_gx2_get_pixel_depth(int fb_id, const std::set<std::pair<float, float>>& coordinates) {
    struct Framebuffer *buffer = (struct Framebuffer *) fb_id;

    // fb 0 = main buffer
    if (!buffer) {
        buffer = &main_framebuffer;
    }

    std::unordered_map<std::pair<float, float>, uint16_t, hash_pair_ff> res;

    for (const auto& c : coordinates) {
        // bug? coordinates sometimes read from oob
        if ((c.first < 0.0f) || (c.first > (float) buffer->depth_buffer.surface.width)
            || (c.second < 0.0f) || (c.second > (float) buffer->depth_buffer.surface.height)) {
            res.emplace(c, 0);
            continue;
        }

        GX2Invalidate(GX2_INVALIDATE_MODE_CPU | GX2_INVALIDATE_MODE_DEPTH_BUFFER, depthReadBuffer.surface.image, depthReadBuffer.surface.imageSize);

        // copy the pixel to the depthReadBuffer
        GX2Rect srcRect = { 
            (int32_t) c.first,
            (int32_t) buffer->depth_buffer.surface.height - (int32_t) c.second,
            (int32_t) c.first + 1,
            (int32_t) (buffer->depth_buffer.surface.height - (int32_t) c.second) + 1
        };
        GX2Point dstPoint = { 0, 0 };
        GX2CopySurfaceEx(&buffer->depth_buffer.surface, 0, 0, &depthReadBuffer.surface, 0, 0, 1, &srcRect, &dstPoint);
        GX2DrawDone();

        gfx_wiiu_set_context_state();

        // read the pixel from the depthReadBuffer
        uint32_t tmp = __builtin_bswap32(*(uint32_t *)depthReadBuffer.surface.image);
        float val = *(float *)&tmp;

        res.emplace(c, val * 65532.0f);
    }

    return res;
}

void gfx_gx2_set_texture_filter(FilteringMode mode) {
    // three-point is not implemented in the shaders yet
    if (mode == FILTER_THREE_POINT) {
        mode = FILTER_LINEAR;
    }

    current_filter_mode = mode;
    gfx_texture_cache_clear();
}

FilteringMode gfx_gx2_get_texture_filter(void) {
    return current_filter_mode;
}

ImGui_ImplGX2_Texture* gfx_gx2_texture_for_imgui(uint32_t texture_id) {
    struct Texture *tex = (struct Texture *) texture_id;
    return &tex->imtex;
}

struct GfxRenderingAPI gfx_gx2_api = {
    gfx_gx2_get_clip_parameters,
    gfx_gx2_unload_shader,
    gfx_gx2_load_shader,
    gfx_gx2_create_and_load_new_shader,
    gfx_gx2_lookup_shader,
    gfx_gx2_shader_get_info,
    gfx_gx2_new_texture,
    gfx_gx2_select_texture,
    gfx_gx2_upload_texture,
    gfx_gx2_set_sampler_parameters,
    gfx_gx2_set_depth_test_and_mask,
    gfx_gx2_set_zmode_decal,
    gfx_gx2_set_viewport,
    gfx_gx2_set_scissor,
    gfx_gx2_set_use_alpha,
    gfx_gx2_draw_triangles,
    gfx_gx2_init,
    gfx_gx2_on_resize,
    gfx_gx2_start_frame,
    gfx_gx2_end_frame,
    gfx_gx2_finish_render,
    gfx_gx2_create_framebuffer,
    gfx_gx2_update_framebuffer_parameters,
    gfx_gx2_start_draw_to_framebuffer,
    gfx_gx2_clear_framebuffer,
    gfx_gx2_resolve_msaa_color_buffer,
    gfx_gx2_get_pixel_depth,
    gfx_gx2_get_framebuffer_texture_id,
    gfx_gx2_select_texture_fb,
    gfx_gx2_delete_texture,
    gfx_gx2_set_texture_filter,
    gfx_gx2_get_texture_filter
};

#endif
