#include "component/CCharacterComponent.hpp"
#include "component/Texture.hpp"
#include "component/Util.hpp"
#include "console/CVar.hpp"
#include "db/Db.hpp"
#include "gx/Blp.hpp"
#include "gx/Device.hpp"
#include "gx/Texture.hpp"
#include "model/CM2Model.hpp"
#include "model/CM2Scene.hpp"
#include "object/Types.hpp"
#include "util/CStatus.hpp"
#include "util/Unimplemented.hpp"
#include <common/ObjectAlloc.hpp>
#include <storm/Memory.hpp>
#include <storm/String.hpp>

uint32_t* CCharacterComponent::s_characterFacialHairStylesList;
st_race* CCharacterComponent::s_chrVarArray;
uint32_t CCharacterComponent::s_chrVarArrayLength;
EGxTexFormat CCharacterComponent::s_gxFormat;
ITEM_FUNC CCharacterComponent::s_itemFunc[];
uint32_t CCharacterComponent::s_mipLevels;
PREP_FUNC CCharacterComponent::s_prepFunc[];
CompSectionInfo CCharacterComponent::s_sectionInfo[];
MipBits* CCharacterComponent::s_textureBuffer;
MipBits* CCharacterComponent::s_textureBufferCompressed;
uint32_t CCharacterComponent::s_textureSize;

CompSectionInfo CCharacterComponent::s_sectionInfoRaw[] = {
    { { 0,   0   }, { 256, 128 } }, // SECTION_ARM_UPPER
    { { 0,   128 }, { 256, 128 } }, // SECTION_ARM_LOWER
    { { 0,   256 }, { 256, 64  } }, // SECTION_HAND
    { { 256, 0,  }, { 256, 128 } }, // SECTION_TORSO_UPPER
    { { 256, 128 }, { 256, 64  } }, // SECTION_TORSO_LOWER
    { { 256, 192 }, { 256, 128 } }, // SECTION_LEG_UPPER
    { { 256, 320 }, { 256, 128 } }, // SECTION_LEG_LOWER
    { { 256, 448 }, { 256, 64  } }, // SECTION_FOOT
    { { 0,   320 }, { 256, 64  } }, // SECTION_HEAD_UPPER
    { { 0,   384 }, { 256, 128 } }, // SECTION_HEAD_LOWER
};

const char* s_componentSections[] = {
    "ArmUpperTexture",
    "ArmLowerTexture",
    "HandTexture",
    "TorsoUpperTexture",
    "TorsoLowerTexture",
    "LegUpperTexture",
    "LegLowerTexture",
    "FootTexture"
};

const char* s_fileDecorations[] = {
    "M",
    "F",
    "U"
};

/**
 * Texture priorities for each item slot and component section. Determines order for pasting
 * textures in RenderPrep functions. Priority start and end for leg component sections is
 * adjusted in the corresponding RenderPrep functions.
 */
int32_t s_itemPriority[NUM_ITEM_SLOT][NUM_COMPONENT_SECTIONS] = {
    { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // ITEMSLOT_0
    { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // ITEMSLOT_1
    {  0,  0, -1,  0,  0, -1, -1, -1, -1, -1 }, // ITEMSLOT_2
    {  1,  1, -1,  1,  1,  1,  1, -1, -1, -1 }, // ITEMSLOT_3
    { -1, -1, -1, -1,  5,  2, -1, -1, -1, -1 }, // ITEMSLOT_4
    { -1, -1, -1, -1, -1,  0,  0, -1, -1, -1 }, // ITEMSLOT_5
    { -1, -1, -1, -1, -1, -1,  2,  0, -1, -1 }, // ITEMSLOT_6
    { -1,  2, -1, -1, -1, -1, -1, -1, -1, -1 }, // ITEMSLOT_7
    { -1,  3,  0, -1, -1, -1, -1, -1, -1, -1 }, // ITEMSLOT_8
    { -1, -1, -1,  4,  4, -1, -1, -1, -1, -1 }, // ITEMSLOT_9
    {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 }, // ITEMSLOT_10
    {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 }, // ITEMSLOT_11
};

/**
 * Total item texture priorities for each component section.
 */
#define SECTION_AU_ITEM_PRIORITIES 2
#define SECTION_AL_ITEM_PRIORITIES 7
#define SECTION_HA_ITEM_PRIORITIES 1
#define SECTION_TU_ITEM_PRIORITIES 5
#define SECTION_TL_ITEM_PRIORITIES 7
#define SECTION_LU_ITEM_PRIORITIES 3
#define SECTION_LL_ITEM_PRIORITIES 6
#define SECTION_FO_ITEM_PRIORITIES 1
#define SECTION_HU_ITEM_PRIORITIES 0
#define SECTION_HL_ITEM_PRIORITIES 0

int32_t s_bInRenderPrep = 0;
char s_buffer[STORM_MAX_PATH];
uint32_t* s_componentHeap;
char* s_pathEnd;
char s_path[STORM_MAX_PATH];
char* s_pathEnd2;
char s_path2[STORM_MAX_PATH];
CStatus s_status;

static CVar* s_componentTextureLevelCvar;
static CVar* s_componentThreadCvar;
static CVar* s_componentCompressCvar;

#define TEXTURE_INDEX(section, texture) (3 * section + texture)

bool ComponentTextureLevelCallback(CVar* var, const char* oldValue, const char* value, void* arg) {
    // 3.3.5a release client has no logic, only returns true
    return true;
}

bool ComponentThreadCallback(CVar* var, const char* oldValue, const char* value, void* arg) {
    // 3.3.5a release client has no logic, only returns true
    return true;
}

bool ComponentCompressCallback(CVar* var, const char* oldValue, const char* value, void* arg) {
    // 3.3.5a release client has no logic, only returns true
    return true;
}

void ReplaceParticleColor(CM2Model* model, int32_t particleColorID) {
    // TODO
}

int32_t CCharacterComponent::AddHandItem(CM2Model* model, const ItemDisplayInfoRec* displayRec, INVENTORY_SLOTS invSlot, SHEATHE_TYPE sheatheType, bool sheathed, bool shield, bool a7, int32_t visualID) {
    if (!model || !displayRec || invSlot > INVSLOT_TABARD) {
        return -1;
    }

    auto itemModelPath = "Item\\ObjectComponents\\Weapon\\";
    auto itemTexturePath = "Item\\ObjectComponents\\Weapon\\";

    GEOCOMPONENTLINKS itemLink;
    GEOCOMPONENTLINKS sheatheLink;

    if (invSlot == INVSLOT_MAINHAND) {
        itemLink = ATTACH_HANDR;
        sheatheLink = CCharacterComponent::GetSheatheLink(sheatheType, true);
    } else if (invSlot == INVSLOT_OFFHAND) {
        itemLink = ATTACH_HANDL;
        sheatheLink = CCharacterComponent::GetSheatheLink(sheatheType, false);
    } else if (invSlot == INVSLOT_RANGED) {
        if (a7) {
            itemLink = ATTACH_HANDR;
            sheatheLink = CCharacterComponent::GetSheatheLink(sheatheType, true);
        } else {
            itemLink = ATTACH_HANDL;
            sheatheLink = CCharacterComponent::GetSheatheLink(sheatheType, false);
        }
    } else {
        return -1;
    }

    if (shield) {
        itemModelPath = "Item\\ObjectComponents\\Shield\\";
        itemTexturePath = "Item\\ObjectComponents\\Shield\\";

        itemLink = ATTACH_SHIELD;
    }

    CCharacterComponent::RemoveLinkpt(model, itemLink);
    CCharacterComponent::RemoveLinkpt(model, sheatheLink);

    auto link = sheathed ? sheatheLink : itemLink;

    if (model->IsLoaded(0, 0) && !model->HasAttachment(link)) {
        return -1;
    }

    SStrPrintf(s_buffer, sizeof(s_buffer), "%s%s", itemModelPath, displayRec->m_modelName[0]);
    SStrCopy(s_pathEnd, s_buffer);

    SStrPrintf(s_buffer, sizeof(s_buffer), "%s%s.blp", itemTexturePath, displayRec->m_modelTexture[0]);
    SStrCopy(s_pathEnd2, s_buffer);

    if (visualID == 0) {
        visualID = displayRec->m_itemVisual;
    }

    CCharacterComponent::AddLink(model, link, s_path, s_path2, visualID, displayRec);

    return link;
}

void CCharacterComponent::AddLink(CM2Model* parent, GEOCOMPONENTLINKS link, char const* modelPath, char const* texturePath, int32_t visualID, const ItemDisplayInfoRec* displayRec) {
    if (!parent) {
        return;
    }

    // Create item model

    auto model = parent->m_scene->CreateModel(modelPath, 0);

    if (!model) {
        return;
    }

    // Create item texture

    auto textureFlags = CGxTexFlags(GxTex_LinearMipNearest, 0, 0, 0, 0, 0, 1);
    auto texture = TextureCreate(texturePath, textureFlags, &s_status, 0);

    if (!texture) {
        model->Release();
        return;
    }

    // Replace item texture

    model->ReplaceTexture(2, texture);
    HandleClose(texture);

    // Add item visual

    if (visualID > 0) {
        // TODO CCharacterComponent::ComponentUtilAddItemVisual(model, visualID);
    }

    // Attach item to parent

    parent->DetachAllChildrenById(link);
    model->AttachToParent(parent, link, nullptr, 0);

    if (link == ATTACH_HANDR) {
        CCharacterComponent::ComponentCloseFingers(parent, HAND_RIGHT);
    } else if (link == ATTACH_HANDL) {
        CCharacterComponent::ComponentCloseFingers(parent, HAND_LEFT);
    }

    // Replace item particle color

    // TODO ReplaceParticleColor(displayRec->m_particleColorID, model);

    model->Release();
}

CCharacterComponent* CCharacterComponent::AllocComponent() {
    uint32_t memHandle;
    void* mem;

    if (!ObjectAlloc(*s_componentHeap, &memHandle, &mem, false)) {
        return nullptr;
    }

    auto component = new (mem) CCharacterComponent();
    component->m_memHandle = memHandle;

    return component;
}

void CCharacterComponent::ApplyMonsterGeosets(CM2Model* model, const CreatureDisplayInfoRec* displayInfoRec) {
    if (!model || !displayInfoRec || !displayInfoRec->m_creatureGeosetData) {
        return;
    }

    for (int32_t group = 100, dataOfs = 0; group < 900; group += 100, dataOfs += 4) {
        auto section = (displayInfoRec->m_creatureGeosetData >> dataOfs) & 0xF;

        if (section) {
            // Hide all sections in group
            model->SetGeometryVisible(group, group + 99, false);

            // Show matching section
            model->SetGeometryVisible(group + section, group + section, true);
        }
    }

    model->OptimizeVisibleGeometry();
}

void CCharacterComponent::ComponentCloseFingers(CM2Model* model, COMP_HAND_SLOT handSlot) {
    uint32_t firstBone;
    uint32_t lastBone;

    if (handSlot == HAND_LEFT) {
        firstBone = 13;
        lastBone = 17;
    } else {
        firstBone = 8;
        lastBone = 12;
    }

    for (uint32_t boneId = firstBone; boneId <= lastBone; boneId++) {
        model->SetBoneSequence(boneId, 15, 0xFFFFFFFF, 0, 1.0f, 0, 1);
    }
}

void CCharacterComponent::ComponentOpenFingers(CM2Model* model, COMP_HAND_SLOT handSlot) {
    uint32_t firstBone;
    uint32_t lastBone;

    if (handSlot == HAND_LEFT) {
        firstBone = 13;
        lastBone = 17;
    } else {
        firstBone = 8;
        lastBone = 12;
    }

    for (uint32_t boneId = firstBone; boneId <= lastBone; boneId++) {
        model->UnsetBoneSequence(boneId, 0, 1);
    }
}

HTEXTURE CCharacterComponent::CreateTexture(const char* fileName, CStatus* status) {
    auto texFlags = CGxTexFlags(GxTex_LinearMipNearest, 0, 0, 0, 0, 0, 1);
    return TextureCreate(fileName, texFlags, status, 0);
}

void CCharacterComponent::FreeComponent(CCharacterComponent* component) {
    component->~CCharacterComponent();
    ObjectFree(*s_componentHeap, component->m_memHandle);
}

GEOCOMPONENTLINKS CCharacterComponent::GetSheatheLink(SHEATHE_TYPE sheatheType, bool a2) {
    switch (sheatheType) {
    case SHEATHE_1:
        return a2 ? ATTACH_SHEATH_MAINHAND : ATTACH_SHEATH_OFFHAND;
    case SHEATHE_2:
        return a2 ? ATTACH_LARGEWEAPONLEFT : ATTACH_LARGEWEAPONRIGHT;
    case SHEATHE_3:
        return a2 ? ATTACH_HIPWEAPONLEFT : ATTACH_HIPWEAPONRIGHT;
    case SHEATHE_4:
        return ATTACH_SHEATH_SHIELD;
    default:
        return ATTACH_NONE;
    }
}

void CCharacterComponent::Initialize() {
    s_componentTextureLevelCvar = CVar::Register(
        "componentTextureLevel",
        "Number of mip levels used for character component textures",
        0x1,
        "8",
        &ComponentTextureLevelCallback,
        DEBUG
    );

    s_componentThreadCvar = CVar::Register(
        "componentThread",
        "Multi thread character component processing",
        0x1,
        "1",
        &ComponentThreadCallback,
        DEBUG
    );

    s_componentCompressCvar = CVar::Register(
        "componentCompress",
        "Character component texture compression",
        0x1,
        "1",
        &ComponentCompressCallback,
        DEBUG
    );

    // TODO

    // TODO proper implementation
    CCharacterComponent::Initialize(GxTex_Rgb565, 9, 0, 0);
}

void CCharacterComponent::Initialize(EGxTexFormat textureFormat, uint32_t textureLevel, int32_t thread, int32_t compress) {
    if (!s_componentHeap) {
        auto heapId = static_cast<uint32_t*>(STORM_ALLOC(sizeof(uint32_t)));
        *heapId = ObjectAllocAddHeap(sizeof(CCharacterComponent), 32, "CCharacterComponent", true);

        s_componentHeap = heapId;
    }

    s_pathEnd = s_path;
    s_pathEnd2 = s_path2;

    CCharacterComponent::s_prepFunc[SECTION_ARM_UPPER]      = &CCharacterComponent::RenderPrepAU;
    CCharacterComponent::s_prepFunc[SECTION_ARM_LOWER]      = &CCharacterComponent::RenderPrepAL;
    CCharacterComponent::s_prepFunc[SECTION_HAND]           = &CCharacterComponent::RenderPrepHA;
    CCharacterComponent::s_prepFunc[SECTION_TORSO_UPPER]    = &CCharacterComponent::RenderPrepTU;
    CCharacterComponent::s_prepFunc[SECTION_TORSO_LOWER]    = &CCharacterComponent::RenderPrepTL;
    CCharacterComponent::s_prepFunc[SECTION_LEG_UPPER]      = &CCharacterComponent::RenderPrepLU;
    CCharacterComponent::s_prepFunc[SECTION_LEG_LOWER]      = &CCharacterComponent::RenderPrepLL;
    CCharacterComponent::s_prepFunc[SECTION_FOOT]           = &CCharacterComponent::RenderPrepFO;
    CCharacterComponent::s_prepFunc[SECTION_HEAD_UPPER]     = &CCharacterComponent::RenderPrepHU;
    CCharacterComponent::s_prepFunc[SECTION_HEAD_LOWER]     = &CCharacterComponent::RenderPrepHL;

    CCharacterComponent::s_itemFunc[SECTION_ARM_UPPER]      = &CCharacterComponent::UpdateItemAU;
    CCharacterComponent::s_itemFunc[SECTION_ARM_LOWER]      = &CCharacterComponent::UpdateItemAL;
    CCharacterComponent::s_itemFunc[SECTION_HAND]           = &CCharacterComponent::UpdateItemHA;
    CCharacterComponent::s_itemFunc[SECTION_TORSO_UPPER]    = &CCharacterComponent::UpdateItemTU;
    CCharacterComponent::s_itemFunc[SECTION_TORSO_LOWER]    = &CCharacterComponent::UpdateItemTL;
    CCharacterComponent::s_itemFunc[SECTION_LEG_UPPER]      = &CCharacterComponent::UpdateItemLU;
    CCharacterComponent::s_itemFunc[SECTION_LEG_LOWER]      = &CCharacterComponent::UpdateItemLL;
    CCharacterComponent::s_itemFunc[SECTION_FOOT]           = &CCharacterComponent::UpdateItemFO;
    CCharacterComponent::s_itemFunc[SECTION_HEAD_UPPER]     = &CCharacterComponent::UpdateItemHU;
    CCharacterComponent::s_itemFunc[SECTION_HEAD_LOWER]     = &CCharacterComponent::UpdateItemHL;

    // Clamp mip levels between 6 and 9
    uint32_t mipLevels = std::min(std::max(textureLevel, 6u), 9u);

    // Cap mip levels to 8 if compression isn't enabled
    if (!compress && mipLevels > 8) {
        mipLevels = 8;
    }

    CCharacterComponent::s_mipLevels = mipLevels;
    CCharacterComponent::s_textureSize = 1 << mipLevels;

    // Scale section info to match mip levels
    for (int32_t i = 0; i < NUM_COMPONENT_SECTIONS; i++) {
        auto& info = CCharacterComponent::s_sectionInfo[i];
        auto& infoRaw = CCharacterComponent::s_sectionInfoRaw[i];

        info.pos.x = infoRaw.pos.x >> (9 - mipLevels);
        info.pos.y = infoRaw.pos.y >> (9 - mipLevels);
        info.size.x = infoRaw.size.x >> (9 - mipLevels);
        info.size.y = infoRaw.size.y >> (9 - mipLevels);
    }

    // TODO

    CCharacterComponent::s_gxFormat = textureFormat;

    // TODO

    CCharacterComponent::InitDbData();

    // TODO

    CCharacterComponent::s_textureBuffer = TextureAllocMippedImg(
        PIXEL_ARGB8888,
        CCharacterComponent::s_textureSize,
        CCharacterComponent::s_textureSize
    );

    // TODO
}

void CCharacterComponent::InitDbData() {
    uint32_t varArrayLength = (g_chrRacesDB.m_maxID + 1) * UNITSEX_NUM_SEXES;
    CCharacterComponent::s_chrVarArrayLength = varArrayLength;

    BuildComponentArray(varArrayLength, &CCharacterComponent::s_chrVarArray);
    CountFacialFeatures(varArrayLength, &CCharacterComponent::s_characterFacialHairStylesList);
}

void CCharacterComponent::Paste(void* srcTexture, MipBits* dstMips, const C2iVector& dstPos, const C2iVector& srcPos, const C2iVector& srcSize, TCTEXTUREINFO& srcInfo, int32_t srcMipLevel) {
    uint32_t dstWidth = CCharacterComponent::s_textureSize * 4;

    auto srcPal = TextureCacheGetPal(srcTexture);

    if (!srcPal) {
        // TODO fill in with crappy green (0x00FF00FF)

        return;
    }

    switch (srcInfo.alphaSize) {
    case 0:
        CCharacterComponent::PasteOpaque(
            srcTexture,
            srcPal,
            dstMips,
            dstPos,
            dstWidth,
            srcPos,
            srcSize,
            srcInfo,
            srcMipLevel,
            -srcMipLevel
        );

        break;

    case 1:
        CCharacterComponent::PasteTransparent1Bit(
            srcTexture,
            srcPal,
            dstMips,
            dstPos,
            dstWidth,
            srcPos,
            srcSize,
            srcInfo,
            srcMipLevel,
            -srcMipLevel
        );

        break;

    case 4:
        CCharacterComponent::PasteTransparent4Bit(
            srcTexture,
            srcPal,
            dstMips,
            dstPos,
            dstWidth,
            srcPos,
            srcSize,
            srcInfo,
            srcMipLevel,
            -srcMipLevel
        );

        break;

    case 8:
        CCharacterComponent::PasteTransparent8Bit(
            srcTexture,
            srcPal,
            dstMips,
            dstPos,
            dstWidth,
            srcPos,
            srcSize,
            srcInfo,
            srcMipLevel,
            -srcMipLevel
        );

        break;

    default:
        // Do nothing
        break;
    }
}

void CCharacterComponent::PasteFromSkin(COMPONENT_SECTIONS section, void* srcTexture, MipBits* dstMips) {
    if (!TextureCacheHasMips(srcTexture)) {
        return;
    }

    auto& sectionInfo = CCharacterComponent::s_sectionInfo[section];

    TCTEXTUREINFO srcInfo;
    TextureCacheGetInfo(srcTexture, srcInfo, 1);

    // Skin is always opaque
    srcInfo.alphaSize = 0;

    if (srcInfo.width >= CCharacterComponent::s_textureSize || srcInfo.height >= CCharacterComponent::s_textureSize ) {
        // Calculate source mip level appropriate for CCharacterComponent::s_textureSize
        int32_t srcMipLevel = 0;
        int32_t srcWidth = srcInfo.width;
        while (srcWidth > CCharacterComponent::s_textureSize) {
            srcWidth /= 2;
            srcMipLevel++;
        }

        CCharacterComponent::Paste(srcTexture, dstMips, sectionInfo.pos, sectionInfo.pos, sectionInfo.size, srcInfo, srcMipLevel);
    } else {
        CCharacterComponent::PasteScale(srcTexture, dstMips, sectionInfo.pos, sectionInfo.pos, sectionInfo.size, srcInfo);
    }
}

void CCharacterComponent::PasteOpaque(void* srcTexture, const BlpPalPixel* srcPal, MipBits* dstMips, const C2iVector& dstPos, uint32_t dstWidth, const C2iVector& srcPos, const C2iVector& srcSize, TCTEXTUREINFO& srcInfo, int32_t srcMipLevel, int32_t dstMipLevelOfs) {
    // Prepare first mip level

    C2iVector curSrcSize = srcSize;

    C2iVector curSrcPos = srcPos;
    C2iVector curDstPos = dstPos;

    uint32_t curSrcWidth = srcInfo.width >> srcMipLevel;
    uint32_t curDstWidth = dstWidth;

    // Paste texture for each mip level

    for (int32_t curMipLevel = srcMipLevel; curMipLevel < srcInfo.mipCount; curMipLevel++) {
        auto srcMip = TextureCacheGetMip(srcTexture, curMipLevel);
        auto srcRow = &srcMip[(curSrcPos.y * curSrcWidth) + curSrcPos.x];

        auto dstMip = reinterpret_cast<uint8_t*>(dstMips->mip[curMipLevel + dstMipLevelOfs]);
        auto dstRow = &dstMip[(curDstPos.y * curDstWidth) + (curDstPos.x * 4)];

        // Calculate the end of the source region
        auto srcEnd = srcRow + curSrcWidth * curSrcSize.y;

        // Copy each row
        while (srcRow < srcEnd) {
            auto dst = reinterpret_cast<C4Pixel*>(dstRow);

            for (uint32_t x = 0; x < curSrcSize.x; x++) {
                // Get palette entry
                auto& src = srcPal[srcRow[x]];

                // Copy color from palette entry
                dst->b = src.b;
                dst->g = src.g;
                dst->r = src.r;
                dst->a = 0xFF;

                dst++;
            }

            // Move to next row
            srcRow += curSrcWidth;
            dstRow += curDstWidth;
        }

        // Prepare next mip level

        curSrcSize.x = std::max(1, curSrcSize.x / 2);
        curSrcSize.y = std::max(1, curSrcSize.y / 2);

        curSrcPos.x /= 2;
        curSrcPos.y /= 2;
        curDstPos.x /= 2;
        curDstPos.y /= 2;

        curDstWidth /= 2;
        curSrcWidth /= 2;
    }
}

void CCharacterComponent::PasteScale(void* srcTexture, MipBits* dstMips, const C2iVector& a3, const C2iVector& a4, const C2iVector& a5, TCTEXTUREINFO& srcInfo) {
    WHOA_UNIMPLEMENTED();
}

void CCharacterComponent::PasteToSection(COMPONENT_SECTIONS section, void* srcTexture, MipBits* dstMips) {
    if (!TextureCacheHasMips(srcTexture)) {
        return;
    }

    auto& sectionInfo = CCharacterComponent::s_sectionInfo[section];

    TCTEXTUREINFO srcInfo;
    TextureCacheGetInfo(srcTexture, srcInfo, 1);

    if (srcInfo.width >= sectionInfo.size.x || srcInfo.height >= sectionInfo.size.y) {
        // Calculate source mip level appropriate for section size
        int32_t srcMipLevel = 0;
        int32_t srcWidth = srcInfo.width;
        while (srcWidth > sectionInfo.size.x) {
            srcWidth /= 2;
            srcMipLevel++;
        }

        C2iVector srcPos = { 0, 0 };

        CCharacterComponent::Paste(srcTexture, dstMips, sectionInfo.pos, srcPos, sectionInfo.size, srcInfo, srcMipLevel);
    } else {
        C2iVector srcPos = { 0, 0 };

        CCharacterComponent::PasteScale(srcTexture, dstMips, sectionInfo.pos, srcPos, sectionInfo.size, srcInfo);
    }
}

void CCharacterComponent::PasteTransparent1Bit(void* srcTexture, const BlpPalPixel* srcPal, MipBits* dstMips, const C2iVector& dstPos, uint32_t dstWidth, const C2iVector& srcPos, const C2iVector& srcSize, TCTEXTUREINFO& srcInfo, int32_t srcMipLevel, int32_t dstMipLevelOfs) {
    // Prepare first mip level

    C2iVector curSrcSize = srcSize;

    C2iVector curSrcPos = srcPos;
    C2iVector curDstPos = dstPos;

    uint32_t curSrcWidth = srcInfo.width >> srcMipLevel;
    uint32_t curDstWidth = dstWidth;

    uint32_t curSrcHeight = srcInfo.height >> srcMipLevel;

    // Paste texture for each mip level

    for (int32_t curMipLevel = srcMipLevel; curMipLevel < srcInfo.mipCount; curMipLevel++) {
        auto srcMip = TextureCacheGetMip(srcTexture, curMipLevel);
        auto srcRow = &srcMip[(curSrcPos.y * curSrcWidth) + curSrcPos.x];
        auto srcAlphaRow = &srcMip[(curSrcWidth * curSrcHeight) + ((curSrcPos.y * curSrcWidth) + curSrcPos.x) / 8];

        auto dstMip = reinterpret_cast<uint8_t*>(dstMips->mip[curMipLevel + dstMipLevelOfs]);
        auto dstRow = &dstMip[(curDstPos.y * curDstWidth) + (curDstPos.x * 4)];

        // Calculate the end of the source region
        auto srcEnd = srcRow + curSrcWidth * curSrcSize.y;

        // Copy each row
        while (srcRow < srcEnd) {
            auto dst = reinterpret_cast<C4Pixel*>(dstRow);

            for (uint32_t x = 0; x < curSrcSize.x; x++) {
                // Get palette entry
                auto& src = srcPal[srcRow[x]];

                // Get alpha
                uint8_t packedAlpha = srcAlphaRow[x / 8];
                uint8_t transparent = (packedAlpha & 1 << x % 8) == 0;

                // Blend src color with dst
                dst->b = transparent ? dst->b : src.b;
                dst->g = transparent ? dst->g : src.g;
                dst->r = transparent ? dst->r : src.r;
                dst->a = 0xFF;

                dst++;
            }

            // Move to next row
            srcRow += curSrcWidth;
            srcAlphaRow += curSrcWidth / 8;
            dstRow += curDstWidth;
        }

        // Prepare next mip level

        curSrcSize.x = std::max(1, curSrcSize.x / 2);
        curSrcSize.y = std::max(1, curSrcSize.y / 2);

        curSrcPos.x /= 2;
        curSrcPos.y /= 2;
        curDstPos.x /= 2;
        curDstPos.y /= 2;

        curDstWidth /= 2;
        curSrcWidth /= 2;

        curSrcHeight /= 2;
    }
}

void CCharacterComponent::PasteTransparent4Bit(void* srcTexture, const BlpPalPixel* srcPal, MipBits* dstMips, const C2iVector& dstPos, uint32_t dstWidth, const C2iVector& srcPos, const C2iVector& srcSize, TCTEXTUREINFO& srcInfo, int32_t srcMipLevel, int32_t dstMipLevelOfs) {
    WHOA_UNIMPLEMENTED();
}

void CCharacterComponent::PasteTransparent8Bit(void* srcTexture, const BlpPalPixel* srcPal, MipBits* dstMips, const C2iVector& dstPos, uint32_t dstWidth, const C2iVector& srcPos, const C2iVector& srcSize, TCTEXTUREINFO& srcInfo, int32_t srcMipLevel, int32_t dstMipLevelOfs) {
    // Prepare first mip level

    C2iVector curSrcSize = srcSize;

    C2iVector curSrcPos = srcPos;
    C2iVector curDstPos = dstPos;

    uint32_t curSrcWidth = srcInfo.width >> srcMipLevel;
    uint32_t curDstWidth = dstWidth;

    uint32_t curSrcHeight = srcInfo.height >> srcMipLevel;

    // Paste texture for each mip level

    for (int32_t curMipLevel = srcMipLevel; curMipLevel < srcInfo.mipCount; curMipLevel++) {
        auto srcMip = TextureCacheGetMip(srcTexture, curMipLevel);
        auto srcRow = &srcMip[(curSrcPos.y * curSrcWidth) + curSrcPos.x];
        auto srcAlphaRow = &srcMip[(curSrcWidth * curSrcHeight) + (curSrcPos.y * curSrcWidth) + curSrcPos.x];

        auto dstMip = reinterpret_cast<uint8_t*>(dstMips->mip[curMipLevel + dstMipLevelOfs]);
        auto dstRow = &dstMip[(curDstPos.y * curDstWidth) + (curDstPos.x * 4)];

        // Calculate the end of the source region
        auto srcEnd = srcRow + curSrcWidth * curSrcSize.y;

        // Copy each row
        while (srcRow < srcEnd) {
            auto dst = reinterpret_cast<C4Pixel*>(dstRow);

            for (uint32_t x = 0; x < curSrcSize.x; x++) {
                // Get palette entry
                auto& src = srcPal[srcRow[x]];

                // Get alpha
                uint8_t alpha = srcAlphaRow[x];
                uint8_t invAlpha = 0xFF - alpha;

                // Blend src color with dst
                dst->b = (src.b * alpha + dst->b * invAlpha) / 0xFF;
                dst->g = (src.g * alpha + dst->g * invAlpha) / 0xFF;
                dst->r = (src.r * alpha + dst->r * invAlpha) / 0xFF;
                dst->a = 0xFF;

                dst++;
            }

            // Move to next row
            srcRow += curSrcWidth;
            srcAlphaRow += curSrcWidth;
            dstRow += curDstWidth;
        }

        // Prepare next mip level

        curSrcSize.x = std::max(1, curSrcSize.x / 2);
        curSrcSize.y = std::max(1, curSrcSize.y / 2);

        curSrcPos.x /= 2;
        curSrcPos.y /= 2;
        curDstPos.x /= 2;
        curDstPos.y /= 2;

        curDstWidth /= 2;
        curSrcWidth /= 2;

        curSrcHeight /= 2;
    }
}

void CCharacterComponent::RemoveLinkpt(CM2Model* model, GEOCOMPONENTLINKS link) {
    if (link == ATTACH_NONE) {
        return;
    }

    if (link == ATTACH_SHIELD || link == ATTACH_HANDL) {
        CCharacterComponent::ComponentOpenFingers(model, HAND_LEFT);
    }

    if (link == ATTACH_HANDR) {
        CCharacterComponent::ComponentOpenFingers(model, HAND_RIGHT);
    }

    if (model && model->IsLoaded(0, 0)) {
        if (model->HasAttachment(link)) {
            model->DetachAllChildrenById(link);
        }
    }
}

void CCharacterComponent::ReplaceMonsterSkin(CM2Model* model, const CreatureDisplayInfoRec* displayInfoRec, const CreatureModelDataRec* modelDataRec) {
    if (!model || !displayInfoRec || !modelDataRec) {
        return;
    }

    CStatus status;
    char texturePath[STORM_MAX_PATH];

    // Copy model path to use as base path for texture
    auto src = modelDataRec->m_modelName;
    auto dst = texturePath;
    while (*src) {
        *dst++ = *src++;
    }
    *dst = '\0';

    // Locate start of model file name
    auto lastSlash = strrchr(texturePath, '\\');
    auto modelFileName = lastSlash ? lastSlash + 1 : texturePath;

    auto textureFlags = CGxTexFlags(GxTex_LinearMipLinear, 1, 1, 0, 0, 0, 1);

    for (uint32_t i = 0; i < 3; i++) {
        auto textureName = displayInfoRec->m_textureVariation[i];

        if (textureName[0] == '\0') {
            continue;
        }

        // Replace model file name with texture name
        src = textureName;
        dst = modelFileName;
        while (*src) {
            *dst++ = *src++;
        }
        *dst = '\0';

        auto texture = TextureCreate(texturePath, textureFlags, &status, 0);

        if (texture) {
            model->ReplaceTexture(11 + i, texture);
            HandleClose(texture);
        }
    }

    ReplaceParticleColor(model, displayInfoRec->m_particleColorID);
}

void CCharacterComponent::UpdateBaseTexture(EGxTexCommand cmd, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipLevel, void* userArg, uint32_t& texelStrideInBytes, const void*& texels) {
    auto component = static_cast<CCharacterComponent*>(userArg);

    switch(cmd) {
    case GxTex_Lock: {
        if (!s_bInRenderPrep) {
            component->RenderPrepAll();
        }

        break;
    }

    case GxTex_Latch: {
        if (component->m_textureFormat == GxTex_Dxt1) {
            // TODO
            STORM_ASSERT(false);
        } else {
            texelStrideInBytes = 4 * width;
        }

        // TODO conditional check on some member of component

        auto buffer = component->m_textureFormat == GxTex_Dxt1
            ? CCharacterComponent::s_textureBufferCompressed
            : CCharacterComponent::s_textureBuffer;

        texels = buffer->mip[mipLevel];

        break;
    }

    // TODO unknown command
    case 3: {
        // TODO
        break;
    }
    }
}

CCharacterComponent::~CCharacterComponent() {
    // TODO

    if (this->m_baseTexture) {
        // TODO GxTexSetCannotUpdate

        HandleClose(this->m_baseTexture);
        this->m_baseTexture = nullptr;
    }

    if (this->m_data.model) {
        this->m_data.model->Release();
        this->m_data.model = nullptr;
    }

    for (auto& texture : this->m_texture) {
        TextureCacheDestroyTexture(texture);
        texture = nullptr;
    }

    for (auto& itemDisplay : this->m_itemDisplays) {
        for (auto& texture : itemDisplay.texture) {
            TextureCacheDestroyTexture(texture);
            texture = nullptr;
        }
    }

    // TODO
}

void CCharacterComponent::AddItem(ITEM_SLOT itemSlot, int32_t displayID, int32_t a4) {
    if (displayID <= 0) {
        return;
    }

    auto displayRec = g_itemDisplayInfoDB.GetRecord(displayID);

    if (!displayRec) {
        return;
    }

    this->AddItem(itemSlot, displayRec, a4);
}

void CCharacterComponent::AddItem(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, int32_t a4) {
    this->m_flags |= 0x4;

    this->m_items[itemSlot] = displayRec->m_ID;

    // Helm

    if (itemSlot == ITEMSLOT_0) {
        // TODO handle helm

        return;
    }

    // Shoulders

    if (itemSlot == ITEMSLOT_1) {
        // TODO handle shoulders

        return;
    }

    // Cape

    if (itemSlot == ITEMSLOT_10) {
        // TODO handle cape

        return;
    }

    // Unk

    if (itemSlot == ITEMSLOT_11) {
        // TODO handle unknown item

        return;
    }

    if (itemSlot == ITEMSLOT_3) {
        // TODO flag manipulation
    }

    bool isNPC = this->m_data.flags & 0x1;

    if (isNPC) {
        return;
    }

    // Items don't manipulate head component sections

    for (int32_t section = SECTION_ARM_UPPER; section <= SECTION_FOOT; section++) {
        if (*displayRec->m_texture[section] && s_itemPriority[itemSlot][section] != -1) {
            (this->*CCharacterComponent::s_itemFunc[section])(itemSlot, displayRec, true);
        }
    }
}

void CCharacterComponent::AddItemBySlot(INVENTORY_SLOTS invSlot, int32_t displayID, int32_t a4) {
    if (invSlot > EQUIPPED_LAST) {
        return;
    }

    STORM_ASSERT(displayID > 0);

    switch (invSlot) {
    case INVSLOT_HEAD:
        this->AddItem(ITEMSLOT_0, displayID, a4);
        break;

    case INVSLOT_SHOULDER:
        this->AddItem(ITEMSLOT_1, displayID, a4);
        break;

    case INVSLOT_BODY:
        this->AddItem(ITEMSLOT_2, displayID, a4);
        break;

    case INVSLOT_CHEST:
        this->AddItem(ITEMSLOT_3, displayID, a4);
        break;

    case INVSLOT_WAIST:
        this->AddItem(ITEMSLOT_4, displayID, a4);
        break;

    case INVSLOT_LEGS:
        this->AddItem(ITEMSLOT_5, displayID, a4);
        break;

    case INVSLOT_FEET:
        this->AddItem(ITEMSLOT_6, displayID, a4);
        break;

    case INVSLOT_WRIST:
        this->AddItem(ITEMSLOT_7, displayID, a4);
        break;

    case INVSLOT_HAND:
        this->AddItem(ITEMSLOT_8, displayID, a4);
        break;

    case INVSLOT_BACK:
        this->AddItem(ITEMSLOT_10, displayID, a4);
        break;

    case INVSLOT_TABARD:
        this->AddItem(ITEMSLOT_9, displayID, a4);
        break;

    default:
        break;
    }
}

void CCharacterComponent::ClearItemDisplay(COMPONENT_SECTIONS section, int32_t priority) {
    if (priority == -1) {
        return;
    }

    if (this->m_itemDisplays[section].texture[priority]) {
        TextureCacheDestroyTexture(this->m_itemDisplays[section].texture[priority]);
        this->m_itemDisplays[section].texture[priority] = nullptr;
    }

    this->m_itemDisplays[section].displayID[priority] = 0;

    this->m_itemDisplays[section].priorityDirty &= ~(1 << priority);
}

void CCharacterComponent::CreateBaseTexture() {
    auto dataFormat = this->m_textureFormat == GxTex_Dxt1
        ? GxTex_Dxt1
        : GxTex_Argb8888;

    CGxTexFlags flags = CGxTexFlags(GxTex_LinearMipLinear, 0, 0, 0, 0, 0, 1);
    if (GxDevApi() == GxApi_GLL) {
        flags.m_bit15 = 1;
    }

    auto baseTexture = TextureCreate(
       CCharacterComponent::s_textureSize,
       CCharacterComponent::s_textureSize,
       this->m_textureFormat,
       dataFormat,
       flags,
       this,
       &CCharacterComponent::UpdateBaseTexture,
       "CharacterBaseSkin",
       1
    );

    this->m_baseTexture = baseTexture;

    this->m_data.model->ReplaceTexture(1, this->m_baseTexture);
}

void* CCharacterComponent::CreateTexture(const ItemDisplayInfoRec* displayRec, int32_t section) {
    SStrPrintf(
        s_path,
        STORM_MAX_PATH,
        "Item\\TextureComponents\\%s\\%s_%s.blp",
        s_componentSections[section],
        displayRec->m_texture[section],
        s_fileDecorations[2]
    );

    // Substitute gender suffix
    if (!SFile::FileExists(s_path)) {
        s_path[SStrLen(s_path) - 5] = *s_fileDecorations[this->m_data.sexID];
    }

    return TextureCacheCreateTexture(s_path);
}

void CCharacterComponent::GeosRenderPrep() {
    // Check for eye glow

    bool eyeGlow = false;

    if (this->m_data.classID == 6) {
        eyeGlow = true;
    } else {
        auto sectionsRec = this->GetSectionsRecord(VARIATION_FACE, this->m_data.faceID, this->m_data.skinColorID, nullptr);

        if (sectionsRec && sectionsRec->m_flags & 0x4) {
            eyeGlow = true;
        }
    }

    // Hide all geosets (0 - 2000)

    this->m_data.model->SetGeometryVisible(0, 2000, 0);

    // Show base skin geoset (0)

    this->m_data.model->SetGeometryVisible(0, 0, 1);

    // Show all enabled geosets

    for (int32_t geoset = 0; geoset < NUM_GEOSET; geoset++) {
        if (geoset == GEOSET_EYE_EFFECTS && eyeGlow) {
            this->m_data.model->SetGeometryVisible(1703, 1703, 1);
        } else {
            this->m_data.model->SetGeometryVisible(this->m_data.geosets[geoset], this->m_data.geosets[geoset], 1);
        }
    }

    // TODO

    this->m_flags &= ~0x4;
}

void CCharacterComponent::GetPreferences(CharacterPreferences* preferences) {
    if (!preferences) {
        return;
    }

    preferences->raceID = this->m_data.raceID;
    preferences->sexID = this->m_data.sexID;
    preferences->classID = this->m_data.classID;
    preferences->hairColorID = this->m_data.hairColorID;
    preferences->skinColorID = this->m_data.skinColorID;
    preferences->faceID = this->m_data.faceID;
    preferences->facialHairStyleID = this->m_data.facialHairStyleID;
    preferences->hairStyleID = this->m_data.hairStyleID;
}

CharSectionsRec* CCharacterComponent::GetSectionsRecord(COMPONENT_VARIATIONS sectionIndex, int32_t variationIndex, int32_t colorIndex, bool* found) {
    return ComponentGetSectionsRecord(
        CCharacterComponent::s_chrVarArray,
        this->m_data.raceID,
        this->m_data.sexID,
        sectionIndex,
        variationIndex,
        colorIndex,
        found
    );
}

int32_t CCharacterComponent::Init(ComponentData* data, const char* a3) {
    // If existing model is present, release it before copying in new data
    if (this->m_data.model) {
        this->m_data.model->Release();
    }

    this->m_data = *data;

    // TODO

    this->SetSkinColor(this->m_data.skinColorID, false, true, a3);
    this->SetHairStyle(this->m_data.hairStyleID, a3);
    this->SetBeardStyle(this->m_data.facialHairStyleID, false, a3);

    return 1;
}

int32_t CCharacterComponent::ItemsLoaded(int32_t a2) {
    if (a2) {
        TCTEXTUREINFO info;

        for (int32_t section = 0; section < NUM_COMPONENT_SECTIONS; section++) {
            if (!(this->m_sectionDirty & (1 << section))) {
                continue;
            }

            auto& itemDisplay = this->m_itemDisplays[section];

            for (int32_t priority = 0; priority < 7; priority++) {
                if (!itemDisplay.displayID[priority] && !itemDisplay.texture[priority]) {
                    continue;
                }

                if (itemDisplay.displayID[priority] && !itemDisplay.texture[priority]) {
                    auto displayRec = g_itemDisplayInfoDB.GetRecord(itemDisplay.displayID[priority]);
                    itemDisplay.texture[priority] = this->CreateTexture(displayRec, section);
                }

                // Trigger texture load
                TextureCacheGetInfo(itemDisplay.texture[priority], info, 1);

                if (TextureCacheHasMips(itemDisplay.texture[priority])) {
                    itemDisplay.priorityDirty |= (1 << priority);
                } else {
                    itemDisplay.priorityDirty &= ~(1 << priority);
                }
            }
        }

        return 1;
    }

    // TODO

    return 1;
}

void CCharacterComponent::LoadBaseVariation(COMPONENT_VARIATIONS sectionIndex, int32_t textureIndex, int32_t variationIndex, int32_t colorIndex, COMPONENT_SECTIONS section, const char* a7) {
    int32_t index = TEXTURE_INDEX(sectionIndex, textureIndex);

    if (this->m_texture[index]) {
        TextureCacheDestroyTexture(this->m_texture[index]);
    }

    auto valid = ComponentValidateBase(
        CCharacterComponent::s_chrVarArray,
        this->m_data.raceID,
        this->m_data.sexID,
        sectionIndex,
        variationIndex,
        colorIndex
    );
    if (!valid) {
        return;
    }

    auto sectionsRec = this->GetSectionsRecord(sectionIndex, variationIndex, colorIndex, nullptr);

    auto textureName = sectionsRec->m_textureName[textureIndex];

    if (*textureName) {
        SStrCopy(s_pathEnd, textureName);
        this->m_texture[index] = TextureCacheCreateTexture(s_path);
    }

    this->m_sectionDirty |= 1 << section;

    // TODO

    this->m_flags &= ~0x8;
}

void CCharacterComponent::PrepSections() {
    // TODO
}

int32_t CCharacterComponent::RenderPrep(int32_t a2) {
    if (this->m_data.flags & 0x1) {
        if (this->m_flags & 0x4) {
            this->GeosRenderPrep();
        }

        return 1;
    }

    // TODO

    if (a2) {
        // TODO

        this->VariationsLoaded(1);
        this->ItemsLoaded(1);

        this->m_flags |= 8u;

        this->RenderPrepSections();
        // TODO this->Sub79F820();

        return 1;
    }

    // TODO
    return 1;
}

void CCharacterComponent::RenderPrepAL() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_ARM_LOWER];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_ARM_LOWER, skinTexture, CCharacterComponent::s_textureBuffer);

    // Item textures

    for (int32_t priority = 0; priority < SECTION_AL_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_ARM_LOWER, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepAU() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_ARM_UPPER];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_ARM_UPPER, skinTexture, CCharacterComponent::s_textureBuffer);

    // Item textures

    for (int32_t priority = 0; priority < SECTION_AU_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_ARM_UPPER, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepFO() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_FOOT];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_FOOT, skinTexture, CCharacterComponent::s_textureBuffer);

    // Item textures

    for (int32_t priority = 0; priority < SECTION_FO_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_FOOT, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepHA() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_HAND];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_HAND, skinTexture, CCharacterComponent::s_textureBuffer);

    // Item textures

    for (int32_t priority = 0; priority < SECTION_HA_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_HAND, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepHL() {
    auto sectionsRec = this->GetSectionsRecord(VARIATION_SKIN, 0, this->m_data.skinColorID, nullptr);

    // Skin texture

    if (sectionsRec && sectionsRec->m_flags & 0x8) {
        auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
        CCharacterComponent::PasteFromSkin(SECTION_HEAD_LOWER, skinTexture, CCharacterComponent::s_textureBuffer);
    }

    // Face texture

    auto faceLowerTexture = this->m_texture[TEXTURE_INDEX(VARIATION_FACE, 0)];
    if (faceLowerTexture) {
        CCharacterComponent::PasteToSection(SECTION_HEAD_LOWER, faceLowerTexture, CCharacterComponent::s_textureBuffer);
    }

    // Hair textures

    auto facialHairLowerTexture = this->m_texture[TEXTURE_INDEX(VARIATION_FACIAL_HAIR, 0)];
    if (facialHairLowerTexture) {
        CCharacterComponent::PasteToSection(SECTION_HEAD_LOWER, facialHairLowerTexture, CCharacterComponent::s_textureBuffer);
    }

    auto hairLowerTexture = this->m_texture[TEXTURE_INDEX(VARIATION_HAIR, 1)];
    if (hairLowerTexture) {
        CCharacterComponent::PasteToSection(SECTION_HEAD_LOWER, hairLowerTexture, CCharacterComponent::s_textureBuffer);
    }
}

void CCharacterComponent::RenderPrepHU() {
    auto sectionsRec = this->GetSectionsRecord(VARIATION_SKIN, 0, this->m_data.skinColorID, nullptr);

    // Skin texture

    if (sectionsRec && sectionsRec->m_flags & 0x8) {
        auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
        CCharacterComponent::PasteFromSkin(SECTION_HEAD_UPPER, skinTexture, CCharacterComponent::s_textureBuffer);
    }

    // Face texture

    auto faceUpperTexture = this->m_texture[TEXTURE_INDEX(VARIATION_FACE, 1)];
    if (faceUpperTexture) {
        CCharacterComponent::PasteToSection(SECTION_HEAD_UPPER, faceUpperTexture, CCharacterComponent::s_textureBuffer);
    }

    // Hair textures

    auto facialHairUpperTexture = this->m_texture[TEXTURE_INDEX(VARIATION_FACIAL_HAIR, 1)];
    if (facialHairUpperTexture) {
        CCharacterComponent::PasteToSection(SECTION_HEAD_UPPER, facialHairUpperTexture, CCharacterComponent::s_textureBuffer);
    }

    auto hairUpperTexture = this->m_texture[TEXTURE_INDEX(VARIATION_HAIR, 2)];
    if (hairUpperTexture) {
        CCharacterComponent::PasteToSection(SECTION_HEAD_UPPER, hairUpperTexture, CCharacterComponent::s_textureBuffer);
    }
}

void CCharacterComponent::RenderPrepLL() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_LEG_LOWER];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_LEG_LOWER, skinTexture, CCharacterComponent::s_textureBuffer);

    // Item textures

    auto firstPriority = 0;
    auto itemPriorities = SECTION_LL_ITEM_PRIORITIES;

    if (this->m_flags & 0x20) {
        firstPriority = 1;
        itemPriorities = SECTION_LL_ITEM_PRIORITIES - 2;
    }

    for (int32_t priority = firstPriority; priority < itemPriorities; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_LEG_LOWER, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepLU() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_LEG_UPPER];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_LEG_UPPER, skinTexture, CCharacterComponent::s_textureBuffer);

    // Underwear texture

    if ((this->m_flags & 0x20) || !(itemDisplay.priorityDirty & ((1 << 0) | (1 << 1)))) {
        auto bottomUnderwearTexture = this->m_texture[TEXTURE_INDEX(VARIATION_UNDERWEAR, 0)];
        if (bottomUnderwearTexture) {
            CCharacterComponent::PasteToSection(SECTION_LEG_UPPER, bottomUnderwearTexture, CCharacterComponent::s_textureBuffer);
        }
    }

    // Item textures

    auto firstPriority = (this->m_flags & 0x20) ? 1 : 0;

    for (int32_t priority = firstPriority; priority < SECTION_LU_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_LEG_UPPER, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepTL() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_TORSO_LOWER];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_TORSO_LOWER, skinTexture, CCharacterComponent::s_textureBuffer);

    // Item textures

    for (int32_t priority = 0; priority < SECTION_TL_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_TORSO_LOWER, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepTU() {
    auto& itemDisplay = this->m_itemDisplays[SECTION_TORSO_UPPER];

    // Skin texture

    auto skinTexture = this->m_texture[TEXTURE_INDEX(VARIATION_SKIN, 0)];
    CCharacterComponent::PasteFromSkin(SECTION_TORSO_UPPER, skinTexture, CCharacterComponent::s_textureBuffer);

    // Underwear texture

    if (!(itemDisplay.priorityDirty & ((1 << 0) | (1 << 1) | (1 << 2)))) {
        auto topUnderwearTexture = this->m_texture[TEXTURE_INDEX(VARIATION_UNDERWEAR, 1)];
        if (topUnderwearTexture) {
            CCharacterComponent::PasteToSection(SECTION_TORSO_UPPER, topUnderwearTexture, CCharacterComponent::s_textureBuffer);
        }
    }

    // Item textures

    for (int32_t priority = 0; priority < SECTION_TU_ITEM_PRIORITIES; priority++) {
        if (itemDisplay.priorityDirty & (1 << priority)) {
            CCharacterComponent::PasteToSection(SECTION_TORSO_UPPER, itemDisplay.texture[priority], CCharacterComponent::s_textureBuffer);
        }
    }
}

void CCharacterComponent::RenderPrepAll() {
    // TODO

    this->m_flags &= ~0x8;

    this->VariationsLoaded(1);
    this->ItemsLoaded(1);

    for (uint32_t i = 0; i < NUM_COMPONENT_SECTIONS; i++) {
        (this->*CCharacterComponent::s_prepFunc[i])();
    }

    // TODO

    this->m_flags &= ~0x1;

    this->m_sectionDirty = 0;

    // TODO
}

void CCharacterComponent::RenderPrepSections() {
    s_bInRenderPrep = 1;

    if (this->m_flags & 0x4) {
        this->GeosRenderPrep();
    }

    if (!this->m_baseTexture) {
        this->CreateBaseTexture();
    }

    this->PrepSections();

    this->m_flags &= ~0x1;

    this->m_sectionDirty = 0;

    // TODO

    s_bInRenderPrep = 0;
}

void CCharacterComponent::ReplaceExtraSkinTexture(const char* a2) {
    if (!ComponentValidateBase(
       CCharacterComponent::s_chrVarArray,
       this->m_data.raceID,
       this->m_data.sexID,
       VARIATION_SKIN,
       0,
       this->m_data.hairColorID
    )) {
        return;
    }

    auto sectionsRec = this->GetSectionsRecord(VARIATION_SKIN, 0, this->m_data.skinColorID, nullptr);

    if (!*sectionsRec->m_textureName[1]) {
        return;
    }

    SStrCopy(s_pathEnd, sectionsRec->m_textureName[1]);

    auto extraSkinTexture = CCharacterComponent::CreateTexture(s_path, &s_status);

    if (extraSkinTexture) {
        this->m_data.model->ReplaceTexture(8, extraSkinTexture);
        HandleClose(extraSkinTexture);
    }
}

void CCharacterComponent::ReplaceHairTexture(int32_t hairStyleID, const char* a3) {
    if (!ComponentValidateBase(
       CCharacterComponent::s_chrVarArray,
       this->m_data.raceID,
       this->m_data.sexID,
       VARIATION_HAIR,
       hairStyleID,
       this->m_data.hairColorID
    )) {
        return;
    }

    auto sectionsRec = this->GetSectionsRecord(VARIATION_HAIR, hairStyleID, this->m_data.hairColorID, nullptr);
    if (!*sectionsRec->m_textureName[0]) {
        return;
    }

    SStrCopy(s_pathEnd, sectionsRec->m_textureName[0]);

    auto hairTexture = CCharacterComponent::CreateTexture(s_path, &s_status);
    if (hairTexture) {
        this->m_data.model->ReplaceTexture(6, hairTexture);
        HandleClose(hairTexture);
    }
}

void CCharacterComponent::SetBeardStyle(int32_t facialHairStyleID, bool a3, const char* a4) {
    auto listIndex = this->m_data.raceID * 2 + this->m_data.sexID;
    if (facialHairStyleID < 0 || facialHairStyleID > CCharacterComponent::s_characterFacialHairStylesList[listIndex]) {
        return;
    }

    this->m_data.facialHairStyleID = facialHairStyleID;

    auto facialHairStyleRec = ComponentGetFacialHairStyleRecord(&this->m_data);

    if (facialHairStyleRec) {
        this->m_data.geosets[1] = 100 + facialHairStyleRec->m_geoset[0];
        this->m_data.geosets[2] = 200 + facialHairStyleRec->m_geoset[2];
        this->m_data.geosets[3] = 300 + facialHairStyleRec->m_geoset[1];
        this->m_data.geosets[16] = 1600 + facialHairStyleRec->m_geoset[3];
        this->m_data.geosets[17] = 1700 + facialHairStyleRec->m_geoset[4];

        this->m_flags |= 0x4;

        if (
            (facialHairStyleRec->m_geoset[0] || facialHairStyleRec->m_geoset[1] || facialHairStyleRec->m_geoset[2])
            && (!this->m_texture[TEXTURE_INDEX(VARIATION_HAIR, 1)] || !this->m_texture[TEXTURE_INDEX(VARIATION_HAIR, 2)])
        ) {
            this->ReplaceHairTexture(1, a4);
        }
    }

    bool isNPC = this->m_data.flags & 0x1;

    if (!isNPC) {
        if (a3) {
            this->LoadBaseVariation(VARIATION_FACIAL_HAIR, 0, this->m_data.facialHairStyleID, this->m_data.hairColorID, SECTION_HEAD_LOWER, a4);
            this->LoadBaseVariation(VARIATION_FACIAL_HAIR, 1, this->m_data.facialHairStyleID, this->m_data.hairColorID, SECTION_HEAD_UPPER, a4);
        }

        this->m_sectionDirty |= (1 << SECTION_HEAD_LOWER) | (1 << SECTION_HEAD_UPPER);

        // TODO component request logic

        this->m_flags &= ~0x8;
    }
}

void CCharacterComponent::SetFace(int32_t faceID, bool a3, const char* a4) {
    bool isNPC = this->m_data.flags & 0x1;

    if (isNPC) {
        return;
    }

    auto skinSectionsRec = this->GetSectionsRecord(VARIATION_SKIN, 0, this->m_data.skinColorID, nullptr);

    if (skinSectionsRec && skinSectionsRec->m_flags & 0x8) {
        return;
    }

    if (!ComponentValidateBase(CCharacterComponent::s_chrVarArray, this->m_data.raceID, this->m_data.sexID, VARIATION_FACE, faceID, this->m_data.skinColorID)) {
        return;
    }

    this->m_data.faceID = faceID;

    this->LoadBaseVariation(VARIATION_FACE, 0, this->m_data.faceID, this->m_data.skinColorID, SECTION_HEAD_LOWER, a4);
    this->LoadBaseVariation(VARIATION_FACE, 1, this->m_data.faceID, this->m_data.skinColorID, SECTION_HEAD_UPPER, a4);

    if (a3) {
        this->LoadBaseVariation(VARIATION_HAIR, 1, this->m_data.hairStyleID, this->m_data.hairColorID, SECTION_HEAD_LOWER, a4);
        this->LoadBaseVariation(VARIATION_HAIR, 2, this->m_data.hairStyleID, this->m_data.hairColorID, SECTION_HEAD_UPPER, a4);
    }

    this->m_flags |= 0x4;
    this->m_sectionDirty |= (1 << SECTION_HEAD_LOWER) | (1 << SECTION_HEAD_UPPER);

    // TODO

    this->m_flags &= ~0x8;
}

void CCharacterComponent::SetHairColor(int32_t hairColorID, bool a3, const char* a4) {
    if (!ComponentValidateBase(
        CCharacterComponent::s_chrVarArray,
        this->m_data.raceID,
        this->m_data.sexID,
        VARIATION_HAIR,
        this->m_data.hairStyleID,
        hairColorID
    )) {
        return;
    }

    this->m_data.hairColorID = hairColorID;

    auto raceRec = g_chrRacesDB.GetRecord(this->m_data.raceID);

    if (this->m_data.sexID == UNITSEX_MALE && this->m_data.hairStyleID == 0 && raceRec->m_flags & 0x8) {
        this->ReplaceHairTexture(1, a4);
    }

    this->ReplaceHairTexture(this->m_data.hairStyleID, a4);

    bool isNPC = this->m_data.flags & 0x1;

    if (!isNPC) {
        if (a3) {
            this->LoadBaseVariation(VARIATION_HAIR, 1, this->m_data.hairStyleID, this->m_data.hairColorID, SECTION_HEAD_LOWER, a4);
            this->LoadBaseVariation(VARIATION_HAIR, 2, this->m_data.hairStyleID, this->m_data.hairColorID, SECTION_HEAD_UPPER, a4);
        }

        this->LoadBaseVariation(VARIATION_FACIAL_HAIR, 0, this->m_data.facialHairStyleID, this->m_data.hairColorID, SECTION_HEAD_LOWER, a4);
        this->LoadBaseVariation(VARIATION_FACIAL_HAIR, 1, this->m_data.facialHairStyleID, this->m_data.hairColorID, SECTION_HEAD_UPPER, a4);
    }
}

void CCharacterComponent::SetHairStyle(int32_t hairStyleID, const char* a3) {
    if (!ComponentValidateBase(
        CCharacterComponent::s_chrVarArray,
        this->m_data.raceID,
        this->m_data.sexID,
        VARIATION_HAIR,
        hairStyleID,
        this->m_data.hairColorID
    )) {
        return;
    }

    this->m_data.hairStyleID = hairStyleID;

    auto hairGeoset = ComponentGetHairGeoset(&this->m_data);
    this->m_data.geosets[0] = hairGeoset;

    bool isNPC = this->m_data.flags & 0x1;

    if (!isNPC) {
        this->LoadBaseVariation(VARIATION_HAIR, 1, this->m_data.hairStyleID, this->m_data.hairColorID, SECTION_HEAD_LOWER, a3);
        this->LoadBaseVariation(VARIATION_HAIR, 2, this->m_data.hairStyleID, this->m_data.hairColorID, SECTION_HEAD_UPPER, a3);
    }

    this->SetHairColor(this->m_data.hairColorID, false, a3);

    this->m_flags |= 0x4;
    this->m_sectionDirty |= (1 << SECTION_HEAD_LOWER) | (1 << SECTION_HEAD_UPPER);

    // TODO component request logic

    this->m_flags &= ~0x8;
}

void CCharacterComponent::SetSkinColor(int32_t skinColorID, bool a3, bool a4, const char* a5) {
    bool isNPC = this->m_data.flags & 0x1;

    this->m_data.skinColorID = skinColorID;

    if (isNPC) {
        return;
    }

    if (!ComponentValidateBase(CCharacterComponent::s_chrVarArray, this->m_data.raceID, this->m_data.sexID, VARIATION_SKIN, 0, skinColorID)) {
        return;
    }

    auto numColors = ComponentGetNumColors(
        CCharacterComponent::s_chrVarArray,
        this->m_data.raceID,
        this->m_data.sexID,
        VARIATION_SKIN,
        0
    );

    auto sectionsRec = this->GetSectionsRecord(VARIATION_SKIN, 0, skinColorID, nullptr);

    if (skinColorID < numColors && sectionsRec && !(sectionsRec->m_flags & 0x8)) {
        auto underwearRec = this->GetSectionsRecord(VARIATION_UNDERWEAR, 0, skinColorID, nullptr);

        auto t0 = TEXTURE_INDEX(VARIATION_UNDERWEAR, 0);
        auto t1 = TEXTURE_INDEX(VARIATION_UNDERWEAR, 1);

        if (this->m_texture[t0]) {
            TextureCacheDestroyTexture(this->m_texture[t0]);
            this->m_texture[t0] = nullptr;
        }

        if (this->m_texture[t1]) {
            TextureCacheDestroyTexture(this->m_texture[t1]);
            this->m_texture[t1] = nullptr;
        }

        if (*underwearRec->m_textureName[0]) {
            SStrCopy(s_pathEnd, underwearRec->m_textureName[0]);
            this->m_texture[t0] = TextureCacheCreateTexture(s_path);
            STORM_ASSERT(this->m_texture[t0]);
        }

        if (*underwearRec->m_textureName[1]) {
            SStrCopy(s_pathEnd, underwearRec->m_textureName[1]);
            this->m_texture[t1] = TextureCacheCreateTexture(s_path);
            STORM_ASSERT(this->m_texture[t1]);
        }
    }

    this->ReplaceExtraSkinTexture(a5);

    this->LoadBaseVariation(
        VARIATION_SKIN,
        0,
        0,
        this->m_data.skinColorID,
        SECTION_TORSO_UPPER,
        a5
    );

    this->SetFace(this->m_data.faceID, a3, a5);

    if (a4) {
        this->m_flags |= 0x1;

        // TODO component request logic

        this->m_flags &= ~0x8;
    }

    this->m_sectionDirty = -1;

    // TODO component request logic

    this->m_flags &= ~0x8;
}

void CCharacterComponent::UpdateItem(ITEM_SLOT itemSlot, COMPONENT_SECTIONS section, const ItemDisplayInfoRec* displayRec, bool update) {
    auto priority = s_itemPriority[itemSlot][section];

    if (section == SECTION_ARM_LOWER) {
        if (displayRec && displayRec->m_geosetGroup[0]) {
            if (itemSlot == ITEMSLOT_3) {
                priority = 5;
            } else if (itemSlot == ITEMSLOT_8) {
                priority = 6;
            }
        }
    }

    if (section == SECTION_LEG_LOWER) {
        if (itemSlot == ITEMSLOT_3 && displayRec && displayRec->m_geosetGroup[2]) {
            priority = 4;

            this->m_flags |= 0x4;
        }

        if (itemSlot == ITEMSLOT_5 && displayRec && displayRec->m_geosetGroup[2]) {
            // TODO

            this->m_flags |= 0x4;
        }

        if (itemSlot == ITEMSLOT_6 && displayRec && displayRec->m_geosetGroup[0]) {
            priority = 3;

            this->m_flags |= 0x4;
        }
    }

    if (section == SECTION_FOOT) {
        if (this->m_flags & 0x10) {
            update = false;
        }
    }

    if (update) {
        if (!this->UpdateItemDisplay(section, displayRec, priority)) {
            return;
        }
    } else {
        this->ClearItemDisplay(section, priority);

        if (section == SECTION_LEG_LOWER) {
            // TODO
        }
    }

    if (priority != -1) {
        this->m_sectionDirty |= (1 << section);

        // TODO component request logic

        this->m_flags &= ~0x8;
    }
}

void CCharacterComponent::UpdateItemAL(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_ARM_LOWER, displayRec, update);
}

void CCharacterComponent::UpdateItemAU(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_ARM_UPPER, displayRec, update);
}

void CCharacterComponent::UpdateItemFO(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_FOOT, displayRec, update);
}

void CCharacterComponent::UpdateItemHA(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_HAND, displayRec, update);
}

void CCharacterComponent::UpdateItemHL(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    // No item displays for head sections
}

void CCharacterComponent::UpdateItemHU(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    // No item displays for head sections
}

void CCharacterComponent::UpdateItemLL(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_LEG_LOWER, displayRec, update);
}

void CCharacterComponent::UpdateItemLU(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_LEG_UPPER, displayRec, update);
}

void CCharacterComponent::UpdateItemTL(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_TORSO_LOWER, displayRec, update);
}

void CCharacterComponent::UpdateItemTU(ITEM_SLOT itemSlot, const ItemDisplayInfoRec* displayRec, bool update) {
    this->UpdateItem(itemSlot, SECTION_TORSO_UPPER, displayRec, update);
}

int32_t CCharacterComponent::UpdateItemDisplay(COMPONENT_SECTIONS section, const ItemDisplayInfoRec* newDisplayRec, int32_t priority) {
    bool isNPC = this->m_data.flags & 0x1;

    if (isNPC) {
        return 0;
    }

    if (this->m_itemDisplays[section].displayID[priority]) {
        auto curDisplayID = this->m_itemDisplays[section].displayID[priority];
        auto curDisplayRec = g_itemDisplayInfoDB.GetRecord(curDisplayID);

        // New display is same as old display
        if (curDisplayRec && curDisplayRec->m_texture[section] == newDisplayRec->m_texture[section]) {
            return 0;
        }
    }

    this->m_itemDisplays[section].priorityDirty &= ~(1 << priority);

    if (this->m_itemDisplays[section].texture[priority]) {
        TextureCacheDestroyTexture(this->m_itemDisplays[section].texture[priority]);
        this->m_itemDisplays[section].texture[priority] = nullptr;
    }

    this->m_itemDisplays[section].displayID[priority] = newDisplayRec->m_ID;

    return 1;
}

int32_t CCharacterComponent::VariationsLoaded(int32_t a2) {
    TCTEXTUREINFO info;

    for (int32_t v = 0; v < NUM_COMPONENT_VARIATIONS; v++) {
        for (int32_t t = 0; t < 3; t++) {
            auto texture = this->m_texture[TEXTURE_INDEX(v, t)];

            if (texture && !TextureCacheGetInfo(texture, info, a2)) {
                return 0;
            }
        }
    }

    this->m_flags &= ~0x2;

    return 1;
}
