import os
import vs
import random
import math
from PySide import QtCore
from PySide import QtGui

# To import the shared functions, we need to update sys.path. This is fucking ugly. Python, fix your fucking import system.
# UPDATE JUNE 3: Making this import mod-agnostic and case-insensitive just made it worse.
# Holy fuck Python. FIX YOUR FUCKING IMPORT SYSTEM.

SysPath_StaticCut = "sdktools\\python\\2.7\\win32\\lib\\site-packages\\distribute-0.6.45-py2.7.egg"
newPath = sys.path[0].replace(SysPath_StaticCut,"")

latestTime = 0
latestFilepath = ""

for f in os.listdir(newPath):
	pypath = os.path.join(newPath, f, "scripts\\sfm")
	pyfile = os.path.join(pypath, "lordaardvarkshared.py")

	if os.path.isfile(pyfile):
		modTime = os.path.getmtime(pyfile)

		if modTime > latestTime:
			latestTime = modTime
			latestFilepath = pypath

sys.path.append(latestFilepath)

for f in os.listdir(latestFilepath):
	if f == "lordaardvarkshared.py":
		import lordaardvarkshared as LordAardvarkShared

	if f == "LordAardvarkShared.py":
		import LordAardvarkShared

# Actual code.
# Because holy shit that import code got ugly.
# Jesus Fucking Christ.

## Conversions
def QColorToVec3(inClr):
    clrRgb = inClr.getRgb()
    
    clrVec = vs.Vector(clrRgb[0] / 255.0, clrRgb[1] / 255.0, clrRgb[2] / 255.0)
    
    return clrVec
    
    
# Classes
class HairStyle:
    def __init__(self, displayName, hairModel, hairBodygroupSet, hairBodygroupElement):
        global HMF_HAIR_STYLES
        
        self.displayName = displayName
        self.hairModel = hairModel
        self.bodygroupSet = hairBodygroupSet
        self.bodygroupElement = hairBodygroupElement
        
        HMF_HAIR_STYLES[displayName] = self
        
class GenericCharacter:
    def __init__(self, headName, bodyTypeName, headNameInternal, bodyTypeInternal, lstSkintoneMaterialIgnoreFilter, lstFaceMaps, faceMatName, lstEyeTextures, lstEyeMatNames, mdlName):
        self.headName = headName
        self.headNameInternal = headNameInternal
        self.bodyTypeName = bodyTypeName
        self.bodyTypeInternal = bodyTypeInternal
        self.faceMaps = {}
        self.faceMatName = faceMatName
        self.eyeTextures = {}
        self.eyeMatNames = lstEyeMatNames
        self.modelName = mdlName
        self.skinToneIgnoreFilters = lstSkintoneMaterialIgnoreFilter
        
        for faceMap in lstFaceMaps:
            self.faceMaps[faceMap.displayName] = faceMap
        
        for eyeTex in lstEyeTextures:
            self.eyeTextures[eyeTex.displayName] = eyeTex
            
        HMF_GENERIC_CHARACTERS[GenericCharacter.GetKey(headNameInternal, bodyTypeInternal)] = self
        
    @staticmethod
    def GetKey(headNameInternal, bodyTypeInternal):
        return headNameInternal + "_" + bodyTypeInternal
        
    def ContextualizeString(self, rawStr):
        outStr = rawStr
        
        for genCharKey in HMF_GENERIC_CHARACTERS:
            genChar = HMF_GENERIC_CHARACTERS[genCharKey]
            
            outStr = outStr.replace(genChar.headNameInternal, self.headNameInternal)
            outStr = outStr.replace(genChar.bodyTypeInternal, self.bodyTypeInternal)
            
        return outStr
        
class ParameterPair:
    def __init__(self, paramName, paramValue):
        self.materialOverride = None
        self.parameterName = paramName
        self.parameterValue = paramValue
        
    def Copy(self):
        newParam = ParameterPair(self.parameterName, self.parameterValue)
        return newParam
        
    def Apply(self, mat):
        paramValue = self.parameterValue
        
        if type(self.parameterValue) == str:
            paramValue = paramValue.replace(HMF_COMMON_PATH_SHORTHAND, self.materialOverride.materialOverrideSet.commonTexturePath)
            
            mat.AddAttributeAsString(self.parameterName)
            
        if type(self.parameterValue) == int:
            mat.AddAttributeAsBool(self.parameterName)
            
        if type(self.parameterValue) == float:
            mat.AddAttributeAsFloat(self.parameterName)
            
        if type(self.parameterValue) == vs.Vector:
            mat.AddAttributeAsVector3(self.parameterName)
            
        mat.SetValue(self.parameterName, paramValue)
            
class Tint:
    def __init__(self, displayName, colorData):
        self.displayName = displayName
        
        if type(colorData) == str:
            self.colorHex = colorData
            self.colorObj = QtGui.QColor("#" + colorData)
            self.colorVector = QColorToVec3(self.colorObj)
            
        if type(colorData) == list:
            self.colorObj = QtGui.QColor.fromRgbF(colorData[0] / 255.0, colorData[1] / 255.0, colorData[2] / 255.0)
            self.colorHex = self.colorObj.name()
            self.colorVector = vs.Vector(colorData[0] / 255.0, colorData[1] / 255.0, colorData[2] / 255.0)
            
            if self.colorObj.red() != colorData[0]: # This version of PyQt does not have extended rgb, so we're just going to null them out and run off the colorVector. Not ideal, but it is what it is
                self.colorObj = None
                self.colorHex = None
                
        
class MaterialName:
    def __init__(self, internalName, displayName):
        self.internalName = internalName
        self.displayName = displayName
        
class MaterialOverride:
    def __init__(self, displayName, lstParamPairs, lstTints):
        self.displayName = displayName
        self.materialOverrideSet = None
        self.parameterPairs = {}
        self.tints = {}
        
        self.tintNames = []
        
        for tint in lstTints:
            self.tints[tint.displayName] = tint
            self.tintNames.append(tint.displayName)

        for paramPair in HMF_OVERRIDE_DEFAULTS:
            newParamPair = paramPair.Copy()
            newParamPair.materialOverride = self
            self.parameterPairs[paramPair.parameterName] = newParamPair
                
        for paramPair in lstParamPairs:
            paramPair.materialOverride = self
            self.parameterPairs[paramPair.parameterName] = paramPair        
            
    def Apply(self, animset, tintName):
        matMap = GetOrCreateMaterialOverrides(animset)
        
        matName = self.materialOverrideSet.materialName
        
        if matName not in matMap:
            return None
            
        mat = matMap[matName]
        
        for paramPairName in self.parameterPairs:
            paramPair = self.parameterPairs[paramPairName]
            
            paramPair.Apply(mat)
            
        if tintName is not None:
            tint = self.tints[tintName]

            mat.AddAttributeAsVector3("$color2")
            mat.SetValue("$color2",tint.colorVector)
            
    def ApplyDefault(self, animset):
        if len(self.tintNames) > 0:
            self.Apply(animset, self.tintNames[0])
            return
            
        self.Apply(animset, None)

class MaterialOverrideSet:
    def __init__(self, displayName, materialName, commonTexturePath, lstMaterialOverrides):
        self.displayName = displayName
        self.materialName = materialName
        self.commonTexturePath = commonTexturePath
        self.materialOverrides = {}
        
        self.materialOverrideNames = []
        
        for matOverride in lstMaterialOverrides:
            matOverride.materialOverrideSet = self
            self.materialOverrides[matOverride.displayName] = matOverride
            self.materialOverrideNames.append(matOverride.displayName)
        
class BodygroupData:
    def __init__(self, bodygroupSetDisplay, bodygroupSetInternal, bodygroupElementDisplay, bodygroupElementInternal, lstMaterialOverrideSets):
        self.bodygroupSetDisplay = bodygroupSetDisplay
        self.bodygroupSetInternal = bodygroupSetInternal
        self.bodygroupElementDisplay = bodygroupElementDisplay
        self.bodygroupElementInternal = bodygroupElementInternal
        self.materialOverrideSets = {}
        
        self.materialOverrideSetNames = []
        
        for matOverrideSet in lstMaterialOverrideSets:
            self.materialOverrideSets[matOverrideSet.displayName] = matOverrideSet
            self.materialOverrideSetNames.append(matOverrideSet.displayName)
        
    @staticmethod
    def GetKey(bodygroupSetDisplay, bodygroupElementDisplay):
        return bodygroupSetDisplay + "_" + bodygroupElementDisplay
        
    def Apply(self, animset, matOverrideSetName, matOverrideName, tintName):
        parentAnimset = getattr(animset, "parentAnimset")
        genChar = GetGenericCharacterFromAnimset(parentAnimset)
        
        SetActiveBodygroup(animset, self.bodygroupSetInternal, genChar.ContextualizeString(self.bodygroupElementInternal))
        
        matOverrideSet = self.materialOverrideSets[matOverrideSetName]
        matOverride = matOverrideSet.materialOverrides[matOverrideName]
        matOverride.Apply(animset, tintName)
        
    def ApplyDefault(self, animset):
        for setName in self.materialOverrideSets:
            overrideSet = self.materialOverrideSets[setName]
            firstOverrideName = overrideSet.materialOverrideNames[0]
            
            matOverride = overrideSet.materialOverrides[firstOverrideName]
            matOverride.ApplyDefault(animset)
        
class Outfit:
    global HMF_OUTFITS
    
    def __init__(self, displayName, mdlName, lstBodygroupDatas):
        self.displayName = displayName
        self.mdlName = mdlName
        self.bodygroupDatas = {}
        self.bodygroupSets = {}
        
        self.bodygroupKeys = []
        
        for bodygroupData in lstBodygroupDatas:
            bodygroupKey = BodygroupData.GetKey(bodygroupData.bodygroupSetDisplay, bodygroupData.bodygroupElementDisplay)
            
            self.bodygroupDatas[bodygroupKey] = bodygroupData
            
            if bodygroupData.bodygroupSetDisplay not in self.bodygroupSets:
                self.bodygroupSets[bodygroupData.bodygroupSetDisplay] = {}
                
            if bodygroupData.bodygroupElementDisplay not in self.bodygroupSets[bodygroupData.bodygroupSetDisplay]:
                self.bodygroupSets[bodygroupData.bodygroupSetDisplay][bodygroupData.bodygroupElementDisplay] = []
                
            self.bodygroupSets[bodygroupData.bodygroupSetDisplay][bodygroupData.bodygroupElementDisplay] = bodygroupData
            
            self.bodygroupKeys.append( [bodygroupData.bodygroupSetDisplay, bodygroupData.bodygroupElementDisplay ] )
        
        HMF_OUTFITS[displayName] = self
        
    def ApplyBodygroup(self, animset, bodygroupSetDisplay, bodygroupElementDisplay, matOverrideSetName, matOverrideName, tintName):
        bodygroupData = self.bodygroupDatas[BodygroupData.GetKey(bodygroupSetDisplay, bodygroupElementDisplay)]
        
        bodygroupData.Apply(animset, matOverrideSetName, matOverrideName, tintName)
        
    def ApplyDefault(self, animset):
        for bodygroupKeyPair in self.bodygroupKeys:
            setDisplay, elementDisplay = bodygroupKeyPair
            bodygroupData = self.bodygroupDatas[BodygroupData.GetKey(setDisplay, elementDisplay)]
            
            bodygroupData.ApplyDefault(animset)
        
class FaceMap:
    def __init__(self, displayName, commonTexturePath, baseTexturePath, bumpmapTexturePath):
        self.displayName = displayName
        self.commonTexturePath = commonTexturePath
        self.baseTexturePath = baseTexturePath.replace(HMF_COMMON_PATH_SHORTHAND,commonTexturePath)
        self.bumpmapTexturePath = bumpmapTexturePath.replace(HMF_COMMON_PATH_SHORTHAND,commonTexturePath)
        
class EyeTexture:
    def __init__(self, displayName, irisPath):
        self.displayName = displayName
        self.irisPath = irisPath
        
# Globals
if "HMF_GameModFolderNames" not in globals():
    HMF_GameInfoPath = "usermod/gameinfo.txt"
    HMF_GameModFolderNames = []

HMF_CurCharAnimset = None
HMF_bReportQueue = False

HMF_GENERIC_CHARACTERS = {}
HMF_HAIR_COLORS = {}
HMF_HAIR_STYLES = {}
HMF_OUTFITS = {}

HMF_OVERRIDE_DEFAULTS = [
    ParameterPair("$color2", vs.Vector(1,1,1)),
    ParameterPair("$blendtintbybasealpha", 1 )
]

HMF_COMMON_PATH_SHORTHAND = "*"

# Hard-coded data for testing
HMF_TINT_MUTED = [
    Tint("White", "ffffff"),
    Tint("Light Gray", "bbbbbb"),
    Tint("Pink", "c66c99"),
    Tint("Black", "212426"),
    Tint("Dark Blue", "38414c"),
    Tint("Light Blue", "7197bb"),
    Tint("Dark Red", "832534"),
    Tint("Dark Gray", "5e5f64"),
    Tint("Chestnut", "7a5b55"),
    Tint("Forest Green", "24372f"),
    Tint("Navy Blue", "232d41"),
    Tint("Pistachio", "b0b589"),
    Tint("Tan", "a39160")
]

HMF_TINT_GAUDY = [
    Tint("Cherry Red", "da0c29"),
    Tint("Daisy Yellow", "f5f500"),
    Tint("Neon Blue", "0087c0"),
    Tint("Neon Green", "7cf500"),
    Tint("Safety Green", "d0f600"),
    Tint("Safety Orange", "ea6218"),
    Tint("Safety Pink", "e6487b")
]

HMF_TINT_NONE = []

HMF_HAIR_COLOR_DATA = [
    [ "Mousey", "99794f" ],
    [ "Brunette", "554737" ],
    [ "Auburn", "5c2b05" ],
    [ "Raven", "2d222c" ],
    [ "Ginger", "b5533b" ],
    [ "Platinum", "f3ebe0" ],
    [ "Gold", "e3d29e" ],
    [ "Pale", "f5f0d2" ]
]

HMF_HAIR_STYLE_DATA = [
    HairStyle("Crazy Morning",  "models/lordaardvark/sfm/dazgeneric/female_hitman_head08_hair.mdl", "Hair", "head08_hair_04"),
    HairStyle("Young At Heart", "models/lordaardvark/sfm/dazgeneric/female_hitman_head08_hair.mdl", "Hair", "head08_hair_05"),
    HairStyle("On The Go",      "models/lordaardvark/sfm/dazgeneric/female_hitman_head08_hair.mdl", "Hair", "head08_hair_06"),
    HairStyle("Miss Noir",      "models/lordaardvark/sfm/dazgeneric/female_hitman_head08_hair.mdl", "Hair", "head08_hair_07"),
    HairStyle("No Apologies",   "models/lordaardvark/sfm/dazgeneric/female_hitman_head08_hair.mdl", "Hair", "head08_hair_08"),
    HairStyle("So Sultry",      "models/lordaardvark/sfm/dazgeneric/female_hitman_head08_hair.mdl", "Hair", "head08_hair_09")
]

HMF_SKINTONE_DATA = [
    Tint("Generic Female",          [ 204, 178, 165 ] ),
    Tint("DazV5 Elizabeth",         [ 211, 172, 153 ] ),
    Tint("DazV5 Elizabeth BAS",     [ 267, 280, 291 ] ),
    Tint("DazV5 DOA Mila",          [ 251, 236, 224 ] ),
    Tint("DazV5 Babs",              [ 194, 198, 203 ] ),
    Tint("DazV5 Selina",            [ 251, 270, 270 ] ),
    Tint("DazV5 Lulu",              [ 200, 196, 177 ] ),
    Tint("DazV5 Scarlet",           [ 243, 273, 303 ] ),
    Tint("DazV5 Diana Allers",      [ 230, 240, 225 ] ),
    Tint("DazV5 Femshep",           [ 242, 241, 243 ] ),
    Tint("DazV5 Kasumi Goto",       [ 224, 240, 220 ] ),
    Tint("DazV5 DVa",               [ 246, 283, 303 ] ),
    Tint("DazV5 Mercy",             [ 267, 305, 322 ] ),
    Tint("DazV5 Sombra",            [ 172, 167, 150 ] ),
    Tint("DazV5 Zarya",             [ 264, 282, 296 ] ),
    Tint("DazV5 Claire Redfield",   [ 233, 262, 277 ] ),
    Tint("DazV5 Rebecca Chambers",  [ 181, 203, 219 ] ),
    Tint("DazV5 L4D Zoey",          [ 195, 196, 202 ] ),
    Tint("Uncolored",               [ 255, 255, 255 ] ),
    Tint("Generic Male 1",          [ 211, 201, 187 ] ),
    Tint("Generic Male 2",          [ 211, 190, 180 ] ),
    Tint("Generic Male 3",          [ 67 , 58 , 54  ] ),
    Tint("Generic Male 4",          [ 192, 206, 203 ] ),
    Tint("Generic Male 5",          [ 210, 225, 232 ] ),
    Tint("Generic Male 6",          [ 154, 162, 154 ] ),
    Tint("Generic Male 7",          [ 178, 170, 174 ] ),
    Tint("Generic Male 8",          [ 93 , 97 , 99  ] ),
    Tint("Generic Male 9",          [ 202, 210, 219 ] ),
    Tint("Generic Male 10",         [ 45 , 44 , 43  ] ),
    Tint("Generic Male 10",         [ 45 , 44 , 43  ] ),
    Tint("Sherbecca Espinosa",      [ 146, 142, 127 ] ),
    Tint("Sherbecca Qasim",         [ 124, 114, 101 ] ),
    Tint("Sherbecca Fisher",        [ 206, 194, 194 ] ),
    Tint("Sherbecca Hyton",         [ 211, 201, 187 ] ),
    Tint("Sherbecca Lowry",         [ 206, 194, 194 ] )
]

HMF_FACE_DATA = [
    FaceMap("Head 23", "models/LordAardvark/DazGeneric/FemaleHitman", "*/face_head23_d", "*/face_head23_n"),
    FaceMap("Head 3",  "models/LordAardvark/DazGeneric/FemaleHitman", "*/face_head3_d",  "*/face_head3_n"),
    FaceMap("Head 21", "models/LordAardvark/DazGeneric/FemaleHitman", "*/face_head21_d", "*/face_head21_n"),
    FaceMap("Head 26", "models/LordAardvark/DazGeneric/FemaleHitman", "*/face_head26_d", "*/face_head26_n"),
    FaceMap("Head 33", "models/LordAardvark/DazGeneric/FemaleHitman", "*/face_head33_d", "*/face_head33_n"),
    FaceMap("Head 43", "models/LordAardvark/DazGeneric/FemaleHitman", "*/face_head43_d", "*/face_head43_n"),
]

HMF_EYE_DATA = [
    EyeTexture("Brown",             "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_brown"),
    EyeTexture("Blue-Green",        "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_bluegreen"),
    EyeTexture("Bright Blue",       "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_brightblue"),
    EyeTexture("Bright Orange",     "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_brightorange"),
    EyeTexture("Dark Gray",         "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_darkgray"),
    EyeTexture("Dark Orange",       "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_darkorange"),
    EyeTexture("Deep Pink",         "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_deeppink"),
    EyeTexture("Gray",              "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_gray"),
    EyeTexture("Light Blue",        "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_lightblue"),
    EyeTexture("Light Purple",      "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_lightpurple"),
    EyeTexture("Orange",            "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_orange"),
    EyeTexture("Pink",              "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_pink"),
    EyeTexture("Purple Lined",      "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_purplelined"),
    EyeTexture("Red",               "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_red"),
    EyeTexture("Turquoise",         "models/LordAardvark/DazGeneric/MaleHitman/pupils/pupil_turquoise")
]

HMF_GENERIC_CHARACTER_DATA = [
    GenericCharacter("Head 08", "Average",    "head08", "average",    [ "eyelashes_head08", "teeth_woman_01_d01", "eyeball_l", "eyeball_r" ], HMF_FACE_DATA, "face_d", HMF_EYE_DATA, [ "eyeball_l", "eyeball_r" ], "models/lordaardvark/sfm/dazgeneric/female_hitman_head08-average.mdl"),
    GenericCharacter("Head 08", "Curvy",      "head08", "curvy",      [ "eyelashes_head08", "teeth_woman_01_d01", "eyeball_l", "eyeball_r" ], HMF_FACE_DATA, "face_d", HMF_EYE_DATA, [ "eyeball_l", "eyeball_r" ], "models/lordaardvark/sfm/dazgeneric/female_hitman_head08-curvy.mdl"),
    GenericCharacter("Head 08", "DOAFantasy", "head08", "doafantasy", [ "eyelashes_head08", "teeth_woman_01_d01", "eyeball_l", "eyeball_r" ], HMF_FACE_DATA, "face_d", HMF_EYE_DATA, [ "eyeball_l", "eyeball_r" ], "models/lordaardvark/sfm/dazgeneric/female_hitman_head08-doafantasy.mdl"),
    GenericCharacter("Head 08", "Petite",     "head08", "petite",     [ "eyelashes_head08", "teeth_woman_01_d01", "eyeball_l", "eyeball_r" ], HMF_FACE_DATA, "face_d", HMF_EYE_DATA, [ "eyeball_l", "eyeball_r" ], "models/lordaardvark/sfm/dazgeneric/female_hitman_head08-petite.mdl"),
    GenericCharacter("Head 08", "Strong",     "head08", "strong",     [ "eyelashes_head08", "teeth_woman_01_d01", "eyeball_l", "eyeball_r" ], HMF_FACE_DATA, "face_d", HMF_EYE_DATA, [ "eyeball_l", "eyeball_r" ], "models/lordaardvark/sfm/dazgeneric/female_hitman_head08-strong.mdl"),
    GenericCharacter("Head 08", "Chubby",     "head08", "chubby",     [ "eyelashes_head08", "teeth_woman_01_d01", "eyeball_l", "eyeball_r" ], HMF_FACE_DATA, "face_d", HMF_EYE_DATA, [ "eyeball_l", "eyeball_r" ], "models/lordaardvark/sfm/dazgeneric/female_hitman_head08-chubby.mdl")
]

HMF_OUTFIT_DATA = [
    # Casual outfit
    Outfit("Casual", "models/LordAardvark/SFM/DazGeneric/female_hitman/outfit_casual-average.mdl", [
        # Shirts
        BodygroupData("Shirt", "shirt", "Sport Tanktop", "average_casual_shirt_sports_full", [
            MaterialOverrideSet("Shirt", "kok_shirt_d", "models/LordAardvark/DazGeneric/FemaleHitman/Outfits/Casual", [
                MaterialOverride("Plain",                       [ ParameterPair("$basetexture", "*/kok_shirt_plain") ], HMF_TINT_MUTED),
                MaterialOverride("White Stripes",               [ ParameterPair("$basetexture", "*/kok_shirt_stripes-white") ], HMF_TINT_MUTED),
                MaterialOverride("Gray Stripes Two-Tone",       [ ParameterPair("$basetexture", "*/kok_shirt_stripes-gray-bicolor") ], HMF_TINT_MUTED),
                MaterialOverride("Gray Stripes Three-Tone",     [ ParameterPair("$basetexture", "*/kok_shirt_stripes-gray-tricolor") ], HMF_TINT_MUTED),
                MaterialOverride("Black Stripes Two-Tone",      [ ParameterPair("$basetexture", "*/kok_shirt_stripes-black-bicolor") ], HMF_TINT_MUTED),
                MaterialOverride("Black Stripes Three-Tone",    [ ParameterPair("$basetexture", "*/kok_shirt_stripes-black-tricolor") ], HMF_TINT_MUTED),
                MaterialOverride("Dazzle Camo",                 [ ParameterPair("$basetexture", "*/kok_shirt_dazzle") ], HMF_TINT_MUTED),
                MaterialOverride("Cowprint",                    [ ParameterPair("$basetexture", "*/kok_shirt_cowprint") ], HMF_TINT_MUTED),
                MaterialOverride("Beads",                       [ ParameterPair("$basetexture", "*/kok_shirt_beads") ], HMF_TINT_MUTED),
                MaterialOverride("Light Bubble-Blob",           [ ParameterPair("$basetexture", "*/kok_shirt_bubble-blob_light") ], HMF_TINT_MUTED),
                MaterialOverride("Dark Bubble-Blob",            [ ParameterPair("$basetexture", "*/kok_shirt_bubble-blob_dark") ], HMF_TINT_MUTED)
            ])
        ]),
        # Shorts
        BodygroupData("Pants", "pants", "Sport Shorts",  "average_casual_pants_sports_shorts_full", [
            MaterialOverrideSet("Shorts", "kok_pant_d", "models/LordAardvark/DazGeneric/FemaleHitman/Outfits/Casual", [
                MaterialOverride("Plain", [
                    ParameterPair("$basetexture", "*/kok_pant_plain"),
                    ParameterPair("$blendtintbybasealpha", 0)
                ], HMF_TINT_MUTED),
                MaterialOverride("Racing Stripes",          [ ParameterPair("$basetexture", "*/kok_pant_stripes-racing") ], HMF_TINT_MUTED),
                MaterialOverride("Navy Camo",               [ ParameterPair("$basetexture", "*/kok_pant_camo-navy") ], HMF_TINT_NONE),
                MaterialOverride("Triangle Camo",           [ ParameterPair("$basetexture", "*/kok_pant_camo-triangle") ], HMF_TINT_NONE),
                MaterialOverride("Dazzle Camo",             [ ParameterPair("$basetexture", "*/kok_pant_camo-dazzle") ], HMF_TINT_MUTED),
                MaterialOverride("Fishscale", [
                    ParameterPair("$basetexture", "*/kok_pant_fishscale-d"),
                    ParameterPair("$bumpmap", "*/kok_pant_fishscale-n")
                ], HMF_TINT_MUTED),
                MaterialOverride("Dark Gradient",           [ ParameterPair("$basetexture", "*/kok_pant_gradient-dark") ], HMF_TINT_GAUDY),
                MaterialOverride("Light Gradient",          [ ParameterPair("$basetexture", "*/kok_pant_gradient-light") ], HMF_TINT_GAUDY),
                MaterialOverride("Leopard Print Gradient",  [ ParameterPair("$basetexture", "*/kok_pant_leopard-gradient") ], HMF_TINT_GAUDY),
                MaterialOverride("Leopard Print Solid", [
                    ParameterPair("$basetexture", "*/kok_pant_leopard-gradient"),
                    ParameterPair("$blendtintbybasealpha", 0)
                ], HMF_TINT_MUTED)
            ])
        ]),
        # Shoes
        BodygroupData("Shoes", "shoes", "Sport Shoes", "average_casual_shoes_sports", [
            MaterialOverrideSet("Base", "kok_pant_shoe-base", "models/LordAardvark/DazGeneric/FemaleHitman/Outfits/Casual", [
                MaterialOverride("Plain", [ ParameterPair("$basetexture", "*/kok_pant_plain") ], HMF_TINT_MUTED)
            ]),
            MaterialOverrideSet("Tongue", "kok_pant_shoe-interior", "models/LordAardvark/DazGeneric/FemaleHitman/Outfits/Casual", [
                MaterialOverride("Plain", [
                    ParameterPair("$basetexture", "*/kok_pant_plain"),
                    ParameterPair("$blendtintbybasealpha", 0)
                ], HMF_TINT_MUTED)
            ])
        ])
    ])
]

# Utilities
def SetCurAnimset(animset=None):
    global HMF_CurCharAnimset
    
    if animset is None:
        HMF_CurCharAnimset = sfm.GetCurrentAnimationSet()
        
    HMF_CurCharAnimset = animset
    
def GetCurAnimset():
    global HMF_CurCharAnimset
    
    if HMF_CurCharAnimset is None:
        HMF_CurCharAnimset = sfm.GetCurrentAnimationSet()
        
    return HMF_CurCharAnimset
    
def GetCurShot():
    return sfmClipEditor.GetSelectedShots()[0]
    
# This took way too fucking long to find some code that actually fucking worked.
# I don't use generators and barely understand how they work but fuck if I care right now.
# It fucking works.
def GetPathsToLeafNodesGenerator(rootNode, curPath=None):
    if curPath is None:
        curPath = tuple()
        
    if rootNode is None:
        return
        
    curPath += (rootNode,)
    
    if not hasattr(rootNode,"children"):
        yield curPath
        
    if hasattr(rootNode,"children"):
        if len(rootNode.children) < 1:
            yield curPath
            
        #yield from GetPathsToLeafNodes(rootNode, curPath)
        for childNode in rootNode.children:
            for path in GetPathsToLeafNodesGenerator(childNode, curPath):
                yield path
                
def GetPathsToLeafNodes(rootNode):
    pathList = []
    
    genList = list(GetPathsToLeafNodesGenerator(rootNode))
    
    for path in genList:
        tupleList = list(path)
        tupleList.pop(0)
        
        pathList.append(tupleList)
        
    return pathList
    
def SetStringAttribute(animset, attrName, attrValue):
    animset.AddAttributeAsString(attrName)
    animset.SetValue(attrName, attrValue)
    
def GetStringAttribute(animset, attrName):
    attrObj = getattr(animset, attrName, None)
    
    if attrObj is None:
        return None
        
    return str(attrObj.GetValue())
    
def AddElementAttribute(animset, elemName):
    elemAttr = animset.AddAttribute(elemName, vs.AT_ELEMENT)
    
    return elemAttr
    
def AddElementArray(animset, arrayName):
    elemArray = animset.AddAttribute(arrayName, vs.AT_ELEMENT_ARRAY)
    
    return elemArray
    
def AddElement(container, elementName):
    elem = vs.CreateElement("DmeElement", elementName, 0)
    
    container.AddToTail(elem)
    
    return elem
    
def GetElementByName(container, elementName):
    for i in range(container.Count()):
        elem = container[i]
        
        if elem.GetName() == elementName:
            return elem 
            
    return None
    
def FindOrCreateElement(container, elementName):
    elem = GetElementByName(container, elementName)
    
    if elem is None:
        elem = AddElement(container, elementName)
        
    return elem
    
def GetOutfitFromAnimset(outfitAnimset):
    if not hasattr(outfitAnimset, "gameModel"):
        return None
        
    if not hasattr(outfitAnimset, "outfitName"):
        return None
        
    outfitName = str(outfitAnimset.outfitName)
    
    if outfitName not in HMF_OUTFITS:
        return None
        
    outfitObject = HMF_OUTFITS[outfitName]
    
    return outfitObject
    
def GetGenericCharacterFromAnimset(animset):
    if not hasattr(animset, "gameModel"):
        return None
        
    mdlName = str(animset.gameModel.modelName)
    
    headNameInternal = None
    bodyTypeInternal = None
    
    for genCharKey in HMF_GENERIC_CHARACTERS:
        genChar = HMF_GENERIC_CHARACTERS[genCharKey]

        if genChar.bodyTypeInternal in mdlName:
            bodyTypeInternal = genChar.bodyTypeInternal
            
        if genChar.headNameInternal in mdlName:
            headNameInternal = genChar.headNameInternal
                
    genCharKey = GenericCharacter.GetKey(headNameInternal, bodyTypeInternal)
    
    if genCharKey in HMF_GENERIC_CHARACTERS:
        genChar = HMF_GENERIC_CHARACTERS[genCharKey]
        
        return genChar
        
    return None
    
def GetGenericCharacterFromNamePair(headNameInternal, bodyTypeInternal):
    genCharKey = GenericCharacter.GetKey(headNameInternal, bodyTypeInternal)
    
    if genCharKey in HMF_GENERIC_CHARACTERS:
        return HMF_GENERIC_CHARACTERS[genCharKey]
        
    return None
    
## Material related stuff
def GetOrCreateMaterial(animset, materialName):
    materialName = str(materialName)
    
    if hasattr(animset.gameModel,"materials"):
        for mat in animset.gameModel.materials:
            if mat is None:
                continue
                
            if str(mat.name) == materialName:
                return mat
                
    newMat = vs.CreateElement("DmeMaterial", materialName, 0)
    
    if not hasattr(animset.gameModel,"materials"):
        animset.gameModel.AddAttribute('materials', vs.AT_ELEMENT_ARRAY)
        
    animset.gameModel.materials.AddToTail(newMat)
    
    return newMat
    
def GetMaterialNames(animset):
    names = []
    
    shdr = animset.gameModel.GetStudioHdr()
    for i in range(shdr.numtextures):
        matname = shdr.pTexture(i).pszName()
        
        names.append(matname)
        
    return names
    
def AnimsetHasMaterialOverrides(animset):
    return (hasattr(animset,"gameModel") and hasattr(animset.gameModel,"materials"))
    
## Bodygroup-related stuff
def IsInt(v):
    try:
        _ = int(v)
    except Exception:
        return False
        
    return True
    
def ComputeBodygroupValue(animset, lstValues):
    mdl = animset.gameModel
    numBodygroups = mdl.GetBodyPartCount()
    
    bgBase = 1
    bgValue = 0
    
    for i in range(numBodygroups):
        if i > 0:
            numParts = mdl.GetBodyPart(i-1).nummodels
            
            bgBase = bgBase * numParts
            
        bgValue = bgValue + (bgBase * lstValues[i])
        
    return bgValue
      
def GetBodygroupName(bodygroup):
    bgName = str(bodygroup.pszName())
    
    if len(bgName) < 1:
        return "<none>"
        
    return bgName
    
def GetBodygroupIndices(animset, grpNameOrIdx, elemNameOrIdx):
    mdl = animset.gameModel
    numBodygroups = mdl.GetBodyPartCount()
    
    idx = [grpNameOrIdx,elemNameOrIdx]
    
    # Simplest case: Already given indices. Just return as is.
    if IsInt(grpNameOrIdx) and IsInt(elemNameOrIdx):
        return idx
        
    # Next simplest case: the group is an index, but the element is not.
    if IsInt(grpNameOrIdx):
        bgObj = mdl.GetBodyPart(idx[0])
        
        numSubs = bgObj.nummodels
        
        for sub in range(numSubs):
            subObj = bgObj.pModel(sub)
            
            subName = GetBodygroupName(subObj)
            
            if subName == elemNameOrIdx:
                idx[1] = sub
                
                return idx
                
        return [None, None] # If we get this far, it means we failed to find the sub-element by name. So we return Nones to represent that we failed.
        
    # Most annoying case: the group is not an index. In this case, we don't care what the element is, because we're going to actually recurse to save a bit of code and condition checking.
    for bg in range(numBodygroups):
        bgObj = mdl.GetBodyPart(bg)
        bgName = GetBodygroupName(bgObj)
        
        if bgName == grpNameOrIdx:
            return GetBodygroupIndices(animset, bg, elemNameOrIdx) # Once we've found the index of the group, we can recurse to hit the previous two cases to handle whether the element is an index or a name.
            
    return [None, None] # If we get this far, then it means we failed to find the group by name. So we return Nones to represent that we failed.
    
def GetBodygroupNames(animset, grpIdx, elemIdx):
    mdl = animset.gameModel
    numBodygroups = mdl.GetBodyPartCount()
    
    bgObj = mdl.GetBodyPart(grpIdx)
    subObj = bgObj.pModel(elemIdx)
    
    return [GetBodygroupName(bgObj), GetBodygroupName(subObj)]
    
def GetBodygroupValues(animset):
    mdl = animset.gameModel
    
    numBodygroups = mdl.GetBodyPartCount()
    
    lstValues = [0] * numBodygroups
    
    bgBase = 1.0
    bgValue = float(str(mdl.body))
    
    for i in range(numBodygroups):
        if i > 0:
            numParts = mdl.GetBodyPart(i-1).nummodels
            
            bgBase = bgBase * numParts

        lstValues[i] = int((bgValue / bgBase) % mdl.GetBodyPart(i).nummodels) # Just truncating these feels hella dirty. But it seems to be correct...
        
    return lstValues
    
def GetActiveBodygroupNames(animset):
    mdl = animset.gameModel
    numBodygroups = mdl.GetBodyPartCount()

    names = []
    
    bgValues = GetBodygroupValues(animset)
    
    for bg in range(numBodygroups):
        names.append(GetBodygroupNames(animset, bg, bgValues[bg]))
        
    return names
    
def SetActiveBodygroups(animset, lstNamePairs):
    mdl = animset.gameModel
    numBodygroups = mdl.GetBodyPartCount()
    
    bodygroupValues = [0] * numBodygroups
    
    for namePair in lstNamePairs:
        grpName, elemName = namePair
        
        grpIdx, elemIdx = GetBodygroupIndices(animset, grpName, elemName)
        
        bodygroupValues[grpIdx] = elemIdx
        
    mdl.body = ComputeBodygroupValue(animset, bodygroupValues)
    
def SetActiveBodygroup(animset, grpNameOrIdx, elemNameOrIdx):
    mdl = animset.gameModel
    numBodygroups = mdl.GetBodyPartCount()
    
    grpIdx, elemIdx = GetBodygroupIndices(animset, grpNameOrIdx, elemNameOrIdx)
    
    bodygroupValues = GetBodygroupValues(animset)
    
    bodygroupValues[grpIdx] = elemIdx
    
    mdl.body = ComputeBodygroupValue(animset, bodygroupValues)
    
## Animset related stuff
def GetRawFileName(fName):
    return os.path.splitext(os.path.basename(fName))[0]
    
# Given a DmeDag with children, return the first DmeDag with the provided name.
def GrabChild(parent,childName):
    return parent.GetChild(parent.FindChild(childName))
    
def AddOrCreateSceneGroup(grpName):
    shot = GetCurShot()
    
    for sceneElement in shot.scene.children:
        sceneElementName = str(sceneElement.name)
        
        if sceneElementName == grpName:
            return sceneElement
            
    grp = vs.CreateElement("DmeDag", grpName, shot.GetFileId())
    
    shot.scene.AddChild(grp)
    
    return grp
    
# https://stackoverflow.com/a/6330109
def SCast(val, to_type, default=None):
    try:
        return to_type(val)
    except (ValueError, TypeError):
        return default
        
def CountInstances(mdlName):
    shot = GetCurShot()
    cnt = 0
    
    animsetNameRoot = GetRawFileName(mdlName).lower()
    
    for child in shot.animationSets:
        if hasattr(child,"gameModel"):
            childName = str(child.name).lower()
            
            if animsetNameRoot in str(childName):
                strNumber = childName.replace(animsetNameRoot,"")
                castNumber = SCast(strNumber, int, -1)
                
                if castNumber > cnt:
                    cnt = castNumber
                
    return cnt
    
def GetAssociatedModelByFilePath(animset, arrayName, mdlName):
    if not hasattr(animset,arrayName):
        animset.AddAttribute(arrayName, vs.AT_ELEMENT_ARRAY)
        
    arrayAnimsets = getattr(animset,arrayName)
    genChar = GetGenericCharacterFromAnimset(animset)
    mdlName = genChar.ContextualizeString(mdlName)
    
    for arrayAnimset in arrayAnimsets:
        #mdlName = GetGenericCharacterFromAnimset(animset).ContextualizeString(str(arrayAnimset.gameModel.modelName))
        if str(arrayAnimset.gameModel.modelName) == mdlName:
            return arrayAnimset
            
    return None
    
def ZeroCtrl(tgtCtrl):
    logs = []
    channels = [tgtCtrl.positionChannel, tgtCtrl.orientationChannel]
    
    for channel in channels:
        layers = channel.log.layers[0]
        
        logs.append(layers.times)
        logs.append(layers.values)
        
    for log in logs:
        log.SetCount(0)
        log.SetCount(1)
      
def LockAndZero(tgtCtrl, parentCtrl):
    ctrlDag = tgtCtrl.GetDag()
    parentDag = parentCtrl.GetDag()
    
    ctrlDag.EnableOverrideParent(True)
    ctrlDag.SetOverrideParent(parentDag, True, True)
        
    ZeroCtrl(tgtCtrl)

# Core logic
# Taken from sfmUtils.py, adapted for general use
def CreateModelAnimationSet( baseName, modelPath ):
    ''' Create a model and animation set for that model and add the model to the scene'''
    
    print modelPath
    
    shot = sfm.GetCurrentShot()
    model = sfm.CreateModel( modelPath )
    animSet = None
    
    if ( model != None ):
        animSet = sfm.CreateAnimationSet( baseName, target=model )
        if ( animSet != None ):            
            dag = vs.CreateElement( "DmeDag", baseName, shot.GetFileId() )
            dag.AddChild( model )
            shot.scene.AddChild( dag )
    
    return animSet

def SpawnModel(mdlPath):
    global HMF_SpawnedAnimsets
    
    SPAWN_OFFSET_DISTANCE = 350
    SPAWN_OFFSET_CORRECTION = vs.Vector(0, 0, -28) # These two values are just vibed man. Don't question where they came from. I don't got an answer for you that will be satisfying to hear
    SPAWN_OFFSET_COUNT = -16
    
    # Get the camera
    shot = GetCurShot()
    camera = shot.camera
    
    # Compute the spawn location
    pos = camera.GetTransform().GetPosition()
    rot = camera.GetTransform().GetOrientation()
    rotForward = rot.AsMatrix().XAxis()
    rotLeft = rot.AsMatrix().YAxis()
    
    rotEuler = rot.AsRadianEuler()
    
    vScaledForward = vs.Vector( 0, 0, 0 )
    vs.mathlib.VectorScale( rotForward, SPAWN_OFFSET_DISTANCE, vScaledForward)
    
    vScaledRight = vs.Vector( 0, 0, 0 )
    vs.mathlib.VectorScale( rotLeft, SPAWN_OFFSET_COUNT * len(HMF_SpawnedAnimsets), vScaledRight )
    
    spawnOffset = vs.Vector( 0, 0, 0 )
    vs.mathlib.VectorAdd(pos, vScaledForward, spawnOffset)
    vs.mathlib.VectorAdd(spawnOffset, vScaledRight, spawnOffset)
    vs.mathlib.VectorAdd(spawnOffset, SPAWN_OFFSET_CORRECTION, spawnOffset)
 
    # Spawn the object
    mdlName = mdlPath.split("/")[-1]
    mdlName = mdlName.replace(".mdl","")
    
    animSetName = mdlName + str(CountInstances(mdlPath) + 1)
    
    animSet = CreateModelAnimationSet(str(animSetName), str(mdlPath))
    animSet.visible = True

    # Position the spawned object
    sfm.SetOperationMode( "Record" )
    
    sfm.PushSelection()
    sfm.ClearSelection()
    sfm.Select(animSetName + ":rootTransform")
    
    sfm.Move(spawnOffset[0], spawnOffset[1], spawnOffset[2])
    sfm.Rotate(0, 0, math.degrees(rotEuler.z) + 180)
    
    sfm.PopSelection()
    
    sfm.SetOperationMode( "Pass" )
    
    return animSet
   
def CreateAndAttachAnimset(targetAnimset, mdlName):
    targetAnimsetRoot = targetAnimset.FindControl( "rootTransform" )
    targetAnimsetName = str(targetAnimset.name)
    
    pos = targetAnimsetRoot.GetPosition()
    rot = targetAnimsetRoot.GetOrientation().AsRadianEuler() # Because fuck Quaternions
    
    # Go through all of the master's bones, and just store their names
    targetAnimsetBones = []
     
    for ctrl in targetAnimset.controls:
        if ctrl.GetTypeString() == "DmeTransformControl": # We only want bones
            targetAnimsetBones.append(str(ctrl.name))
            
    attachAnimsetName = GetRawFileName(mdlName) + str(CountInstances(mdlName) + 1)
    
    attachAnimset = sfmUtils.CreateModelAnimationSet(str(attachAnimsetName), str(mdlName))
    attachAnimset.visible = True
    
    # Position this attachmentst's rootTransform atop the master's
    sfm.SetOperationMode( "Record" ) # Change the operation mode so changes can be made

    sfm.PushSelection() # Push the current selection to preserve it

    sfm.ClearSelection()
    sfm.Select(attachAnimsetName + ":rootTransform")

    sfm.Move(pos[0], pos[1], pos[2])
    sfm.Rotate(math.degrees(rot.x), math.degrees(rot.y), math.degrees(rot.z))

    sfm.PopSelection() # Pop the current selection to restore the previous selection
    
    sfm.SetOperationMode( "Pass" ) # Restore the operation mode
    
    # Parent all of the bones that can be parented.
    for bone in targetAnimsetBones:
        attachBone = attachAnimset.FindControl(bone)
        
        if attachBone != None:
            parentName = targetAnimsetName + ":" + bone
            childName = attachAnimsetName + ":" + bone
            
            parentBone = targetAnimset.FindControl(bone)
            
            LockAndZero(attachBone, parentBone)
            
    # Store a reference on the attached object back to the object it is attached to
    attachAnimset.AddAttribute("parentAnimset", vs.AT_ELEMENT)
    attachAnimset.SetValue("parentAnimset", targetAnimset)
    
    return attachAnimset
    
def AddAnimsetToGroup(animset, grpName):
    shot = GetCurShot()
    
    grp = AddOrCreateSceneGroup(grpName)
    
    animsetDag = GrabChild(shot.scene, animset.GetName())
    
    shot.scene.RemoveChild(animsetDag)
    grp.AddChild(animsetDag)
        
def GetOrCreateMaterialOverrides(animset):
    matMap = {}
    
    matNames = GetMaterialNames(animset)
    
    if AnimsetHasMaterialOverrides(animset): # This animset already has material overrides. Just quickly populate the material map with the names of all the materials and their respective objects
        for matName in matNames:
            matMap[matName] =  GetOrCreateMaterial(animset,matName)
            
        return matMap
        
    # Otherwise, we need to build the material overrides
    shdr = animset.gameModel.GetStudioHdr()
    
    for matName in matNames:
        for j in range(shdr.numcdtextures):
            texpath = shdr.pCdtexture(j)
            fullname = texpath + matName
            
            absPath = LordAardvarkShared.GetAbsolutePath(HMF_GameModFolderNames, os.path.join("materials",fullname + ".vmt"))
            
            # Only apply this material if it exists. This is to deal with multiple $cdmaterial's on models.
            if not absPath:
                continue
                
            mat = GetOrCreateMaterial(animset,matName)
            mat.SetValue("mtlName",str(fullname))
            mat.SetValue("groupName","Material Overrides")      

            matMap[matName] = mat
            
    return matMap
    
def ApplyUniversalMaterialParameter(matMap, paramName, paramValue):
    paramPair = ParameterPair(paramName, paramValue)
    
    for matName in matMap:
        mat = matMap[matName]
        
        paramPair.Apply(mat)
    
def AssertMaterialOverrideDefaults(charAnimset):
    if not hasattr(charAnimset,"gameModel"):
        return
        
    firstFaceMap = HMF_FACE_DATA[0]
    firstEyeColor = HMF_EYE_DATA[0]
    firstSkinTone = HMF_SKINTONE_DATA[0]
    firstHairColor = HMF_HAIR_COLOR_DATA[0][0]
    
    if not hasattr(charAnimset.gameModel,"materials"): # Do not assert on models that already have material overrides
        GetOrCreateMaterialOverrides(charAnimset)

        ApplyFaceMap(charAnimset, firstFaceMap.displayName, firstFaceMap.displayName)
        ApplyEyeColor(charAnimset, firstEyeColor.displayName, firstEyeColor.displayName)
        ApplySkinTone(charAnimset, firstSkinTone.displayName)

    for hairAnimset in GetAllHairs(charAnimset):
        if ((hasattr(hairAnimset,"gameModel")) and (not hasattr(hairAnimset.gameModel,"materials"))):
            GetOrCreateMaterialOverrides(hairAnimset)
            
            ApplyHairColor(hairAnimset, firstHairColor)
        
    for outfitAnimset in GetAllOutfits(charAnimset):
        if ((hasattr(outfitAnimset,"gameModel")) and (not hasattr(outfitAnimset.gameModel,"materials"))):
            GetOrCreateMaterialOverrides(outfitAnimset)
            
            outfitObj = GetOutfitFromAnimset(outfitAnimset)
            
            if outfitObj is not None:
                outfitObj.ApplyDefault(outfitAnimset)
            
def AssertMaterialOverrideDefaultsUniversally():
    # This is required because Source Filmmaker has a material-override leak, where if you override the materials of one model, and other models have that same material and do NOT have material overrides
    # then the applied material override will "leak" to all other models. This is purely a memory leak, and restarting SFM or applying overrides to the affected models will revert these changes.
    # However, this "leak" is not desired behavior, as it is very disorienting for every model to be changing when you are in fact only customizing one character. And so, we need to explicitly apply
    # material overrides to every single viable model, with the default values, to ensure that this "leak" does not impact them and cause distracting changes on models that are not currently being affected
    #
    # volvo plz fix  ༼ つ ◕_◕ ༽つ
    
    validAnimsets = GetCustomizableAnimsets()
    
    for animset in validAnimsets:
        AssertMaterialOverrideDefaults(animset)    
    
def CreateHairModel(charAnimset, hairStyleObject):
    mdlName = hairStyleObject.hairModel
    
    grpName = charAnimset.GetName() + " :: hair"
    
    hairAnimset = CreateAndAttachAnimset(charAnimset, mdlName)
    AddAnimsetToGroup(hairAnimset, grpName)
    
    if not hasattr(charAnimset,"hairModels"):
        charAnimset.AddAttribute("hairModels", vs.AT_ELEMENT_ARRAY)
        
    charAnimset.hairModels.AddToTail(hairAnimset)
    
    return hairAnimset
    
def ApplyHairColor(hairAnimset, hairColorNameOrObject):
    if type(hairColorNameOrObject) == str:
        hairColorName = hairColorNameOrObject
        
        if hairColorName not in HMF_HAIR_COLORS:
            return False
            
        hairColorNameOrObject = HMF_HAIR_COLORS[hairColorName]
        
    hairColorObject = hairColorNameOrObject
    
    SetStringAttribute(hairAnimset, "hairColor", hairColorName)
    
    matMap = GetOrCreateMaterialOverrides(hairAnimset)
    
    for matName in matMap:
        mat = matMap[matName]
        
        mat.AddAttributeAsVector3("$color2")
        mat.SetValue("$color2", QColorToVec3(hairColorObject))
        
def GetAllHairs(charAnimset):
    hairAnimsets = []
    
    if not hasattr(charAnimset,"hairModels"):
        charAnimset.AddAttribute("hairModels", vs.AT_ELEMENT_ARRAY)
        
    hairModels = charAnimset.hairModels
    
    for animset in hairModels:
        hairAnimsets.append(animset)

    return hairAnimsets
    
def SetActiveHair(charAnimset, hairAnimset):
    if not hasattr(charAnimset,"hairModels"):
        charAnimset.AddAttribute("hairModels", vs.AT_ELEMENT_ARRAY)
        
    hairModels = charAnimset.hairModels
    
    for animset in hairModels:
        animset.visible = (str(animset) == str(hairAnimset))
        
    if not hasattr(charAnimset,"activeHair"):
        charAnimset.AddAttribute("activeHair", vs.AT_ELEMENT)
        
    charAnimset.SetValue("activeHair", hairAnimset)
        
def GetActiveHair(charAnimset):
    if not hasattr(charAnimset, "activeHair"):
        return None
        
    return charAnimset.activeHair
    
def SetHairStyle(charAnimset, hairStyleNameOrObject):
    hairStyleName = hairStyleNameOrObject
    hairStyleObject = hairStyleNameOrObject
    
    if type(hairStyleObject) == str:
        if hairStyleName not in HMF_HAIR_STYLES:
            return False
            
        hairStyleObject = HMF_HAIR_STYLES[hairStyleName]

    hairAnimset = GetAssociatedModelByFilePath(charAnimset, "hairModels", hairStyleObject.hairModel)

    if hairAnimset is None: # We need to create the hair animset, attach it, and record it
        hairAnimset = CreateHairModel(charAnimset, hairStyleObject)
        
    SetStringAttribute(hairAnimset, "hairStyle", hairStyleName)
    
    SetActiveHair(charAnimset, hairAnimset)
    SetActiveBodygroup(hairAnimset, hairStyleObject.bodygroupSet, hairStyleObject.bodygroupElement)
    
    return hairAnimset
    
def SetHairColor(charAnimset, hairColorName):
    activeHair = GetActiveHair(charAnimset)
    
    if activeHair is None:
        return False
        
    ApplyHairColor(activeHair, hairColorName)
    
def ApplySkinTone(charAnimset, skinToneDisplayName):
    skinToneTint = None
    
    for tint in HMF_SKINTONE_DATA:
        if tint.displayName == skinToneDisplayName:
            skinToneTint = tint
            break
            
    if skinToneTint is None:
        return False
        
    SetStringAttribute(charAnimset, "skinTone", str(skinToneDisplayName))
    
    SetSkinTone(charAnimset, skinToneTint)
    
def SetSkinTone(charAnimset, skinToneTint):
    genchar = GetGenericCharacterFromAnimset(charAnimset)
    matMap = GetOrCreateMaterialOverrides(charAnimset)
    
    for matName in matMap:
        if matName in genchar.skinToneIgnoreFilters:
            continue
        
        mat = matMap[matName]
        
        mat.AddAttributeAsVector3("$color2")
        mat.SetValue("$color2", skinToneTint.colorVector)
        
def ApplyFaceMap(charAnimset, diffuseDisplayName, bumpDisplayName):
    genchar = GetGenericCharacterFromAnimset(charAnimset)
    
    SetStringAttribute(charAnimset, "facemapDiffuse", diffuseDisplayName)
    SetStringAttribute(charAnimset, "facemapBump", bumpDisplayName)
    
    if len(diffuseDisplayName) < 1:
        return
        
    if len(bumpDisplayName) < 1:
        return
        
    baseTexturePath = genchar.faceMaps[diffuseDisplayName].baseTexturePath
    bumpmapTexturePath = genchar.faceMaps[bumpDisplayName].bumpmapTexturePath
    
    SetFaceMap(charAnimset, baseTexturePath, bumpmapTexturePath)
    
def SetFaceMap(charAnimset, baseTexturePath, bumpmapTexturePath):
    genchar = GetGenericCharacterFromAnimset(charAnimset)
    
    matMap = GetOrCreateMaterialOverrides(charAnimset)
    
    faceMat = matMap[genchar.faceMatName]

    faceMat.AddAttributeAsString("$basetexture")
    faceMat.AddAttributeAsString("$bumpmap")
    faceMat.AddAttributeAsString("$stretch")
    faceMat.AddAttributeAsString("$compress")
    
    blushTexPath = baseTexturePath.replace("_d","_blush") # Yay assumptions
    
    faceMat.SetValue("$basetexture",    baseTexturePath)    
    faceMat.SetValue("$bumpmap",        bumpmapTexturePath)    
    faceMat.SetValue("$stretch",        blushTexPath)    
    faceMat.SetValue("$compress",       blushTexPath) 
    
def ApplyEyeColor(charAnimset, leftEyeDisplayName, rightEyeDisplayName):
    genchar = GetGenericCharacterFromAnimset(charAnimset)
    
    SetStringAttribute(charAnimset, "eyeLeft",  leftEyeDisplayName)
    SetStringAttribute(charAnimset, "eyeRight", rightEyeDisplayName)
    
    if len(leftEyeDisplayName) < 1:
        return
        
    if len(rightEyeDisplayName) < 1:
        return
        
    leftEyePath = genchar.eyeTextures[leftEyeDisplayName].irisPath
    rightEyePath = genchar.eyeTextures[rightEyeDisplayName].irisPath
    
    SetEyeColor(charAnimset, leftEyePath, rightEyePath)
    
def SetEyeColor(charAnimset, leftEyePath, rightEyePath):
    genchar = GetGenericCharacterFromAnimset(charAnimset)
    
    matMap = GetOrCreateMaterialOverrides(charAnimset)

    leftEyeMat = matMap[genchar.eyeMatNames[0]] # We assume the eyes are defined as [left, right]. If Valve can assume it, so can I. Fuck you
    rightEyeMat = matMap[genchar.eyeMatNames[1]]
    
    leftEyeMat.AddAttributeAsString("$iris")
    rightEyeMat.AddAttributeAsString("$iris")
    
    leftEyeMat.SetValue("$iris", leftEyePath)
    rightEyeMat.SetValue("$iris", rightEyePath)
    
def CreateOutfitModel(charAnimset, outfitObject):
    #mdlName = outfitObject.mdlName
    mdlName = GetGenericCharacterFromAnimset(charAnimset).ContextualizeString(outfitObject.mdlName)
    
    grpName = charAnimset.GetName() + " :: outfit"
    
    charAnimset.gameModel.evaluateProceduralBones = False
    
    outfitAnimset = CreateAndAttachAnimset(charAnimset, mdlName)
    AddAnimsetToGroup(outfitAnimset, grpName)
    
    if not hasattr(charAnimset,"outfitModels"):
        charAnimset.AddAttribute("outfitModels", vs.AT_ELEMENT_ARRAY)
        
    charAnimset.outfitModels.AddToTail(outfitAnimset)
    
    outfitAnimset.AddAttributeAsString("outfitName")
    outfitAnimset.SetValue("outfitName", outfitObject.displayName)
    
    return outfitAnimset
    
def GetAllOutfits(charAnimset):
    outfitAnimsets = []
    
    if not hasattr(charAnimset,"outfitModels"):
        charAnimset.AddAttribute("outfitModels", vs.AT_ELEMENT_ARRAY)
        
    outfitModels = charAnimset.outfitModels
    
    for animset in outfitModels:
        outfitAnimsets.append(animset)
        
    return outfitAnimsets
    
def SetActiveOutfit(charAnimset, outfitAnimset):
    if not hasattr(charAnimset,"outfitModels"):
        charAnimset.AddAttribute("outfitModels", vs.AT_ELEMENT_ARRAY)
        
    outfitModels = charAnimset.outfitModels
    
    for animset in outfitModels:
        animset.visible = (str(animset) == str(outfitAnimset))
        
    if not hasattr(charAnimset,"activeOutfit"):
        charAnimset.AddAttribute("activeOutfit", vs.AT_ELEMENT)
        
    charAnimset.SetValue("activeOutfit", outfitAnimset)
        
def GetActiveOutfit(charAnimset):
    if not hasattr(charAnimset, "activeOutfit"):
        return None
        
    return charAnimset.activeOutfit
    
def EquipOutfit(charAnimset, outfitNameOrObject):
    outfitName = outfitNameOrObject
    outfitObject = outfitNameOrObject
    
    if type(outfitObject) == str:
        if outfitName not in HMF_OUTFITS:
            return False
            
        outfitObject = HMF_OUTFITS[outfitName]
        
    outfitAnimset = GetAssociatedModelByFilePath(charAnimset, "outfitModels", outfitObject.mdlName)
            
    if outfitAnimset is None: # We need to create the hair animset, attach it, and record it
        outfitAnimset = CreateOutfitModel(charAnimset, outfitObject)
        
    SetActiveOutfit(charAnimset, outfitAnimset)
    
    #SetActiveBodygroup(hairAnimset, hairStyleObject.bodygroupSet, hairStyleObject.bodygroupElement)
    
    return outfitAnimset
    
def StoreOutfitBodygroupData(outfitAnimset, outfitBodygroupData):
    bodygroupSetDisplay = outfitBodygroupData["bodygroupSet"]
    bodygroupElementDisplay = outfitBodygroupData["bodygroupElement"]
    matOverrideSetName = outfitBodygroupData["matOverrideSet"]
    matOverrideName = outfitBodygroupData["matOverride"]
    tintName = outfitBodygroupData["tint"]

    # Haha oh boy
    
    outfitDataAttribute = AddElementArray(outfitAnimset, "outfitData")
    outfitDataElement = FindOrCreateElement(outfitDataAttribute, "data")
    SetStringAttribute(outfitDataElement, "dataType","data")
    
    bodygroupSetsArray = AddElementArray(outfitDataElement, "children")
    
    bodygroupSetElement = FindOrCreateElement(bodygroupSetsArray, bodygroupSetDisplay)
    SetStringAttribute(bodygroupSetElement, "dataType", "bodygroupSet")
    bodygroupElementsArray = AddElementArray(bodygroupSetElement, "children")
    
    bodygroupElementElement = FindOrCreateElement(bodygroupElementsArray, bodygroupElementDisplay) # Don't like that variable name one bit
    SetStringAttribute(bodygroupElementElement, "dataType", "bodygroupElement")
    matOverrideSetsArray = AddElementArray(bodygroupElementElement, "children")
    
    matOverrideSetElement = FindOrCreateElement(matOverrideSetsArray, matOverrideSetName)
    SetStringAttribute(matOverrideSetElement, "dataType", "matOverrideSet")
    matOverridesArray = AddElementArray(matOverrideSetElement, "children")
    matOverridesArray.SetCount(0) # Ensure that only ever one material override is stored at a time. This effectively clears the existing array
    
    matOverrideElement = FindOrCreateElement(matOverridesArray, matOverrideName)
    SetStringAttribute(matOverrideElement, "dataType", "matOverride")
    tintsArray = AddElementArray(matOverrideElement, "children")
    
    tintElement = FindOrCreateElement(tintsArray, tintName)
    SetStringAttribute(tintElement, "dataType", "tint")
    
def LoadOutfitBodygroupData(outfitAnimset):
    outfitBodygroupData = []
    
    if not hasattr(outfitAnimset, "outfitData"):
        return None
        
    outfitDataAttribute = getattr(outfitAnimset,"outfitData")
    outfitDataElement = GetElementByName(outfitDataAttribute,"data")

    rawPaths = GetPathsToLeafNodes(outfitDataElement)
    finalPaths = []
    
    for path in rawPaths:
        newPath = []
        
        for node in path:
            newPath.append(node.GetName())
            
        finalPaths.append(newPath)
        
    return finalPaths
    
def ApplyOutfitBodygroup(charAnimset, outfitBodygroupData):
    bodygroupSetDisplay = outfitBodygroupData["bodygroupSet"]
    bodygroupElementDisplay = outfitBodygroupData["bodygroupElement"]
    matOverrideSetName = outfitBodygroupData["matOverrideSet"]
    matOverrideName = outfitBodygroupData["matOverride"]
    tintName = outfitBodygroupData["tint"]
    
    outfitAnimset = GetActiveOutfit(charAnimset)
    outfitName = str(outfitAnimset.outfitName)
    
    if outfitName not in HMF_OUTFITS:
        return False
        
    outfitObject = HMF_OUTFITS[outfitName]
    
    StoreOutfitBodygroupData(outfitAnimset, outfitBodygroupData)
        
    outfitObject.ApplyBodygroup(outfitAnimset, bodygroupSetDisplay, bodygroupElementDisplay, matOverrideSetName, matOverrideName, tintName)
    
# File management
def LoadHairColorData(fName):
    global HMF_HAIR_COLORS
    
    # Testing with hard-coded values
    
    for colorData in HMF_HAIR_COLOR_DATA:
        hairName, hairHex = colorData
        hairColor = QtGui.QColor("#" + hairHex)
        
        HMF_HAIR_COLORS[hairName] = hairColor
        
def LoadHairStyleData(fName):
    pass
    
# UI
## Globals
HMF_UiElements = {}
HMF_AnimsetQueue = []
HMF_SpawnedAnimsets = []
HMF_SelectedAnimsets = []

## Stupid fucking having to make a near-empty child of QDialog just so it can emit signals. Why the fuck do windows not emit a fucking closed signal PySide what the fuck are you doing?
class QDialogExtended(QtGui.QDialog):
    closed = QtCore.Signal()
    
    _data = {}
    
    def __init__(self, bShowMinMax=False):
        super(QDialogExtended, self).__init__()  

    def closeEvent(self, event):
        super(QDialogExtended, self).closeEvent(event)
        
        self.closed.emit()
        
    def setData(self, dataKey, dataValue):
        self._data[dataKey] = dataValue
        
    def data(self, dataKey):
        return self._data[dataKey]
        
## Utilities
def RegisteredElement(elementClass, elementName):
    elementObj = elementClass()
    
    HMF_UiElements[elementName] = elementObj
    
    return elementObj
    
def GetUiElement(elementName):
    return HMF_UiElements[elementName]
    
def GetCheckedListItems(lstWidget, bReturnDataObjects=True):
    checkedListItems = []
    
    for i in range(lstWidget.count()):
        lstItem = lstWidget.item(i)
        
        if lstItem.checkState() is QtCore.Qt.CheckState.Checked:
            returnItem = lstItem
            
            if bReturnDataObjects:
                returnItem = lstItem.data(QtCore.Qt.UserRole)
                
            checkedListItems.append(returnItem)
            
    return checkedListItems
    
def GetSelectedListItems(lstWidget, bReturnDataObjects=True):
    selectedListItems = []
    
    for i in range(lstWidget.count()):
        lstItem = lstWidget.item(i)
        
        if lstItem.isSelected():
            returnItem = lstItem
            
            if bReturnDataObjects:
                returnItem = lstItem.data(QtCore.Qt.UserRole)
                
            selectedListItems.append(returnItem)
            
    return selectedListItems
    
def RandomizeComboBox(drpBox):
    curIdx = drpBox.currentIndex()
    randomIdx = random.randrange(drpBox.count())
    
    if curIdx == randomIdx: # Bump the index up if the random ends up picking the current value
        randomIdx = randomIdx + 1
        
    if randomIdx >= drpBox.count(): # Wrap around to the beginning of the list if we bump off the end
        randomIdx = 0
    
    drpBox.setCurrentIndex(randomIdx)
    
def CreateButtonRows(grid, btnRows, operatingAnimset):
    global HMF_AnimsetQueue
    global HMF_bReportQueue
    
    curY = 0
    curX = 0
    
    #if len(HMF_AnimsetQueue) > 0 and bReportQueue:
    if HMF_bReportQueue:
        animset = operatingAnimset
        
        lblAnimset = QtGui.QLabel()
        lblAnimset.setText("Modifying: " + animset.GetName())
        grid.addWidget(lblAnimset, 0, 0, 1, 2)
        
        curY = curY + 1
        
    # Buttons, autogenerated from a list
    for btnRow in btnRows:
        numCols = 2
        if len(btnRow) > 1:
            numCols = 1
            
        curX = 0
        
        for btnData in btnRow:
            btn = RegisteredElement(QtGui.QPushButton,btnData[0] + str(operatingAnimset))
            btn.setText(btnData[0])
            btn.clicked.connect(btnData[1])
            grid.addWidget(btn, curY, curX, 1, numCols)
            
            curX = curX + 1
            
        curY = curY + 1
            
def CreateCheckableListItem(lstWidget, displayText, dataObj, checkState=QtCore.Qt.Unchecked):
    # Size hint
    hint = QtCore.QSize()
    hint.setHeight(24)
    
    # Create the new Outfit's item
    item = QtGui.QListWidgetItem()
    item.setData(QtCore.Qt.UserRole,dataObj)
    item.setText(displayText)
    
    if checkState is not None:
        item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
        item.setCheckState(checkState)
        
    item.setSizeHint(hint)
    
    lstWidget.addItem(item)
    
    return item
            
def CreateCheckableTreeNode(parentNode, nodeDisplay, nodeData, checkState=QtCore.Qt.Unchecked):
    hint = QtCore.QSize()
    hint.setHeight(24)
        
    newNode = QtGui.QTreeWidgetItem(parentNode)
    newNode.setText(0, nodeDisplay)
    newNode.setData(0, QtCore.Qt.UserRole, nodeData)
    newNode.setSizeHint(0, hint)
    newNode.setCheckState(0, checkState)
    #newNode.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.ChildIndicatorPolicy.DontShowIndicator)
    
    return newNode
    
def GetTreeWidgetSiblingNodes(node):
    parentNode = node.parent()
    
    siblingNodes = []
    
    if parentNode is None:
        treeWidget = node.treeWidget()
        
        for idx in range(treeWidget.topLevelItemCount()):
            childNode = treeWidget.topLevelItem(idx)
            
            siblingNodes.append(childNode)
            
        return siblingNodes
        
    for idx in range(parentNode.childCount()):
        childNode = parentNode.child(idx)
        
        siblingNodes.append(childNode)
        
    return siblingNodes
    
def GetCheckedTreeNodePath(treeWidget):
    checkedNodePath = []
    
    curNode = treeWidget
    
    for idx in range(treeWidget.topLevelItemCount()):
        topLevelNode = treeWidget.topLevelItem(idx)
        
        if topLevelNode.checkState(0) == QtCore.Qt.Checked:
            curNode = topLevelNode
            checkedNodePath.append(curNode)
            
    while (curNode.childCount() > 0):
        selectedNode = None
        
        for idx in range(curNode.childCount()):
            childNode = curNode.child(idx)
            
            if childNode.checkState(0) == QtCore.Qt.Checked:
                selectedNode = childNode
                break
                
        checkedNodePath.append(selectedNode)
        curNode = selectedNode
        
    return checkedNodePath
    
def GetTreeChildByName(widgetItem, childName):
    childNodes = None
    
    if type(widgetItem) == QtGui.QTreeWidget:
        childNodes = [widgetItem.topLevelItem(idx) for idx in range(widgetItem.topLevelItemCount())]
    else:
        childNodes = [widgetItem.child(idx) for idx in range(widgetItem.childCount())]
        
    for childNode in childNodes:
        if childNode.text(0) == childName:
            return childNode
            
    return None
    
def CheckNodesFromBodygroupData(treeWidget, outfitAnimset):
    allPaths = LoadOutfitBodygroupData(outfitAnimset)

    treeWidget.blockSignals(True)
    
    for path in allPaths:
        treeNode = treeWidget
        
        while (treeNode is not None) and (len(path) > 0):
            nodeName = path.pop(0)
            treeNode = GetTreeChildByName(treeNode, nodeName)
            
            if treeNode is not None:
                treeNode.setCheckState(0, QtCore.Qt.Checked)
                
    treeWidget.blockSignals(False)
            
def CheckAllFirstNodes(treeWidget):
    curNode = treeWidget.topLevelItem(0)
    
    treeWidget.blockSignals(True)
    
    curNode.setCheckState(0,QtCore.Qt.Checked) # Check the first top-level item
    
    nodesToVisit = []
    for idx in range(treeWidget.topLevelItemCount()): # And begin by visiting all of the top-level items
        nodesToVisit.append(treeWidget.topLevelItem(idx)) 
        
    while len(nodesToVisit) > 0:
        curNode = nodesToVisit.pop()
        childNodeCount = curNode.childCount()
        
        if childNodeCount > 0: # Only check the first child of every node
            firstNode = curNode.child(0)
            firstNode.setCheckState(0, QtCore.Qt.Checked)
            
        for idx in range(childNodeCount): # But visit every child of a given node
            nodesToVisit.append(curNode.child(idx))
            
    treeWidget.blockSignals(False)
    
def CheckRandomChildNode(treeWidgetItem, bRecurse=True, bCheckNode=True):
    if bCheckNode:
        treeWidgetItem.setCheckState(0, QtCore.Qt.Checked)
        
    if treeWidgetItem.childCount() < 1:
        return False
        
    childIdx = random.randrange(treeWidgetItem.childCount())
    childNode = treeWidgetItem.child(childIdx)
    
    if childNode.checkState(0) == QtCore.Qt.Checked: # If we randomly select a node that is already checked, then just bump down to the next node
        childIdx = childIdx + 1
        
        if childIdx >= treeWidgetItem.childCount(): # If we bump off the edge of the list, just wrap around to the beginning
            childIdx = 0
            
        childNode = treeWidgetItem.child(childIdx)
        
    childNode.setCheckState(0, QtCore.Qt.Checked)
    
    if bRecurse:
        CheckRandomChildNode(childNode, bRecurse=bRecurse, bCheckNode=False)
        
def ForceTreeExpansion(treeWidget):
    nodesToVisit = []
    
    for idx in range(treeWidget.topLevelItemCount()):
        nodesToVisit.append(treeWidget.topLevelItem(idx))
        
    treeWidget.blockSignals(True)
    
    while len(nodesToVisit) > 0:
        curNode = nodesToVisit.pop()
        
        curNode.setExpanded(curNode.checkState(0) == QtCore.Qt.Checked)
        
        for idx in range(curNode.childCount()):
            nodesToVisit.append(curNode.child(idx))
        
    treeWidget.blockSignals(False)

def InitializeBodygroupsTree(treeWidget, operatingAnimset):
    treeWidget.blockSignals(True)
    
    #curAnimset = GetCurAnimset()
    curAnimset = operatingAnimset
    outfitAnimset = GetActiveOutfit(curAnimset)
    
    outfitName = str(outfitAnimset.outfitName)
    
    outfitObject = HMF_OUTFITS[outfitName]
    
    outfitBodygroupSetMap = {}
    
    for bodygroupKey in outfitObject.bodygroupDatas:
        bodygroupData = outfitObject.bodygroupDatas[bodygroupKey]
        
        bodygroupSetNode = None
        bodygroupElementNode = None
        
        if bodygroupData.bodygroupSetDisplay not in outfitBodygroupSetMap:
            newNode = CreateCheckableTreeNode(treeWidget, bodygroupData.bodygroupSetDisplay, bodygroupData.bodygroupSetDisplay)
            newNode.setData(1, QtCore.Qt.UserRole, "bodygroupSet")
            
            outfitBodygroupSetMap[bodygroupData.bodygroupSetDisplay] = newNode
            
        bodygroupSetNode = outfitBodygroupSetMap[bodygroupData.bodygroupSetDisplay]
        bodygroupElementNode = CreateCheckableTreeNode(bodygroupSetNode, bodygroupData.bodygroupElementDisplay, bodygroupData)
        bodygroupElementNode.setData(1, QtCore.Qt.UserRole, "bodygroupElement")
        
        for matOverrideSetName in bodygroupData.materialOverrideSets:
            matOverrideSet = bodygroupData.materialOverrideSets[matOverrideSetName]
            
            matOverrideSetNode = CreateCheckableTreeNode(bodygroupElementNode, matOverrideSet.displayName, matOverrideSet)
            matOverrideSetNode.setData(1, QtCore.Qt.UserRole, "matOverrideSet")
            
            for matOverrideName in matOverrideSet.materialOverrides:
                matOverride = matOverrideSet.materialOverrides[matOverrideName]
                
                matOverrideNode = CreateCheckableTreeNode(matOverrideSetNode, matOverride.displayName, matOverride)
                matOverrideNode.setData(1, QtCore.Qt.UserRole, "matOverride")
                
                for tintName in matOverride.tints:
                    tint = matOverride.tints[tintName]
                    
                    tintNode = CreateCheckableTreeNode(matOverrideNode, tint.displayName, tint)
                    tintNode.setData(1, QtCore.Qt.UserRole, "tint")
                    
    treeWidget.blockSignals(False)
    
    CheckAllFirstNodes(treeWidget)
    #CheckNodesFromBodygroupData(treeWidget, outfitAnimset)
    ForceTreeExpansion(treeWidget)
    
def GetCustomizableAnimsets():
    shot = GetCurShot()
    
    validAnimsets = []
    
    for animset in shot.animationSets:
        if not hasattr(animset,"gameModel"):
            continue
            
        isValid = False
        mdlName = str(animset.gameModel.modelName)
        
        for key in HMF_GENERIC_CHARACTERS:
            genChar = HMF_GENERIC_CHARACTERS[key]
            
            if mdlName == genChar.modelName:
                validAnimsets.append(animset)
                isValid = True
                break
                
        if isValid:
            continue
            
    return validAnimsets
            
## Context menus
def CreateContext_BodygroupsTree(pos, operatingAnimset):
    treeBodygroups = GetUiElement("treeBodygroups" + str(operatingAnimset))
    itemWidget = treeBodygroups.itemAt(pos)
    
    if itemWidget is not None:
        menu = QtGui.QMenu()
        
        menu.addAction("Randomize", lambda itemWidget=itemWidget: CheckRandomChildNode(itemWidget))
        
        menu.exec_(treeBodygroups.viewport().mapToGlobal(pos))
        
def CreateContext_NewAnimsets(pos):
    lstNewAnimsets = GetUiElement("lstNewAnimsets")
    
    menu = QtGui.QMenu()
    menu.addAction("Spawn Random", lambda: onSpawnRandomCharacter())
    menu.exec_(lstNewAnimsets.viewport().mapToGlobal(pos))
    
## Hooks
def onApplySkinTone(operatingAnimset):
    drpSkinTone = GetUiElement("drpSkinTone" + str(operatingAnimset))
    
    ApplySkinTone(operatingAnimset, drpSkinTone.currentText())
    
    #skinToneTint = HMF_SKINTONE_DATA[drpSkinTone.currentIndex()]
    
    #SetSkinTone(GetCurAnimset(), skinToneTint)
    
def onRandomizeSkinTone(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpSkinTone" + str(operatingAnimset)))
    
def onDiffuseIndexChanged(operatingAnimset):
    drpDiffuseTexture = GetUiElement("drpDiffuseTexture" + str(operatingAnimset))
    drpBumpmapTexture = GetUiElement("drpBumpmapTexture" + str(operatingAnimset))
    
    curIdx = drpDiffuseTexture.currentIndex()
    
    drpBumpmapTexture.setCurrentIndex(curIdx)
    
    onApplyFaceMap(operatingAnimset)
    
def onRandomizeDiffuseTexture(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpDiffuseTexture" + str(operatingAnimset)))
    
def onRandomizeBumpmapTexture(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpBumpmapTexture" + str(operatingAnimset)))
    
def onApplyFaceMap(operatingAnimset):
    drpDiffuseTexture = GetUiElement("drpDiffuseTexture" + str(operatingAnimset))
    drpBumpmapTexture = GetUiElement("drpBumpmapTexture" + str(operatingAnimset))
    
    diffuseDisplayName = str(drpDiffuseTexture.currentText())
    bumpDisplayName = str(drpBumpmapTexture.currentText())
    
    ApplyFaceMap(operatingAnimset, diffuseDisplayName, bumpDisplayName)
    
def onEyeColorIndexChanged(operatingAnimset):
    drpLeftEye = GetUiElement("drpLeftEye" + str(operatingAnimset))
    drpRightEye = GetUiElement("drpRightEye" + str(operatingAnimset))
    
    curIdx = drpLeftEye.currentIndex()
    
    drpRightEye.setCurrentIndex(curIdx)
    
    onApplyEyeColor(operatingAnimset)
    
def onRandomizeLeftEye(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpLeftEye" + str(operatingAnimset)))
    
def onRandomizeRightEye(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpRightEye" + str(operatingAnimset)))
    
def onApplyEyeColor(operatingAnimset):
    genchar = GetGenericCharacterFromAnimset(operatingAnimset)
    
    drpLeftEye = GetUiElement("drpLeftEye" + str(operatingAnimset))
    drpRightEye = GetUiElement("drpRightEye" + str(operatingAnimset))
    
    leftEyeDisplayName = str(drpLeftEye.currentText())
    rightEyeDisplayName = str(drpRightEye.currentText())
    
    ApplyEyeColor(operatingAnimset, leftEyeDisplayName, rightEyeDisplayName)
    
def onRandomizeHairStyle(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpHairStyle" + str(operatingAnimset)))
    
def onRandomizeHairColor(operatingAnimset):
    RandomizeComboBox(GetUiElement("drpHairColor" + str(operatingAnimset)))
    
def onAttachHair(operatingAnimset):
    lstHairStyles = GetUiElement("lstHairStyles" + str(operatingAnimset))
    window = GetUiElement("windowHairStyleAttach" + str(operatingAnimset))
    
    selectedHairs = GetSelectedListItems(lstHairStyles)
    
    if selectedHairs < 1:
        return
        
    hairStyle = selectedHairs[0]
    animset = operatingAnimset
    
    hairAnimset = SetHairStyle(animset, hairStyle.displayName)
    AssertMaterialOverrideDefaults(animset)
    
    window.close()
    
def onAttachOutfit(operatingAnimset):
    lstOutfits = GetUiElement("lstOutfits" + str(operatingAnimset))
    window = GetUiElement("windowOutfitAttach" + str(operatingAnimset))
    
    selectedOutfits = GetSelectedListItems(lstOutfits)
    
    if selectedOutfits < 1:
        return
        
    outfit = selectedOutfits[0]
    animset = operatingAnimset
    
    outfitAnimset = EquipOutfit(animset, outfit.displayName)
    AssertMaterialOverrideDefaults(animset)
    
    window.close()   
    
#animset = sfm.GetCurrentAnimationSet()
#
#if GetActiveHair(animset) is None:
#    SetHairStyle(sfm.GetCurrentAnimationSet(), "Crazy Morning")
#    
#if GetActiveOutfit(animset) is None:
#    EquipOutfit(sfm.GetCurrentAnimationSet(), "Casual")

    
def onApplyHairStyle(operatingAnimset):
    drpHairStyle = GetUiElement("drpHairStyle" + str(operatingAnimset))
    drpHairColor = GetUiElement("drpHairColor" + str(operatingAnimset))
    
    hairStyleName = str(drpHairStyle.currentText())
    hairColorName = str(drpHairColor.currentText())
    
    SetHairStyle(operatingAnimset, hairStyleName)
    SetHairColor(operatingAnimset, hairColorName)
    
def onTreeBodygroupApply(operatingAnimset):
    curAnimset = operatingAnimset
    
    checkedPath = GetCheckedTreeNodePath(GetUiElement("treeBodygroups" + str(operatingAnimset)))
    
    bodygroupDataDict = {"bodygroupSet":None, "bodygroupElement":None, "matOverrideSet":None, "matOverride":None, "tint":None}
    for node in checkedPath:
        dataKey = node.data(1, QtCore.Qt.UserRole)
        dataValue = str(node.text(0))
        
        bodygroupDataDict[dataKey] = dataValue
        
    #ApplyOutfitBodygroup(curAnimset, bodygroupDataDict["bodygroupSet"], bodygroupDataDict["bodygroupElement"], bodygroupDataDict["matOverrideSet"], bodygroupDataDict["matOverride"], bodygroupDataDict["tint"])
    ApplyOutfitBodygroup(curAnimset, bodygroupDataDict)
    
def onTreeBodygroupItemChecked(item, operatingAnimset):
    treeWidget = item.treeWidget()
    
    treeWidget.blockSignals(True)
    
    item.setCheckState(0, QtCore.Qt.Checked) # Force a node to always be checked. Even if the user attempts to uncheck a node, force it checked anyways. Only the sibling-check function can uncheck nodes. The user cannot
    
    childNodes = GetTreeWidgetSiblingNodes(item)
    for childNode in childNodes:
        if childNode is not item:
            childNode.setCheckState(0, QtCore.Qt.Unchecked)
            
    treeWidget.blockSignals(False)
    
    ForceTreeExpansion(treeWidget)
    
    onTreeBodygroupApply(operatingAnimset)
    
def onIncrementAnimsetQueue():
    global HMF_AnimsetQueue
    global HMF_SelectedAnimsets
    
    if len(HMF_AnimsetQueue) > 0:
        nextAnimset = HMF_AnimsetQueue[0]
        HMF_AnimsetQueue.pop(0)
        
        SetCurAnimset(nextAnimset)
        #CreateUI_AttachElementsOrCustomizeCharacter()
        CreateUI_AttachElements(nextAnimset)
        
    else: # Once the queue is depleted, meaning that we were attaching outfits and we no longer have any to attach to, then we immediately open customize windows for all the models that were attached to.
        for animset in reversed(HMF_SelectedAnimsets): 
            CreateUI_CustomizeCharacter(animset)
            
        HMF_SelectedAnimsets = []
        
#def onModifySelected():
def onAttachToSelected():
    global HMF_AnimsetQueue
    global HMF_SelectedAnimsets
    
    lstExistingAnimsets = GetUiElement("lstExistingAnimsets")
    window = GetUiElement("windowSpawnOrModify")
    
    selectedAnimsets = GetCheckedListItems(lstExistingAnimsets)
    AssertMaterialOverrideDefaultsUniversally()
    
    validAnimsets = GetCustomizableAnimsets()
    
    HMF_SelectedAnimsets = selectedAnimsets
    
    for animset in validAnimsets:
        AssertMaterialOverrideDefaults(animset)
        
    for animset in selectedAnimsets:
        HMF_AnimsetQueue.append(animset)
        
    window.close()
    
    onIncrementAnimsetQueue()
    #QtCore.QTimer.singleShot(1000, lambda: onIncrementAnimsetQueue())

def onCustomizeSelected():
    global HMF_AnimsetQueue
    
    lstExistingAnimsets = GetUiElement("lstExistingAnimsets")
    window = GetUiElement("windowSpawnOrModify")
    
    selectedAnimsets = GetCheckedListItems(lstExistingAnimsets)
    AssertMaterialOverrideDefaultsUniversally()
    
    validAnimsets = GetCustomizableAnimsets()
    
    for animset in validAnimsets:
        AssertMaterialOverrideDefaults(animset)
        
    for animset in reversed(selectedAnimsets):
        CreateUI_CustomizeCharacter(animset)
        
    window.close()
        
def onCustomizeCharacter(operatingAnimset):
    window = GetUiElement("windowSpawnOrModify")
    
    window.close()
    
    CreateUI_CustomizeCharacter(operatingAnimset)
    
def onCharacterSpawned_UpdateCounter():
    global HMF_SpawnedAnimsets
    txtSpawnCounter = GetUiElement("txtSpawnCounter")
    txtSpawnCounter.setText(str(len(HMF_SpawnedAnimsets)) + " Models Spawned")
    
    InitializeCustomizableAnimsets() # Update the left-hand list to reflect the newly-spawned model
    
def onSpawnCharacter(item):
    global HMF_SpawnedAnimsets
    
    genChar = item.data(QtCore.Qt.UserRole)
    mdlName = genChar.modelName
    
    newAnimset = SpawnModel(mdlName)
    HMF_SpawnedAnimsets.append(newAnimset)
    
    onCharacterSpawned_UpdateCounter()
    
def onSpawnRandomCharacter():
    pass
    
def onMainWindowClosed():
    onIncrementAnimsetQueue()
    
## Windows
def InitializeSkinToneUI(operatingAnimset):
    animset = operatingAnimset
    
    drpSkinTone = GetUiElement("drpSkinTone" + str(operatingAnimset))
    
    skinToneDisplayName = GetStringAttribute(animset, "skinTone")
    
    drpSkinTone.setCurrentIndex(drpSkinTone.findText(skinToneDisplayName))
    
def InitializeFaceMapUI(operatingAnimset):
    animset = operatingAnimset
    
    drpDiffuseTexture = GetUiElement("drpDiffuseTexture" + str(operatingAnimset))
    drpBumpmapTexture = GetUiElement("drpBumpmapTexture" + str(operatingAnimset))
    
    diffuseTextureDisplayName = GetStringAttribute(animset, "facemapDiffuse")
    bumpmapTextureDisplayName = GetStringAttribute(animset, "facemapBump")
    
    drpDiffuseTexture.setCurrentIndex(drpDiffuseTexture.findText(diffuseTextureDisplayName))
    drpBumpmapTexture.setCurrentIndex(drpBumpmapTexture.findText(bumpmapTextureDisplayName))
    
def InitializeEyeColorUI(operatingAnimset):
    animset = operatingAnimset
    
    drpLeftEye = GetUiElement("drpLeftEye" + str(operatingAnimset))
    drpRightEye = GetUiElement("drpRightEye" + str(operatingAnimset))
    
    leftEyeDisplayName = GetStringAttribute(animset, "eyeLeft")
    rightEyeDisplayName = GetStringAttribute(animset, "eyeRight")
    
    drpLeftEye.setCurrentIndex( drpLeftEye.findText( leftEyeDisplayName ))
    drpRightEye.setCurrentIndex(drpRightEye.findText(rightEyeDisplayName))
    
def InitializeHairStyleAttachUI(operatingAnimset):
    lstHairStyles = GetUiElement("lstHairStyles" + str(operatingAnimset))
    
    for hairStyle in HMF_HAIR_STYLE_DATA:
        CreateCheckableListItem(lstHairStyles, hairStyle.displayName, hairStyle, checkState=None)
        
def InitializeOutfitBodygroupsAttachUI(operatingAnimset):
    lstOutfits = GetUiElement("lstOutfits" + str(operatingAnimset))
    
    for outfit in HMF_OUTFIT_DATA:
        CreateCheckableListItem(lstOutfits, outfit.displayName, outfit, checkState=None)

def InitializeHairStyleUI(operatingAnimset):
    hairAnimset = GetActiveHair(operatingAnimset)
    
    drpHairStyle = GetUiElement("drpHairStyle" + str(operatingAnimset))
    drpHairColor = GetUiElement("drpHairColor" + str(operatingAnimset))

    hairStyleDisplayName = GetStringAttribute(hairAnimset, "hairStyle")
    hairColorDisplayName = GetStringAttribute(hairAnimset, "hairColor")
    
    drpHairStyle.setCurrentIndex(drpHairStyle.findText(hairStyleDisplayName))
    drpHairColor.setCurrentIndex(drpHairColor.findText(hairColorDisplayName))
    
def InitializeCustomizableAnimsets():
    lstExistingAnimsets = GetUiElement("lstExistingAnimsets")
    lstExistingAnimsets.clear()
    
    validAnimsets = GetCustomizableAnimsets()
    
    for animset in validAnimsets:
        CreateCheckableListItem(lstExistingAnimsets, animset.GetName(), animset)
        
def InitializeNewAnimsets():
    lstNewAnimsets = GetUiElement("lstNewAnimsets")

    for key in HMF_GENERIC_CHARACTERS:
        genChar = HMF_GENERIC_CHARACTERS[key]
        
        displayName = genChar.headName + " " + genChar.bodyTypeName
        
        CreateCheckableListItem(lstNewAnimsets, displayName, genChar, checkState=None)
        
def InitializeCustomizeUI(operatingAnimset):
    #curAnimset = GetCurAnimset()
    curAnimset = operatingAnimset

    btnHairstyle = GetUiElement("Hair Style" + str(operatingAnimset))
    btnOutfit = GetUiElement("Outfit" + str(operatingAnimset))
    btnActiveHair = GetUiElement("Set Active Hair" + str(operatingAnimset))
    btnActiveOutfit = GetUiElement("Set Active Outfit" + str(operatingAnimset))
    
    if not hasattr(curAnimset, "gameModel"): # What are we doing here
        return None
        
    allHairs = GetAllHairs(curAnimset)
    allOutfits = GetAllOutfits(curAnimset)
    activeHair = GetActiveHair(curAnimset)
    activeOutfit = GetActiveOutfit(curAnimset)
    
    btnHairstyle.setEnabled(not (activeHair is None))
    btnOutfit.setEnabled(not (activeOutfit is None))
    btnActiveHair.setEnabled(len(allHairs) > 0)
    btnActiveOutfit.setEnabled(len(allOutfits) > 0)
        
def CreateUI_SkinTone(operatingAnimset):
    #animset = GetCurAnimset()
    animset = operatingAnimset
    
    if not hasattr(animset,"gameModel"):
        CreateAnimsetUI()
        return 0
        
    # Create the base UI window
    window = RegisteredElement(LordAardvarkShared.NonModalWindow,"window" + str(operatingAnimset))
    window.resize(360,80)
    window.setWindowTitle("HMF v1.0.0 - Skin Tone")
   
    applySkinTone = window.safeConnect(lambda operatingAnimset=operatingAnimset: onApplySkinTone(operatingAnimset))
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    grid.setColumnMinimumWidth(0, 72)
    
    # Labels along the top
    lblSkinTone = QtGui.QLabel()
    lblSkinTone.setText("Skin Tone:")
    grid.addWidget(lblSkinTone, 0, 0, 1, 1)

    # Dropdowns and apply button
    skintoneNames = [skintone.displayName for skintone in HMF_SKINTONE_DATA]
    
    drpSkinTone = RegisteredElement(QtGui.QComboBox,"drpSkinTone" + str(operatingAnimset))
    drpSkinTone.addItems(skintoneNames)
    drpSkinTone.currentIndexChanged.connect(lambda: window.safeCall(applySkinTone))
    grid.addWidget(drpSkinTone, 1, 0, 1, 1)

    btnApplySkinTone = RegisteredElement(QtGui.QPushButton,"btnApplySkinTone" + str(operatingAnimset))
    btnApplySkinTone.setText("Apply")
    btnApplySkinTone.clicked.connect(lambda: window.close())
    grid.addWidget(btnApplySkinTone, 1, 2, 1, 1)
    
    # Randomize buttons
    btnRandomizeSkinTone = RegisteredElement(QtGui.QPushButton, "btnRandomizeSkinTone" + str(operatingAnimset))
    btnRandomizeSkinTone.setText("Randomize")
    btnRandomizeSkinTone.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeSkinTone(operatingAnimset))
    grid.addWidget(btnRandomizeSkinTone, 2, 0, 1, 1)

    # Display the window
    InitializeSkinToneUI(operatingAnimset)
    
    window.setLayout(grid)
    window.show()    
    
def CreateUI_FaceMap(operatingAnimset):
    #animset = GetCurAnimset()
    animset = operatingAnimset
    
    if not hasattr(animset,"gameModel"):
        CreateAnimsetUI()
        return 0

    genchar = GetGenericCharacterFromAnimset(animset)
    
    # Create the base UI window
    window = RegisteredElement(LordAardvarkShared.NonModalWindow,"window" + str(operatingAnimset))
    window.resize(360,80)
    window.setWindowTitle("HMF v1.0.0 - Face Texture")
   
    applyFaceMap = window.safeConnect(lambda operatingAnimset=operatingAnimset: onApplyFaceMap(operatingAnimset))
    diffuseUpdate = window.safeConnect(lambda operatingAnimset=operatingAnimset: onDiffuseIndexChanged(operatingAnimset))
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    grid.setColumnMinimumWidth(0, 72)
    
    # Labels along the top
    lblDiffuseTexture = QtGui.QLabel()
    lblDiffuseTexture.setText("Diffuse:")
    grid.addWidget(lblDiffuseTexture, 0, 0, 1, 1)
    
    lblBumpmapTexture = QtGui.QLabel()
    lblBumpmapTexture.setText("Bumpmap:")
    grid.addWidget(lblBumpmapTexture, 0, 1, 1, 1)
    
    # Dropdowns and apply button
    facemapNames = [facemapDisplay for facemapDisplay in genchar.faceMaps]
    
    drpDiffuseTexture = RegisteredElement(QtGui.QComboBox,"drpDiffuseTexture" + str(operatingAnimset))
    drpDiffuseTexture.addItems(facemapNames)
    drpDiffuseTexture.currentIndexChanged.connect(lambda: window.safeCall(diffuseUpdate))
    grid.addWidget(drpDiffuseTexture, 1, 0, 1, 1)
    
    drpBumpmapTexture = RegisteredElement(QtGui.QComboBox,"drpBumpmapTexture" + str(operatingAnimset))
    drpBumpmapTexture.addItems(facemapNames)
    drpBumpmapTexture.currentIndexChanged.connect(lambda: window.safeCall(applyFaceMap))
    grid.addWidget(drpBumpmapTexture, 1, 1, 1, 1)
    
    btnApplyFacemap = RegisteredElement(QtGui.QPushButton,"btnApplyFacemap" + str(operatingAnimset))
    btnApplyFacemap.setText("Apply")
    btnApplyFacemap.clicked.connect(lambda: window.close())
    grid.addWidget(btnApplyFacemap, 1, 2, 1, 1)
    
    # Randomize buttons
    btnRandomizeDiffuseTexture = RegisteredElement(QtGui.QPushButton, "btnRandomizeDiffuseTexture" + str(operatingAnimset))
    btnRandomizeDiffuseTexture.setText("Randomize")
    btnRandomizeDiffuseTexture.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeDiffuseTexture(operatingAnimset))
    grid.addWidget(btnRandomizeDiffuseTexture, 2, 0, 1, 1)
    
    btnRandomizeBumpmapTexture = RegisteredElement(QtGui.QPushButton, "btnRandomizeBumpmapTexture" + str(operatingAnimset))
    btnRandomizeBumpmapTexture.setText("Randomize")
    btnRandomizeBumpmapTexture.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeBumpmapTexture(operatingAnimset))
    grid.addWidget(btnRandomizeBumpmapTexture, 2, 1, 1, 1)
    
    # Display the window
    InitializeFaceMapUI(operatingAnimset)
    
    window.setLayout(grid)
    window.show()    
    
def CreateUI_EyeColor(operatingAnimset):
    #animset = GetCurAnimset()
    animset = operatingAnimset
    
    if not hasattr(animset,"gameModel"):
        CreateAnimsetUI()
        return 0

    genchar = GetGenericCharacterFromAnimset(animset)
    
    # Create the base UI window
    window = RegisteredElement(LordAardvarkShared.NonModalWindow,"window" + str(operatingAnimset))
    window.resize(360,80)
    window.setWindowTitle("HMF v1.0.0 - Eye Color")
   
    applyEyeColor = window.safeConnect(lambda operatingAnimset=operatingAnimset: onApplyEyeColor(operatingAnimset))
    eyeTextureUpdate = window.safeConnect(lambda operatingAnimset=operatingAnimset: onEyeColorIndexChanged(operatingAnimset))
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    grid.setColumnMinimumWidth(0, 72)
    
    # Labels along the top
    lblLeftEye = QtGui.QLabel()
    lblLeftEye.setText("Left Eye:")
    grid.addWidget(lblLeftEye, 0, 0, 1, 1)
    
    lblRightEye = QtGui.QLabel()
    lblRightEye.setText("Right Eye:")
    grid.addWidget(lblRightEye, 0, 1, 1, 1)
    
    # Dropdowns and apply button
    eyeTexNames = [eyeTexDisplay for eyeTexDisplay in genchar.eyeTextures]
    
    drpLeftEye = RegisteredElement(QtGui.QComboBox,"drpLeftEye" + str(operatingAnimset))
    drpLeftEye.addItems(eyeTexNames)
    drpLeftEye.currentIndexChanged.connect(lambda: window.safeCall(eyeTextureUpdate))
    grid.addWidget(drpLeftEye, 1, 0, 1, 1)
    
    drpRightEye = RegisteredElement(QtGui.QComboBox,"drpRightEye" + str(operatingAnimset))
    drpRightEye.addItems(eyeTexNames)
    drpRightEye.currentIndexChanged.connect(lambda: window.safeCall(applyEyeColor))
    grid.addWidget(drpRightEye, 1, 1, 1, 1)
    
    btnApplyFacemap = RegisteredElement(QtGui.QPushButton,"btnApplyFacemap" + str(operatingAnimset))
    btnApplyFacemap.setText("Apply")
    btnApplyFacemap.clicked.connect(lambda: window.close())
    grid.addWidget(btnApplyFacemap, 1, 2, 1, 1)
    
    # Randomize buttons
    btnRandomizeLeftEye = RegisteredElement(QtGui.QPushButton, "btnRandomizeLeftEye" + str(operatingAnimset))
    btnRandomizeLeftEye.setText("Randomize")
    btnRandomizeLeftEye.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeLeftEye(operatingAnimset))
    grid.addWidget(btnRandomizeLeftEye, 2, 0, 1, 1)
    
    btnRandomizeRightEye = RegisteredElement(QtGui.QPushButton, "btnRandomizeRightEye" + str(operatingAnimset))
    btnRandomizeRightEye.setText("Randomize")
    btnRandomizeRightEye.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeRightEye(operatingAnimset))
    grid.addWidget(btnRandomizeRightEye, 2, 1, 1, 1)
    
    # Display the window
    InitializeEyeColorUI(operatingAnimset)
    
    window.setLayout(grid)
    window.show()    
    
def CreateUI_HairStyle_Attach(operatingAnimset):
    # Create the base UI window
    window = RegisteredElement(QtGui.QDialog, "windowHairStyleAttach" + str(operatingAnimset))
    window.resize(300,480)
    window.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint)
    window.setWindowTitle("Hitman Female Customizer - Attach Hair")
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    
    # UI elements
    lstHairStyles = RegisteredElement(QtGui.QListWidget,"lstHairStyles" + str(operatingAnimset))
    grid.addWidget(lstHairStyles,1, 0, 10, 1)
    
    btnAttachHair = RegisteredElement(QtGui.QPushButton, "btnAttachHair" + str(operatingAnimset))
    btnAttachHair.setText("Attach Hair")
    btnAttachHair.clicked.connect(lambda operatingAnimset=operatingAnimset: onAttachHair(operatingAnimset))
    grid.addWidget(btnAttachHair, 11, 0, 1, 1)

    InitializeHairStyleAttachUI(operatingAnimset)
    
    # Display the window
    window.setLayout(grid)
    window.setModal(True)
    window.exec_()
    
def CreateUI_HairStyle(operatingAnimset):
    #animset = GetCurAnimset()
    animset = operatingAnimset
    
    if not hasattr(animset,"gameModel"):
        CreateAnimsetUI()
        return 0
    
    # Create the base UI window
    window = RegisteredElement(LordAardvarkShared.NonModalWindow,"window" + str(operatingAnimset))
    window.resize(360,80)
    window.setWindowTitle("HMF v1.0.0 - HairStyle")
    
    applyHairStyle = window.safeConnect(lambda operatingAnimset=operatingAnimset: onApplyHairStyle(operatingAnimset))

    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    grid.setColumnMinimumWidth(0, 72)
    
    # Labels along the top
    lblHairStyle = QtGui.QLabel()
    lblHairStyle.setText("Hair Style:")
    grid.addWidget(lblHairStyle, 0, 0, 1, 1)
    
    lblHairColor = QtGui.QLabel()
    lblHairColor.setText("Hair Color:")
    grid.addWidget(lblHairColor, 0, 1, 1, 1)
    
    # Dropdowns and apply button
    hairStyleNames = [hairStyleName for hairStyleName in HMF_HAIR_STYLES]
    hairColorNames = [hairColorName for hairColorName in HMF_HAIR_COLORS]
    
    hairStyleNames.reverse()
    hairColorNames.reverse()
    
    drpHairStyle = RegisteredElement(QtGui.QComboBox,"drpHairStyle" + str(operatingAnimset))
    drpHairStyle.addItems(hairStyleNames)
    drpHairStyle.currentIndexChanged.connect(lambda: window.safeCall(applyHairStyle))
    grid.addWidget(drpHairStyle, 1, 0, 1, 1)
    
    drpHairColor = RegisteredElement(QtGui.QComboBox,"drpHairColor" + str(operatingAnimset))
    drpHairColor.addItems(hairColorNames)
    drpHairColor.currentIndexChanged.connect(lambda: window.safeCall(applyHairStyle))
    grid.addWidget(drpHairColor, 1, 1, 1, 1)
    
    btnApplyHair = RegisteredElement(QtGui.QPushButton,"btnApplyHair" + str(operatingAnimset))
    btnApplyHair.setText("Apply")
    btnApplyHair.clicked.connect(lambda: window.close())
    grid.addWidget(btnApplyHair, 1, 2, 1, 1)
    
    # Randomize buttons
    btnRandomizeHairStyle = RegisteredElement(QtGui.QPushButton, "btnRandomizeHairStyle" + str(operatingAnimset))
    btnRandomizeHairStyle.setText("Randomize")
    btnRandomizeHairStyle.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeHairStyle(operatingAnimset))
    grid.addWidget(btnRandomizeHairStyle, 2, 0, 1, 1)
    
    btnRandomizeHairColor = RegisteredElement(QtGui.QPushButton, "btnRandomizeHairColor" + str(operatingAnimset))
    btnRandomizeHairColor.setText("Randomize")
    btnRandomizeHairColor.clicked.connect(lambda operatingAnimset=operatingAnimset: onRandomizeHairColor(operatingAnimset))
    grid.addWidget(btnRandomizeHairColor, 2, 1, 1, 1)
    
    # Display the window
    InitializeHairStyleUI(operatingAnimset)
    
    window.setLayout(grid)
    window.show()    

def CreateUI_OutfitBodygroups_Attach(operatingAnimset):
    # Create the base UI window
    window = RegisteredElement(QtGui.QDialog, "windowOutfitAttach" + str(operatingAnimset))
    window.resize(300,480)
    window.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint)
    window.setWindowTitle("Hitman Female Customizer - Attach Outfit")
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    
    # UI elements
    lstOutfits = RegisteredElement(QtGui.QListWidget,"lstOutfits" + str(operatingAnimset))
    grid.addWidget(lstOutfits,1, 0, 10, 1)
    
    btnAttachOutfit = RegisteredElement(QtGui.QPushButton, "btnAttachOutfit" + str(operatingAnimset))
    btnAttachOutfit.setText("Attach Outfit")
    btnAttachOutfit.clicked.connect(lambda operatingAnimset=operatingAnimset: onAttachOutfit(operatingAnimset))
    grid.addWidget(btnAttachOutfit, 11, 0, 1, 1)

    InitializeOutfitBodygroupsAttachUI(operatingAnimset)
    
    # Display the window
    window.setLayout(grid)
    window.setModal(True)
    window.exec_()
    
def CreateUI_OutfitBodygroups(operatingAnimset):
    #animset = GetCurAnimset()
    animset = operatingAnimset
    
    if not hasattr(animset,"gameModel"):
        CreateAnimsetUI()
        return 0
        
    # Create the base UI window
    window = RegisteredElement(LordAardvarkShared.NonModalWindow,"windowBodygroups" + str(operatingAnimset))
    window.resize(360,720)
    window.setWindowTitle("HMF v1.0.0 - Outfit Bodygroups")

    applyBodygroup = window.safeConnect(lambda item, operatingAnimset=operatingAnimset: onTreeBodygroupItemChecked(item, operatingAnimset))
        
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    grid.setColumnMinimumWidth(1, 72)   

    # Tree widget for the outfit bodygroups
    treeBodygroups = RegisteredElement(QtGui.QTreeWidget,"treeBodygroups" + str(operatingAnimset))
    treeBodygroups.setColumnCount(1)
    treeBodygroups.setHeaderLabels(["Bodygroups"])
    treeBodygroups.header().setVisible(False)
    treeBodygroups.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
    treeBodygroups.setDropIndicatorShown(True)
    treeBodygroups.setItemsExpandable(False)
    treeBodygroups.setRootIsDecorated(False)
    treeBodygroups.setStyleSheet("QTreeWidget::branch { border-image: url(none.png); }")
    #treeBodygroups.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
    
    treeBodygroups.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    treeBodygroups.customContextMenuRequested.connect(lambda pos, operatingAnimset=operatingAnimset: CreateContext_BodygroupsTree(pos, operatingAnimset))
    
    #treeBodygroups.itemChanged.connect(lambda item: onTreeBodygroupItemChecked(item))
    treeBodygroups.itemChanged.connect(lambda item: window.safeCall(applyBodygroup,item))
    #treeBodygroups.itemClicked.connect(lambda item: fuckYou())
    #treeBodygroups.doubleClicked.connect(TreeItem_OnDoubleClick)
    grid.addWidget(treeBodygroups, 0, 0, 1, 2)
    
    InitializeBodygroupsTree(treeBodygroups, operatingAnimset)
    
    # Buttons
    btnApplyBodygroup = RegisteredElement(QtGui.QPushButton, "btnApplyBodygroup" + str(operatingAnimset))
    btnApplyBodygroup.setText("Apply")
    btnApplyBodygroup.clicked.connect(lambda: window.close())
    grid.addWidget(btnApplyBodygroup, 1, 0, 1, 1)
    
    btnConfirmBodygroups = RegisteredElement(QtGui.QPushButton, "btnConfirmBodygroups" + str(operatingAnimset))
    btnConfirmBodygroups.setText("Confirm")
    btnConfirmBodygroups.clicked.connect(lambda: window.close())
    grid.addWidget(btnConfirmBodygroups, 1, 1, 1, 1)
    
    # Display the window
    window.setLayout(grid)
    window.show()       
    
def CreateUI_CustomizeCharacter(operatingAnimset):
    global HMF_AnimsetQueue
    
    #animset = GetCurAnimset()
    animset = operatingAnimset
    
    # Create the base UI window
    window = RegisteredElement(LordAardvarkShared.NonModalWindow,"windowCustomizeCharacter" + str(operatingAnimset))
    window.resize(320,120)
    window.closed.connect(lambda: onMainWindowClosed())
    window.setWindowTitle("HMF v1.0.0 - Customize Character")
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)

    btnRows = [
        [ [ "Skin Tone", lambda operatingAnimset=operatingAnimset: CreateUI_SkinTone(operatingAnimset) ] ],
        [ [ "Face Texture", lambda operatingAnimset=operatingAnimset: CreateUI_FaceMap(operatingAnimset) ] ],
        [ [ "Eye Color", lambda operatingAnimset=operatingAnimset: CreateUI_EyeColor(operatingAnimset) ] ],
        [ [ "Hair Style", lambda operatingAnimset=operatingAnimset: CreateUI_HairStyle(operatingAnimset) ], [ "Set Active Hair", lambda operatingAnimset=operatingAnimset: CreateUI_HairStyle_SetActive(operatingAnimset) ] ],
        [ [ "Outfit", lambda operatingAnimset=operatingAnimset: CreateUI_OutfitBodygroups(operatingAnimset) ], [ "Set Active Outfit", lambda operatingAnimset=operatingAnimset: CreateUI_OutfitBodygroups_SetActive(operatingAnimset) ] ],
        [ [ "Confirm", lambda: window.close() ] ]
    ]
    
    CreateButtonRows(grid, btnRows, operatingAnimset)
    
    InitializeCustomizeUI(operatingAnimset)
    
    # Display the window
    window.setLayout(grid)
    window.show()
    
def CreateUI_SpawnCharacterOrModifyExistingCharacter():
    # Create the base UI window
    window = RegisteredElement(QtGui.QDialog, "windowSpawnOrModify")
    window.resize(600,480)
    window.setWindowTitle("Hitman Female Customizer - Spawn or Modify Character")
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    
    # UI elements
    # Existing animsets
    lblExistingAnimsets = QtGui.QLabel()
    lblExistingAnimsets.setText("Customize Existing:")
    grid.addWidget(lblExistingAnimsets, 0, 0, 1, 2)
    
    lstExistingAnimsets = RegisteredElement(QtGui.QListWidget,"lstExistingAnimsets")
    grid.addWidget(lstExistingAnimsets, 1, 0, 10, 2)
    
    btnAttachToSelected = RegisteredElement(QtGui.QPushButton, "btnAttachToSelected")
    btnAttachToSelected.setText("Attach To Selected")
    btnAttachToSelected.clicked.connect(lambda: onAttachToSelected())
    grid.addWidget(btnAttachToSelected, 11, 0, 1, 1)
    
    btnCustomizeSelected = RegisteredElement(QtGui.QPushButton, "btnCustomizeSelected")
    btnCustomizeSelected.setText("Customize Selected")
    #btnCustomizeSelected.clicked.connect(lambda: onModifySelected())
    btnCustomizeSelected.clicked.connect(lambda: onCustomizeSelected())
    grid.addWidget(btnCustomizeSelected, 11, 1, 1, 1)
    
    # New models
    lblNewAnimsets = QtGui.QLabel()
    lblNewAnimsets.setText("Spawn New Models [Double-Click to Spawn]")
    grid.addWidget(lblNewAnimsets, 0, 2, 1, 2)
    
    lstNewAnimsets = RegisteredElement(QtGui.QListWidget, "lstNewAnimsets")
    lstNewAnimsets.itemDoubleClicked.connect(lambda item: onSpawnCharacter(item))
    lstNewAnimsets.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    lstNewAnimsets.customContextMenuRequested.connect(CreateContext_NewAnimsets)
    grid.addWidget(lstNewAnimsets, 1, 2, 10, 2)
    
    txtSpawnCounter = RegisteredElement(QtGui.QLineEdit, "txtSpawnCounter")
    txtSpawnCounter.setText("0 Models Spawned")
    txtSpawnCounter.setReadOnly(True)
    grid.addWidget(txtSpawnCounter, 11, 2, 1, 2)
    
    # Initialize the lists
    InitializeCustomizableAnimsets()
    InitializeNewAnimsets()
    
    # Display the window
    window.setLayout(grid)
    window.setModal(True)
    window.exec_()

#def CreateUI_AttachElementsOrCustomizeCharacter():
def CreateUI_AttachElements(operatingAnimset):
    operatingAnimset = GetCurAnimset()
    
    # Create the base UI window
    #window = RegisteredElement(QDialogExtended, "windowSpawnOrModify")
    window = RegisteredElement(QDialogExtended, "windowAttachElements")
    window.resize(300,120)
    window.closed.connect(lambda: onIncrementAnimsetQueue())
    window.setWindowTitle("Hitman Female Customizer")
    window.setData(QtCore.Qt.UserRole, operatingAnimset)
    
    grid = QtGui.QGridLayout()
    grid.setSpacing(12)
    
    # UI elements
    btnRows = [
        [ [ "Attach Hair", lambda operatingAnimset=operatingAnimset: CreateUI_HairStyle_Attach(operatingAnimset) ] ],
        [ [ "Attach Outfit", lambda operatingAnimset=operatingAnimset: CreateUI_OutfitBodygroups_Attach(operatingAnimset) ] ],
        #[ [ "Customize Character", lambda window=window: onCustomizeCharacter(window.data(QtCore.Qt.UserRole)) ] ],
        [ [ "Confirm", lambda window=window: window.close() ] ]
    ]
    
    CreateButtonRows(grid, btnRows, operatingAnimset)
    
    # Display the window
    window.setLayout(grid)
    window.setModal(True)
    window.exec_()

def CreateUI():
    global HMF_bReportQueue
    
    HMF_bReportQueue = False
    
    animset = sfm.GetCurrentAnimationSet()
    
    AssertMaterialOverrideDefaultsUniversally()
    
    if not hasattr(animset,"gameModel"): # Running the script on a camera (or anything else that isn't a model) will prompt the user to create or modify existing animsets
        HMF_bReportQueue = True
        
        CreateUI_SpawnCharacterOrModifyExistingCharacter()
        return
        
    # Otherwise, we will modify the existing character
    CreateUI_CustomizeCharacter(animset)
    #CreateUI_AttachElementsOrCustomizeCharacter()
    
# Initialization
if len(HMF_GameModFolderNames) < 1:
    LordAardvarkShared.ParseGameInfo(HMF_GameInfoPath, HMF_GameModFolderNames)
    
LoadHairColorData("fuck you")
LoadHairStyleData("fuck you")

if hasattr(LordAardvarkShared,"Version") and LordAardvarkShared.Version[0] >= 1:
    CreateUI()
else:
    QtGui.QMessageBox.warning(None, "Module Version Error", "Your currently-installed version of the LordAardvarkShared module is out of date.\n\nPlease visit the Assets/Autoreleases page on LordAardvark.com and download & install the most recent version of the 2026 Ecosystem Tools, and restart SFM.\n\nIf this error persists, please message me on bluesky at lord-aardvark.bsky.social .")