#include "gx/CGxDevice.hpp"
#include "gx/Gx.hpp"
#include "gx/Shader.hpp"
#include "gx/texture/CGxTex.hpp"
#include "gx/Texture.hpp"
#include "gx/RenderState.hpp"
#include "gx/Transform.hpp"
#include "gx/Draw.hpp"
#include "util/SFile.hpp"
#include "event/Input.hpp"
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <limits>
#include <new>
#include <storm/Error.hpp>
#include <storm/Memory.hpp>

#if defined(WHOA_SYSTEM_WIN)
    #include "gx/d3d/CGxDeviceD3d.hpp"
#endif

#if defined(WHOA_BUILD_GLSDL)
    #include "gx/glsdl/CGxDeviceGLSDL.hpp"
#endif

#if defined(WHOA_SYSTEM_MAC)
    #include "gx/gll/CGxDeviceGLL.hpp"
#endif

uint32_t CGxDevice::s_alphaRef[] = {
    0,      // GxBlend_Opaque
    224,    // GxBlend_AlphaKey
    1,      // GxBlend_Alpha
    1,      // GxBlend_Add
    1,      // GxBlend_Mod
    1,      // GxBlend_Mod2x
    1,      // GxBlend_ModAdd
    0,      // GxBlend_InvSrcAlphaAdd
    0,      // GxBlend_InvSrcAlphaOpaque
    0,      // GxBlend_SrcAlphaOpaque
    0,      // GxBlend_NoAlphaAdd
    0       // GxBlend_ConstantAlpha
};

C3Vector CGxDevice::s_pointScaleIdentity = { 1.0f, 0.0f, 0.0f };

uint32_t CGxDevice::s_primVtxAdjust[] = {
    0,      // GxPrim_Points
    0,      // GxPrim_Lines
    1,      // GxPrim_LineStrip
    0,      // GxPrim_Triangles
    2,      // GxPrim_TriangleStrip
    2,      // GxPrim_TriangleFan
};

uint32_t CGxDevice::s_primVtxDiv[] = {
    1,      // GxPrim_Points
    2,      // GxPrim_Lines
    1,      // GxPrim_LineStrip
    3,      // GxPrim_Triangles
    1,      // GxPrim_TriangleStrip
    1,      // GxPrim_TriangleFan
};

ShaderConstants CGxDevice::s_shadowConstants[2];

uint32_t CGxDevice::s_streamPoolSize[] = {
    0x2C0000,   // GxPoolTarget_Vertex
    0x40000     // GxPoolTarget_Index
};

uint32_t CGxDevice::s_texFormatBitDepth[] = {
    0,      // GxTex_Unknown
    32,     // GxTex_Abgr8888
    32,     // GxTex_Argb8888
    16,     // GxTex_Argb4444
    16,     // GxTex_Argb1555
    16,     // GxTex_Rgb565
    4,      // GxTex_Dxt1
    8,      // GxTex_Dxt3
    8,      // GxTex_Dxt5
    16,     // GxTex_Uv88
    32,     // GxTex_Gr1616F
    32,     // GxTex_R32F
    32      // GxTex_D24X8
};

uint32_t CGxDevice::s_texFormatBytesPerBlock[] = {
    0,      // GxTex_Unknown
    4,      // GxTex_Abgr8888
    4,      // GxTex_Argb8888
    2,      // GxTex_Argb4444
    2,      // GxTex_Argb1555
    2,      // GxTex_Rgb565
    8,      // GxTex_Dxt1
    16,     // GxTex_Dxt3
    16,     // GxTex_Dxt5
    2,      // GxTex_Uv88
    4,      // GxTex_Gr1616F
    4,      // GxTex_R32F
    4       // GxTex_D24X8
};

CGxShader* CGxDevice::s_uiVertexShader = nullptr;
CGxShader* CGxDevice::s_uiPixelShader = nullptr;

void CGxDevice::LogOpen() {
    // TODO
}

void CGxDevice::LogClose() {
    // TODO
}

void CGxDevice::Log(const char* format, ...) {
    // TODO
}

void CGxDevice::Log(const CGxFormat& format) {
    // TODO
}

#if defined(WHOA_SYSTEM_WIN)
CGxDevice* CGxDevice::NewD3d() {
    auto m = SMemAlloc(sizeof(CGxDeviceD3d), __FILE__, __LINE__, 0x0);
    return new (m) CGxDeviceD3d();
}

CGxDevice* CGxDevice::NewD3d9Ex() {
    // TODO
    return nullptr;
}
#endif

#if defined(WHOA_SYSTEM_MAC)
CGxDevice* CGxDevice::NewGLL() {
    auto m = SMemAlloc(sizeof(CGxDeviceGLL), __FILE__, __LINE__, 0x0);
    return new (m) CGxDeviceGLL();
}
#endif

CGxDevice* CGxDevice::NewOpenGl() {
    return nullptr;
}

#if defined(WHOA_BUILD_GLSDL)
CGxDevice* CGxDevice::NewGLSDL() {
    auto m = SMemAlloc(sizeof(CGxDeviceGLSDL), __FILE__, __LINE__, 0x0);
    return new (m) CGxDeviceGLSDL();
}
#endif

uint32_t CGxDevice::PrimCalcCount(EGxPrim primType, uint32_t count) {
    auto div = CGxDevice::s_primVtxDiv[primType];
    if (div != 1) {
        count /= div;
    }

    return count - CGxDevice::s_primVtxAdjust[primType];
}

void CGxDevice::ICursorUpdate(EGxTexCommand command, uint32_t width, uint32_t height, uint32_t face, uint32_t level, void* userArg, uint32_t& texelStrideInBytes, const void*& texels) {
    // TODO
    if (command == GxTex_Latch) {
        auto device = static_cast<CGxDevice*>(userArg);

        texelStrideInBytes = 0x80;
        texels = device->m_cursor;
    }
}

CGxDevice::CGxDevice() {
    // TODO
    // - implement rest of constructor

    this->IRsInit();

    // Set default viewport
    this->m_viewport.x = { 0.0f, 1.0f };
    this->m_viewport.y = { 0.0f, 1.0f };
    this->m_viewport.z = { 0.0f, 1.0f };

    // Turn on all master enables
    this->m_appMasterEnables = 511;
    this->m_hwMasterEnables = 511;

    this->ShaderConstantsClear();
}

CGxBuf* CGxDevice::BufCreate(CGxPool* pool, uint32_t itemSize, uint32_t itemCount, uint32_t index) {
    auto m = SMemAlloc(sizeof(CGxBuf), __FILE__, __LINE__, 0x0);
    auto buf = new (m) CGxBuf(pool, itemSize, itemCount, index);

    pool->m_bufList.LinkToTail(buf);

    return buf;
}

char* CGxDevice::BufLock(CGxBuf* buf) {
    buf->unk1E = 1;
    buf->unk1F = 0;

    this->m_bufLocked[buf->m_pool->m_target] = buf;

    return nullptr;
}

CGxBuf* CGxDevice::BufStream(EGxPoolTarget target, uint32_t itemSize, uint32_t itemCount) {
    CGxBuf* buf = this->m_streamBufs[target];
    CGxPool* pool = buf->m_pool;

    if (pool && pool->m_size < itemSize * itemCount) {
        this->PoolSizeSet(pool, itemSize * itemCount);
    }

    buf->m_itemSize = itemSize;
    buf->m_itemCount = itemCount;
    buf->m_size = itemSize * itemCount;
    buf->unk1C = 0;

    return buf;
}

int32_t CGxDevice::BufUnlock(CGxBuf* buf, uint32_t size) {
    this->m_bufLocked[buf->m_pool->m_target] = nullptr;

    return 1;
}

void CGxDevice::BufData(CGxBuf* buf, const void* data, size_t size, uintptr_t offset) {
    buf->unk1E = 1;
    buf->unk1F = 0;
}

const CGxCaps& CGxDevice::Caps() const {
    return this->m_caps;
}

int32_t CGxDevice::DeviceCreate(int32_t (*windowProc)(void* window, uint32_t message, uintptr_t wparam, intptr_t lparam), const CGxFormat& format) {
    this->m_windowProc = windowProc;

    return this->DeviceSetFormat(format);
}

void CGxDevice::DeviceCreatePools() {
    this->m_vertexPool = this->PoolCreate(
        GxPoolTarget_Vertex,
        GxPoolUsage_Stream,
        CGxDevice::s_streamPoolSize[GxPoolTarget_Vertex],
        GxPoolHintBit_Unk3,
        "Stream_vtx"
    );

    this->m_indexPool = this->PoolCreate(
        GxPoolTarget_Index,
        GxPoolUsage_Stream,
        CGxDevice::s_streamPoolSize[GxPoolTarget_Index],
        GxPoolHintBit_Unk3,
        "Stream_idx"
    );
}

void CGxDevice::DeviceCreateStreamBufs() {
    this->m_streamBufs[GxPoolTarget_Vertex] = this->BufCreate(this->m_vertexPool, 0, 0, 0);
    this->m_streamBufs[GxPoolTarget_Index] = this->BufCreate(this->m_indexPool, 0, 0, 0);
}

const CRect& CGxDevice::DeviceCurWindow() {
    return this->m_curWindowRect;
}

int32_t CGxDevice::DeviceSetFormat(const CGxFormat& format) {
    memcpy(&this->m_format, &format, sizeof(this->m_format));

    return 1;
}

void CGxDevice::DeviceSetCurWindow(const CRect& rect) {
    this->m_curWindowRect = rect;
}

void CGxDevice::DeviceSetDefWindow(const CRect& rect) {
    this->m_defWindowRect = rect;
    this->DeviceSetCurWindow(rect);
}

const CRect& CGxDevice::DeviceDefWindow() {
    return this->m_defWindowRect;
}

void CGxDevice::ICursorCreate(const CGxFormat& format) {
    int32_t hardwareCursor = format.hwCursor && this->m_caps.m_hardwareCursor;

    this->m_hardwareCursor = hardwareCursor;

    // If hardware cursor is disabled, and there is no cursor texture yet, create one
    if (!hardwareCursor && this->m_cursorTexture == nullptr) {
        // default flags?
        CGxTexFlags cursorTextureFlags;

        // Create a 32x32 cursor texture
        GxTexCreate(
            32,
            32,
            GxTex_Argb8888,
            cursorTextureFlags,
            reinterpret_cast<void*>(this),
            CGxDevice::ICursorUpdate,
            this->m_cursorTexture
        );
    }
}

void CGxDevice::ICursorDestroy() {
    if (this->m_cursorTexture) {
        GxTexDestroy(this->m_cursorTexture);
        this->m_cursorTexture = nullptr;
    }
}

void CGxDevice::ICursorDraw() {
    if (!this->m_cursorVisible) {
        return;
    }

    if (this->m_hardwareCursor) {
        return;
    }

    int32_t mouseX;
    int32_t mouseY;
    OsInputGetMousePosition(&mouseX, &mouseY);

    if (mouseX <= -1 || mouseY <= -1 || mouseX >= this->m_curWindowRect.maxX || mouseY >= this->m_curWindowRect.maxY) {
        return;
    }

    GxRsPush();
    // Turn off everything
    GxRsSet(GxRs_PolygonOffset, 0);
    GxRsSet(GxRs_NormalizeNormals, 0);
    GxRsSet(GxRs_BlendingMode, GxBlend_AlphaKey);
    GxRsSetAlphaRef();
    GxRsSet(GxRs_Lighting, 0);
    GxRsSet(GxRs_Fog, 0);
    GxRsSet(GxRs_DepthTest, 0);
    GxRsSet(GxRs_DepthWrite, 0);
    GxRsSet(GxRs_ColorWrite, 15);
    GxRsSet(GxRs_Culling, 0);
    GxRsSet(GxRs_ClipPlaneMask, 0);
    GxRsSet(GxRs_Texture0, this->m_cursorTexture);
    GxRsSet(GxRs_Texture1, static_cast<CGxTex*>(nullptr));
    GxRsSet(GxRs_ColorOp0, 0);
    GxRsSet(GxRs_AlphaOp0, 0);
    GxRsSet(GxRs_TexGen0, 0);
    GxRsSet(GxRs_Unk61, 0);

    C44Matrix identity;
    GxXformPush(GxXform_World, identity);

    float cursorDepth = 1.0f;

    C44Matrix projection;
	// Workaround for software cursor
	// C44Matrix projection(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0.002, 0, -1, -1, 0, 1);
    // this->XformSetProjection(projection);

    if (!this->StereoEnabled() ||
        (CGxDevice::s_uiVertexShader == 0 || !s_uiVertexShader->Valid()) ||
        (CGxDevice::s_uiPixelShader == 0 || !s_uiPixelShader->Valid())) {
        // Disable shaders
        GxRsSet(GxRs_VertexShader, static_cast<CGxShader*>(nullptr));
        GxRsSet(GxRs_PixelShader, static_cast<CGxShader*>(nullptr));
    } else {
        cursorDepth = this->m_cursorDepth;

        float minX, maxX, minY, maxY, minZ, maxZ;
        GxXformViewport(minX, maxX, minY, maxY, minZ, maxZ);

        GxXformProjection(projection);

        C44Matrix mProj;
        mProj.a0 = 2.0f / (maxX - minX);
        mProj.b0 = 0.0f;
        mProj.c0 = 0.0f;
        mProj.d0 = -((minX + maxX) / (maxX - minX));
        mProj.a1 = 0.0f;
        mProj.b1 = 2.0f / (maxY - minY);
        mProj.c1 = 0.0f;
        mProj.d1 = -((minY + maxY) / (maxY - minY));
        mProj.a2 = 0.0f;
        mProj.b2 = 0.0f;
        mProj.c2 = 1.00008f;
        mProj.d2 = -0.400016f;
        mProj.a3 = 0.0f;
        mProj.b3 = 0.0f;
        mProj.c3 = 1.0f;
        mProj.d3 = 0.0f;
        GxXformSetProjection(mProj);

        GxRsSet(GxRs_VertexShader, CGxDevice::s_uiVertexShader);
        GxRsSet(GxRs_PixelShader, CGxDevice::s_uiPixelShader);

        C44Matrix transposition;
        GxXformProjNativeTranspose(transposition);
        GxShaderConstantsSet(GxSh_Vertex, 0, reinterpret_cast<float*>(&transposition), 0);
    }

    auto buffer = GxBufStream(GxPoolTarget_Vertex, sizeof(CGxVertexPCT), 4);
    auto vertices = reinterpret_cast<CGxVertexPCT*>(GxBufLock(buffer));

    if (!vertices) {
        return;
    }

    auto scaleX = this->m_curWindowRect.maxX > 0.0f ? 1.0f / this->m_curWindowRect.maxX : 0.0f;
    auto scaleY = this->m_curWindowRect.maxY > 0.0f ? 1.0f / this->m_curWindowRect.maxY : 0.0f;

    mouseX -= this->m_cursorHotspotX;
    mouseY -= this->m_cursorHotspotY;

    auto minX = std::abs(static_cast<float>(mouseX)) * scaleX;
    auto maxX = std::abs(static_cast<float>(mouseX + 32)) * scaleX;

    auto minY = 1.0f - (std::abs(static_cast<float>(mouseY)) * scaleY);
    auto maxY = 1.0f - (std::abs(static_cast<float>(mouseY + 32)) * scaleY);

    if (this->m_api == GxApi_D3d9 || this->m_api == GxApi_D3d9Ex) {
        minX -= (scaleX * 0.5f);
        maxX += (scaleX * 0.5f);
        minY -= (scaleY * 0.5f);
        maxY += (scaleY * 0.5f);
    }

    // Vertex coordinates
    vertices[0].p.x = minX;
    vertices[0].p.y = minY;
    vertices[0].p.z = cursorDepth;

    vertices[1].p.x = minX;
    vertices[1].p.y = maxY;
    vertices[1].p.z = cursorDepth;

    vertices[2].p.x = maxX;
    vertices[2].p.y = minY;
    vertices[2].p.z = cursorDepth;

    vertices[3].p.x = maxX;
    vertices[3].p.y = maxY;
    vertices[3].p.z = cursorDepth;

    // Color values
    vertices[0].c = { 0xFF, 0xFF, 0xFF, 0xFF };
    vertices[1].c = { 0xFF, 0xFF, 0xFF, 0xFF };
    vertices[2].c = { 0xFF, 0xFF, 0xFF, 0xFF };
    vertices[3].c = { 0xFF, 0xFF, 0xFF, 0xFF };

    // Texture coordinates
    vertices[0].tc[0].x = 0.0f;
    vertices[0].tc[0].y = 0.0f;

    vertices[1].tc[0].x = 0.0f;
    vertices[1].tc[0].y = 1.0f;

    vertices[2].tc[0].x = 1.0f;
    vertices[2].tc[0].y = 0.0f;

    vertices[3].tc[0].x = 1.0f;
    vertices[3].tc[0].y = 1.0f;

    GxBufUnlock(buffer, 0);
    GxPrimVertexPtr(buffer, GxVBF_PCT);

    CGxBatch batch;
    batch.m_primType = GxPrim_TriangleStrip;
    batch.m_count = 4;
    batch.m_start = 0;
    batch.m_minIndex = 0;
    batch.m_maxIndex = 3;

    GxDraw(&batch, 0);

    GxXformPop(GxXform_World);

    if (this->StereoEnabled()) {
        GxXformSetProjection(projection);
    }

    GxRsPop();
}

int32_t CGxDevice::IDevIsWindowed() {
    return this->m_format.window;
}

void CGxDevice::IRsDirty(EGxRenderState which) {
    auto rs = &this->m_appRenderStates[which];

    if (!rs->m_dirty) {
        auto ds = this->m_dirtyStates.New();
        *ds = which;

        rs->m_dirty = 1;
    }

    if (rs->m_stackDepth != this->m_stackOffsets.Count()) {
        auto ps = this->m_pushedStates.New();
        ps->m_which = which;
        ps->m_value = rs->m_value;
        ps->m_stackDepth = rs->m_stackDepth;

        rs->m_stackDepth = this->m_stackOffsets.Count();
    }
}

void CGxDevice::IRsForceUpdate() {
    for (int32_t which = 0; which < GxRenderStates_Last; which++) {
        auto& rs = this->m_appRenderStates[which];
        auto& hs = this->m_hwRenderStates[which];

        auto ds = this->m_dirtyStates.New();
        *ds = static_cast<EGxRenderState>(which);

        rs.m_dirty = 1;

        // TODO manage this through operators
        hs.m_data.i[0] = ~rs.m_value.m_data.i[0];
        hs.m_data.i[1] = ~rs.m_value.m_data.i[1];
        hs.m_data.i[2] = ~rs.m_value.m_data.i[2];
        hs.filler = ~rs.m_value.filler;
    }
}

void CGxDevice::IRsForceUpdate(EGxRenderState which) {
    if (!this->m_context) {
        return;
    }

    auto& rs = this->m_appRenderStates[which];
    auto& hs = this->m_hwRenderStates[which];

    auto ds = this->m_dirtyStates.New();
    *ds = which;

    rs.m_dirty = 1;

    // TODO manage this through operators
    hs.m_data.i[0] = ~rs.m_value.m_data.i[0];
    hs.m_data.i[1] = ~rs.m_value.m_data.i[1];
    hs.m_data.i[2] = ~rs.m_value.m_data.i[2];
    hs.filler = ~rs.m_value.filler;
}

void CGxDevice::IRsInit() {
    this->m_appRenderStates.SetCount(GxRenderStates_Last);
    this->m_hwRenderStates.SetCount(GxRenderStates_Last);

    memset(this->m_appRenderStates.m_data, 0, sizeof(CGxAppRenderState) * GxRenderStates_Last);
    memset(this->m_hwRenderStates.m_data, 0, sizeof(CGxStateBom) * GxRenderStates_Last);

    this->m_appRenderStates[GxRs_PolygonOffset].m_value     = 0;
    this->m_appRenderStates[GxRs_MatDiffuse].m_value        = 0xFFFFFFFF;
    this->m_appRenderStates[GxRs_MatEmissive].m_value       = 0;
    this->m_appRenderStates[GxRs_MatSpecular].m_value       = 0;
    this->m_appRenderStates[GxRs_MatSpecularExp].m_value    = 0.0f;
    this->m_appRenderStates[GxRs_NormalizeNormals].m_value  = 0;
    this->m_appRenderStates[GxRs_BlendingMode].m_value      = GxBlend_Opaque;
    this->m_appRenderStates[GxRs_FogStart].m_value          = 1.0f;
    this->m_appRenderStates[GxRs_FogEnd].m_value            = 0.0f;
    this->m_appRenderStates[GxRs_FogColor].m_value          = 0xFF808080;
    this->m_appRenderStates[GxRs_Lighting].m_value          = 1;
    this->m_appRenderStates[GxRs_Fog].m_value               = 1;
    this->m_appRenderStates[GxRs_DepthTest].m_value         = 1;
    this->m_appRenderStates[GxRs_DepthFunc].m_value         = 0;
    this->m_appRenderStates[GxRs_DepthWrite].m_value        = 1;
    this->m_appRenderStates[GxRs_ColorWrite].m_value        = 15;
    this->m_appRenderStates[GxRs_Culling].m_value           = 1;
    this->m_appRenderStates[GxRs_ClipPlaneMask].m_value     = 0;
    this->m_appRenderStates[GxRs_Multisample].m_value       = 1;
    this->m_appRenderStates[GxRs_ScissorTest].m_value       = 0;

    this->m_appRenderStates[GxRs_Texture0].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp0].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp0].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen0].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk61].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk69].m_value             = 0;

    this->m_appRenderStates[GxRs_Texture1].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp1].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp1].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen1].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk62].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk70].m_value             = 1;

    this->m_appRenderStates[GxRs_Texture2].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp2].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp2].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen2].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk63].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk71].m_value             = 2;

    this->m_appRenderStates[GxRs_Texture3].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp3].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp3].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen3].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk64].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk72].m_value             = 3;

    this->m_appRenderStates[GxRs_Texture4].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp4].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp4].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen4].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk65].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk73].m_value             = 4;

    this->m_appRenderStates[GxRs_Texture5].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp5].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp5].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen5].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk66].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk74].m_value             = 5;

    this->m_appRenderStates[GxRs_Texture6].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp6].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp6].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen6].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk67].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk75].m_value             = 6;

    this->m_appRenderStates[GxRs_Texture7].m_value          = nullptr;
    this->m_appRenderStates[GxRs_ColorOp7].m_value          = 0;
    this->m_appRenderStates[GxRs_AlphaOp7].m_value          = 0;
    this->m_appRenderStates[GxRs_TexGen7].m_value           = 0;
    this->m_appRenderStates[GxRs_Unk68].m_value             = 0;
    this->m_appRenderStates[GxRs_Unk76].m_value             = 7;

    this->m_appRenderStates[GxRs_Texture8].m_value          = nullptr;
    this->m_appRenderStates[GxRs_Texture9].m_value          = nullptr;
    this->m_appRenderStates[GxRs_Texture10].m_value         = nullptr;
    this->m_appRenderStates[GxRs_Texture11].m_value         = nullptr;
    this->m_appRenderStates[GxRs_Texture12].m_value         = nullptr;
    this->m_appRenderStates[GxRs_Texture13].m_value         = nullptr;
    this->m_appRenderStates[GxRs_Texture14].m_value         = nullptr;
    this->m_appRenderStates[GxRs_Texture15].m_value         = nullptr;

    this->m_appRenderStates[GxRs_PixelShader].m_value       = nullptr;
    this->m_appRenderStates[GxRs_VertexShader].m_value      = nullptr;

    this->m_appRenderStates[GxRs_PointScale].m_value        = 1.0f;
    this->m_appRenderStates[GxRs_PointScaleAttenuation].m_value = CGxDevice::s_pointScaleIdentity;
    this->m_appRenderStates[GxRs_PointScaleMin].m_value     = 0.0f;
    this->m_appRenderStates[GxRs_PointScaleMax].m_value     = 1.0f;
    this->m_appRenderStates[GxRs_PointSprite].m_value       = 0;

    this->m_appRenderStates[GxRs_Unk84].m_value             = 0.0f;
    this->m_appRenderStates[GxRs_ColorMaterial].m_value     = 0;
}

void CGxDevice::IRsSync(int32_t force) {
    if (force) {
        this->IRsForceUpdate();
    }

    for (int32_t i = this->m_dirtyStates.Count() - 1; i >= 0; i--) {
        auto ds = this->m_dirtyStates[i];
        auto rs = &this->m_appRenderStates[ds];
        auto hs = &this->m_hwRenderStates[ds];

        if (rs->m_dirty && rs->m_value != *hs) {
            this->IRsSendToHw(ds);
        }

        *hs = rs->m_value;
        rs->m_dirty = 0;
    }

    this->m_dirtyStates.SetCount(0);
}

void CGxDevice::IShaderLoad(CGxShader* shaders[], EGxShTarget target, const char* a4, const char* a5, int32_t permutations) {
    int32_t profile = this->m_caps.m_shaderTargets[target];

    if (!profile) {
        return;
    }

    char path[260];
    SFile* file = nullptr;

    while (true) {
        sprintf(path, "%s\\%s\\%s.bls", a4, g_gxShaderProfileNames[target][profile], a5);
        SFile::Open(path, &file);

        // Opened shader file
        if (file) {
            break;
        }

        // Fallback
        if (target == GxSh_Vertex) {
            // vs_3_0 -> vs_2_0
            // vs_2_0 -> vs_1_1

            if (profile == 3) {
                profile = 2;
            } else if (profile == 2) {
                profile = 1;
            } else {
                break;
            }
        } else if (target == GxSh_Pixel) {
            // nvts3 -> nvts
            // nvts2 -> nvts
            // ps_3_0 -> ps_2_0
            // ps_2_0 -> ps_1_4
            // ps_1_4 -> ps_1_1

            if (profile == 9 || profile == 10) {
                profile = 8;
            } else if (profile == 4) {
                profile = 3;
            } else if (profile == 3) {
                profile = 2;
            } else if (profile == 2) {
                profile = 1;
            } else {
                break;
            }
        }
    }

    if (!file) {
        return;
    }

    uint32_t signature;
    uint32_t version;
    uint32_t permutationCount;

    if (file) {
        SFile::Read(file, &signature, 4, nullptr, nullptr, nullptr);

        if (signature == 'GXSH') {
            SFile::Read(file, &version, 4, nullptr, nullptr, nullptr);

            if (version == 0x10003) {
                SFile::Read(file, &permutationCount, 4, nullptr, nullptr, nullptr);

                // TODO
                // assert(permutationCount == permutations);

                if (permutations > 0) {
                    for (int32_t p = 0; p < permutations; p++) {
                        shaders[p]->Load(file);

                        shaders[p]->loaded = 0;
                        shaders[p]->int34 = 0;
                    }
                }
            }
        }

        SFile::Close(file);
    }
}

void CGxDevice::ITexMarkAsUpdated(CGxTex* texId) {
    if (!texId->m_needsUpdate) {
        return;
    }

    texId->m_updateRect = {
        static_cast<int32_t>(texId->m_height),
        static_cast<int32_t>(texId->m_width),
        0,
        0
    };

    texId->m_needsUpdate = 0;
}

void CGxDevice::ITexWHDStartEnd(CGxTex* texId, uint32_t& width, uint32_t& height, uint32_t& baseMip, uint32_t& mipCount) {
    width = texId->m_width;
    height = texId->m_height;

    if ((texId->m_flags.m_filter == GxTex_Nearest || texId->m_flags.m_filter == GxTex_Linear || texId->m_flags.m_generateMipMaps) && !texId->m_flags.m_forceMipTracking) {
        baseMip = 0;
        mipCount = 1;

        return;
    }

    mipCount = 1;

    if (texId->m_format == GxTex_Dxt1 || texId->m_format == GxTex_Dxt3 || texId->m_format == GxTex_Dxt5) {
        uint32_t shortEdge = std::min(texId->m_width, texId->m_height);

        while (shortEdge > 4) {
            shortEdge /= 2;
            mipCount++;
        }
    } else {
        uint32_t longEdge = std::max(texId->m_width, texId->m_height);

        while (longEdge > 1) {
            longEdge /= 2;
            mipCount++;
        }
    }

    baseMip = std::min(this->m_baseMipLevel, mipCount - 1);

    width >>= baseMip;
    height >>= baseMip;

    if (texId->m_flags.m_forceMipTracking) {
        mipCount = baseMip + 1;
    }
}

int32_t CGxDevice::MasterEnable(EGxMasterEnables state) {
    return ((1 << state) & this->m_appMasterEnables) != 0;
}

void CGxDevice::MasterEnableSet(EGxMasterEnables state, int32_t enable) {
    this->m_appMasterEnables = ((enable & 1) << state) | (this->m_appMasterEnables & ~(1 << state));

    switch (state) {
        case GxMasterEnable_Lighting:
            this->IRsForceUpdate(GxRs_Lighting);
            break;

        case GxMasterEnable_Fog:
            this->IRsForceUpdate(GxRs_Fog);
            break;

        case GxMasterEnable_DepthTest:
            this->IRsForceUpdate(GxRs_DepthTest);
            break;

        case GxMasterEnable_DepthWrite:
            this->IRsForceUpdate(GxRs_DepthWrite);
            break;

        case GxMasterEnable_ColorWrite:
            this->IRsForceUpdate(GxRs_ColorWrite);
            break;

        case GxMasterEnable_Culling:
            this->IRsForceUpdate(GxRs_Culling);
            break;

        default:
            break;
    }
}

void CGxDevice::PrimIndexPtr(CGxBuf* buf) {
    if (buf->unk1E || this->m_primIndexBuf != buf) {
        buf->unk1E = 0;
        this->m_primIndexDirty = 1;
        this->m_primIndexBuf = buf;
    }
}

void CGxDevice::PrimVertexFormat(CGxBuf* buf, CGxVertexAttrib* attribs, uint32_t count) {
    for (int32_t i = 0; i < count; i++) {
        int32_t attrib = attribs->attrib;

        int32_t dirty = buf->unk1E
            || this->m_primVertexFormatBuf[attrib] != buf
            || this->m_primVertexFormatAttrib[attrib].type != attribs->type
            || this->m_primVertexFormatAttrib[attrib].offset != attribs->offset
            || this->m_primVertexFormatAttrib[attrib].bufSize != attribs->bufSize;

        if (dirty) {
            this->m_primVertexDirty |= 1 << attrib;
        }

        this->m_primVertexFormatBuf[attrib] = buf;

        this->m_primVertexFormatAttrib[attrib].attrib = attribs->attrib;
        this->m_primVertexFormatAttrib[attrib].type = attribs->type;
        this->m_primVertexFormatAttrib[attrib].offset = attribs->offset;
        this->m_primVertexFormatAttrib[attrib].bufSize = attribs->bufSize;

        attribs++;
    }

    buf->unk1E = 0;
    this->m_primVertexFormat = GxVertexBufferFormats_Last;
}

void CGxDevice::PrimVertexMask(uint32_t mask) {
    this->m_primVertexDirty |= mask ^ this->m_primVertexMask;
    this->m_primVertexMask = mask;
    this->m_primVertexFormat = GxVertexBufferFormats_Last;
}

void CGxDevice::PrimVertexPtr(CGxBuf* buf, EGxVertexBufferFormat format) {
    this->m_primVertexFormat = format;
    this->m_primVertexBuf = buf;
    this->m_primVertexSize = Buffer::s_vertexBufDesc[format].size;
}

CGxPool* CGxDevice::PoolCreate(EGxPoolTarget target, EGxPoolUsage usage, uint32_t size, EGxPoolHintBits hint, const char* name) {
    auto m = SMemAlloc(sizeof(CGxPool), __FILE__, __LINE__, 0x0);
    auto pool = new (m) CGxPool(target, usage, size, hint, name);

    this->m_poolList.LinkToTail(pool);

    return pool;
}

void CGxDevice::RsGet(EGxRenderState which, int32_t& value) {
    value = static_cast<int32_t>(this->m_appRenderStates[which].m_value);
}

void CGxDevice::RsSet(EGxRenderState which, int32_t value) {
    if (!this->m_context) {
        return;
    }

    if (this->m_appRenderStates[which].m_value != value) {
        this->IRsDirty(which);
        this->m_appRenderStates[which].m_value = value;
    }
}

void CGxDevice::RsSet(EGxRenderState which, void* value) {
    if (!this->m_context) {
        return;
    }

    if (this->m_appRenderStates[which].m_value != value) {
        this->IRsDirty(which);
        this->m_appRenderStates[which].m_value = value;

        if (value) {
            if (which >= GxRs_Texture0 && which <= GxRs_Texture15) {
                CGxTex* texture = static_cast<CGxTex*>(value);

                if (texture->m_flags.m_renderTarget && texture->m_needsUpdate) {
                    this->ITexMarkAsUpdated(texture);
                }
            }
        }
    }
}

void CGxDevice::RsSetAlphaRef() {
    if (!this->m_context) {
        return;
    }

    int32_t blendingMode;
    this->RsGet(GxRs_BlendingMode, blendingMode);

    this->RsSet(GxRs_AlphaRef, CGxDevice::s_alphaRef[blendingMode]);
}

void CGxDevice::RsPop() {
    auto topOfStack = this->m_stackOffsets[this->m_stackOffsets.Count() - 1];

    if (this->m_pushedStates.Count() > topOfStack) {
        auto bottomOfStack = this->m_pushedStates.Count() - 1;
        auto stackSize = this->m_pushedStates.Count() - topOfStack;

        for (uint32_t stackOffset = 0; stackOffset < stackSize; stackOffset++) {
            auto ps = &this->m_pushedStates[bottomOfStack - stackOffset];
            auto rs = &this->m_appRenderStates[ps->m_which];

            if (!rs->m_dirty) {
                auto ds = this->m_dirtyStates.New();
                *ds = ps->m_which;

                rs->m_dirty = 1;
            }

            rs->m_value = ps->m_value;
            rs->m_stackDepth = ps->m_stackDepth;
        }
    }

    this->m_pushedStates.SetCount(topOfStack);
    this->m_stackOffsets.SetCount(this->m_stackOffsets.Count() - 1);
}

void CGxDevice::RsPush() {
    auto offset = this->m_stackOffsets.New();
    *offset = this->m_pushedStates.Count();
}

void CGxDevice::ScenePresent() {
    // TODO
}

void CGxDevice::ShaderConstantsClear() {
    for (int32_t i = 0; i < 256; i++) {
        CGxDevice::s_shadowConstants[0].constants[i] = {
            std::numeric_limits<float>::max(),
            std::numeric_limits<float>::max(),
            std::numeric_limits<float>::max(),
            std::numeric_limits<float>::max()
        };

        CGxDevice::s_shadowConstants[1].constants[i] = {
            std::numeric_limits<float>::max(),
            std::numeric_limits<float>::max(),
            std::numeric_limits<float>::max(),
            std::numeric_limits<float>::max()
        };
    }

    CGxDevice::s_shadowConstants[0].unk1 = 0;
    CGxDevice::s_shadowConstants[0].unk2 = 255;

    CGxDevice::s_shadowConstants[1].unk1 = 0;
    CGxDevice::s_shadowConstants[1].unk2 = 255;
}

char* CGxDevice::ShaderConstantsLock(EGxShTarget target) {
    return target == GxSh_Vertex
        ? reinterpret_cast<char*>(&CGxDevice::s_shadowConstants[1].constants)
        : reinterpret_cast<char*>(&CGxDevice::s_shadowConstants[0].constants);
}

void CGxDevice::ShaderConstantsSet(EGxShTarget target, uint32_t index, const float* constants, uint32_t count) {
    STORM_ASSERT((index + count - 1) <= 255);

    if (!count) {
        return;
    }

    ShaderConstants* dst;

    if (target == GxSh_Vertex) {
        dst = &CGxDevice::s_shadowConstants[1];
    } else if (target == GxSh_Pixel) {
        dst = &CGxDevice::s_shadowConstants[0];
    } else {
        STORM_ASSERT(false);
    }

    const float* c = constants;

    for (int32_t i = index; i < index + count; i++, c += 4) {
        int32_t dirty = 0;

        if (dst->constants[i].x != c[0]) {
            dirty = 1;
            dst->constants[i].x = c[0];
        }

        if (dst->constants[i].y != c[1]) {
            dirty = 1;
            dst->constants[i].y = c[1];
        }

        if (dst->constants[i].z != c[2]) {
            dirty = 1;
            dst->constants[i].z = c[2];
        }

        if (dst->constants[i].w != c[3]) {
            dirty = 1;
            dst->constants[i].w = c[3];
        }

        if (dirty) {
            if (dst->unk2 > i) {
                dst->unk2 = i;
            }

            if (dst->unk1 < i) {
                dst->unk1 = i;
            }
        }
    }
}

void CGxDevice::ShaderConstantsUnlock(EGxShTarget target, uint32_t index, uint32_t count) {
    if (target == GxSh_Pixel) {
        ShaderConstants& dst = CGxDevice::s_shadowConstants[0];
        dst.unk2 = std::min(dst.unk2, index);
        dst.unk1 = std::max(dst.unk1, index + count - 1);
    } else {
        ShaderConstants& dst = CGxDevice::s_shadowConstants[1];
        dst.unk2 = std::min(dst.unk2, index);
        dst.unk1 = std::max(dst.unk1, index + count - 1);
    }
}

void CGxDevice::ShaderCreate(CGxShader* shaders[], EGxShTarget target, const char* a4, const char* a5, int32_t permutations) {
    auto shaderList = &this->m_shaderList[target];

    if (permutations == 0) {
        return;
    }

    if (permutations == 1) {
        auto shader = shaderList->Ptr(a5);

        if (shader) {
            shaders[0] = shader;
            shader->refCount++;
            return;
        }

        shader = shaderList->New(a5, 0, 0);
        shaders[0] = shader;

        shader->refCount++;
        shader->target = target;

        this->IShaderLoad(shaders, target, a4, a5, permutations);

        return;
    }

    memset(shaders, 0, permutations * sizeof(void*));

    char key[256];

    int32_t p = 0;

    while (p < permutations) {
        sprintf(key, "%s:%d", a5, p);

        auto shader = shaderList->Ptr(key);
        shaders[p] = shader;

        if (!shader) {
            break;
        }

        p++;
    }

    // Fully loaded
    if (p == permutations) {
        for (p = 0; p < permutations; p++) {
            shaders[p]->refCount++;
        }

        return;
    }

    for (p = 0; p < permutations; p++) {
        sprintf(key, "%s:%d", a5, p);

        auto shader = shaderList->New(key, 0, 0);
        shaders[p] = shader;

        shader->refCount++;
        shader->target = target;
    }

    this->IShaderLoad(shaders, target, a4, a5, permutations);
}

int32_t CGxDevice::TexCreate(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t depth, EGxTexFormat format, EGxTexFormat dataFormat, CGxTexFlags flags, void* userArg, void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), const char* name, CGxTex*& texId) {
    auto m = SMemAlloc(sizeof(CGxTex), __FILE__, __LINE__, 0);
    auto tex = new (m) CGxTex(
        target,
        width,
        height,
        depth,
        format,
        dataFormat,
        flags,
        userArg,
        userFunc,
        name
    );

    texId = tex;

    // TODO
    // - link tex to list in device

    return 1;
}

void CGxDevice::TexDestroy(CGxTex* texId) {
    // TODO

    if (texId) {
        delete texId;
    }
}

void CGxDevice::TexMarkForUpdate(CGxTex* texId, const CiRect& updateRect, int32_t immediate) {
    texId->m_needsUpdate = 1;

    // If the bounds of the updateRect are invalid, default to { 0, 0, height, width }
    if (updateRect.minY >= updateRect.maxY || updateRect.minX >= updateRect.maxX) {
        texId->m_updateRect = {
            0,
            0,
            static_cast<int32_t>(texId->m_height),
            static_cast<int32_t>(texId->m_width)
        };
    } else {
        texId->m_updateRect = updateRect;
    }

    if (immediate) {
        this->ITexMarkAsUpdated(texId);
    }
}

void CGxDevice::TexSetWrap(CGxTex* texId, EGxTexWrapMode wrapU, EGxTexWrapMode wrapV) {
    if (texId->m_flags.m_wrapU == wrapU && texId->m_flags.m_wrapV == wrapV) {
        return;
    }

    texId->m_flags.m_wrapU = wrapU;
    texId->m_flags.m_wrapV = wrapV;
    texId->m_needsFlagUpdate = 1;

    for (int32_t rs = GxRs_Texture0; rs <= GxRs_Texture15; rs++) {
        this->IRsForceUpdate(static_cast<EGxRenderState>(rs));
    }
}

void CGxDevice::ValidateDraw(CGxBatch* batch, int32_t count) {
    // TODO
}

void CGxDevice::XformPop(EGxXform xf) {
    this->m_xforms[xf].Pop();
}

void CGxDevice::XformProjection(C44Matrix& matrix) {
    matrix = this->m_projection;
}

void CGxDevice::XformProjNative(C44Matrix& matrix) {
    matrix = this->m_projNative;

    if (this->m_api == GxApi_OpenGl) {
        matrix.c0 *= -1.0f;
        matrix.c1 *= -1.0f;
        matrix.c2 *= -1.0f;
        matrix.c3 *= -1.0f;
    }
}

void CGxDevice::XformPush(EGxXform xf) {
    this->m_xforms[xf].Push();
}

// 1-liner for Push/Set
void CGxDevice::XformPush(EGxXform xf, const C44Matrix& matrix) {
    this->m_xforms[xf].Push();
    this->m_xforms[xf].Top() = matrix;
}

void CGxDevice::XformSet(EGxXform xf, const C44Matrix& matrix) {
    this->m_xforms[xf].Top() = matrix;
}

void CGxDevice::XformSetProjection(const C44Matrix& matrix) {
    this->m_projection = matrix;
}

void CGxDevice::XformSetView(const C44Matrix& matrix) {
    this->m_xforms[GxXform_View].Top() = matrix;

    for (int32_t i = GxRs_TexGen0; i < GxRs_TexGen7; i++) {
        if (static_cast<int32_t>(this->m_appRenderStates[i].m_value) - 1 <= 1) {
            this->IRsForceUpdate(static_cast<EGxRenderState>(i));
        }
    }
}

void CGxDevice::XformSetViewport(float minX, float maxX, float minY, float maxY, float minZ, float maxZ) {
    if (
        minX == this->m_viewport.x.l
        && maxX == this->m_viewport.x.h
        && minY == this->m_viewport.y.l
        && maxY == this->m_viewport.y.h
        && minZ == this->m_viewport.z.l
        && maxZ == this->m_viewport.z.h
    ) {
        return;
    }

    this->intF6C = 1;

    this->m_viewport.x.l = minX;
    this->m_viewport.x.h = maxX;
    this->m_viewport.y.l = minY;
    this->m_viewport.y.h = maxY;
    this->m_viewport.z.l = minZ;
    this->m_viewport.z.h = maxZ;
}

void CGxDevice::XformView(C44Matrix& matrix) {
    matrix = this->m_xforms[GxXform_View].m_mtx[this->m_xforms[GxXform_View].m_level];
}

void CGxDevice::XformViewport(float& minX, float& maxX, float& minY, float& maxY, float& minZ, float& maxZ) {
    minX = this->m_viewport.x.l;
    maxX = this->m_viewport.x.h;
    minY = this->m_viewport.y.l;
    maxY = this->m_viewport.y.h;
    minZ = this->m_viewport.z.l;
    maxZ = this->m_viewport.z.h;
}

void CGxDevice::CursorSetVisible(int32_t visible) {
    this->m_cursorVisible = visible;
}

uint32_t* CGxDevice::CursorLock() {
    return this->m_cursor;
}

void CGxDevice::CursorUnlock(uint32_t x, uint32_t y) {
    this->m_cursorHotspotX = x;
    this->m_cursorHotspotY = y;
}
