local KEYBOARD_BUCKET_MANAGER

--[[ Slider Randomization Helper...all sliders share the sliderObject from the top control, so this just helps cut down on duplicate functions ]]--
local function RandomizeSlider(control, randomizeType)
    control.sliderObject:Randomize(randomizeType)
end

--[[ Character Create Slider and Appearance Slider Managers ]]--
-- Manages a collection of sliders with a pool

local CharacterCreateSliderManager = ZO_InitializingObject:Subclass()

function CharacterCreateSliderManager:Initialize(parent)
    local function CreateSlider(pool)
        local control = ZO_ObjectPool_CreateNamedControl("CharacterCreateSlider", "ZO_CharacterCreateSlider_Keyboard", pool, parent)
        return ZO_CharacterCreateSlider_Keyboard:New(control)
    end

    local function CreateAppearanceSlider(pool)
        local control = ZO_ObjectPool_CreateNamedControl("CharacterCreateAppearanceSlider", "ZO_CharacterCreateSlider_Keyboard", pool, parent)
        return ZO_CharacterCreateAppearanceSlider_Keyboard:New(control)
    end

    local function CreateColorPicker(pool)
        local control = ZO_ObjectPool_CreateNamedControl("CharacterCreateColorPicker", "ZO_CharacterCreateColorSlider_Keyboard", pool, parent)
        return ZO_CharacterCreateColorSlider_Keyboard:New(control)
    end

    local function CreateDropdown(pool)
        local control = ZO_ObjectPool_CreateNamedControl("CharacterCreateDropdown", "ZO_CharacterCreateDropDownSlider_Keyboard", pool, parent)
        return ZO_CharacterCreateDropdownSlider_Keyboard:New(control)
    end

    local function ResetSlider(slider)
        local sliderControl = slider.control
        KEYBOARD_BUCKET_MANAGER:RemoveControl(sliderControl)
        sliderControl:SetHidden(true)
        slider:SetLocked(false)
    end

    local function ResetColorPicker(slider)
        local sliderControl = slider.control
        KEYBOARD_BUCKET_MANAGER:RemoveControl(sliderControl)
        ZO_ColorSwatchPicker_Clear(sliderControl:GetNamedChild("Slider"))
        sliderControl:SetHidden(true)
        slider:SetLocked(false)
    end

    self.pools =
    {
        [CHARACTER_CREATE_SLIDER_TYPE_SLIDER] = ZO_ObjectPool:New(CreateSlider, ResetSlider),
        [CHARACTER_CREATE_SLIDER_TYPE_ICON] = ZO_ObjectPool:New(CreateAppearanceSlider, ResetSlider),
        [CHARACTER_CREATE_SLIDER_TYPE_COLOR] = ZO_ObjectPool:New(CreateColorPicker, ResetColorPicker),
        [CHARACTER_CREATE_SLIDER_TYPE_NAMED] = ZO_ObjectPool:New(CreateDropdown, ResetSlider),
    }
end

function CharacterCreateSliderManager:AcquireObject(objectType)
    local pool = self.pools[objectType]
    if pool then
        return pool:AcquireObject()
    end
end

function CharacterCreateSliderManager:ReleaseAllObjects()
    for poolType, pool in pairs(self.pools) do
        pool:ReleaseAllObjects()
    end
end

--[[ Creator Control Initialization ]]--

local function FindOrdering(orderingTable, name)
    for i = 1, #orderingTable do
        if orderingTable[i] == name then
            return i
        end
    end

    return 1
end

--[[ Character Create Keyboard ]]--

local ZO_CharacterCreate_Keyboard = ZO_CharacterCreate_Base:Subclass()

function ZO_CharacterCreate_Keyboard:Initialize(...)
    ZO_CharacterCreate_Base.Initialize(self, ...)

    local function HandleZoomChanged(eventCode, zoomInAllowed, zoomOutAllowed)
        ZO_CharacterCreateZoomIn:SetEnabled(zoomInAllowed)
        ZO_CharacterCreateZoomOut:SetEnabled(zoomOutAllowed)
    end

    EVENT_MANAGER:RegisterForEvent("ZO_CharacterCreate", EVENT_CHARACTER_CREATE_ZOOM_CHANGED, HandleZoomChanged)

    local function OnStateChanged(oldState, newState)
        if newState == SCENE_FRAGMENT_SHOWING then
            self:ResetControls()
        elseif newState == SCENE_FRAGMENT_HIDING then
            self:UpdateUnsavedSettings()
        end
    end

    CHARACTER_CREATE_FRAGMENT = ZO_FadeSceneFragment:New(self.control, 300)
    CHARACTER_CREATE_FRAGMENT:RegisterCallback("StateChange", OnStateChanged)
end

function ZO_CharacterCreate_Keyboard:UpdateUnsavedSettings()
    for name, nameTable in pairs(self.controlsByNameCategory) do
        for category, slider in pairs(nameTable) do
            ZO_CHARACTERCREATE_MANAGER:SetCharacterUnsavedSetting(name, category, slider:GetValue(), slider:IsLocked())
        end
    end

    local UNUSED_VALUE = nil
    ZO_CHARACTERCREATE_MANAGER:SetCharacterUnsavedSetting("triangle", "physique", UNUSED_VALUE, self.physiqueTriangle:IsLocked())
    ZO_CHARACTERCREATE_MANAGER:SetCharacterUnsavedSetting("triangle", "face", UNUSED_VALUE, self.faceTriangle:IsLocked())
end

function ZO_CharacterCreate_Keyboard:OnCharacterCreateRequested()
    self.createButton:SetEnabled(false)
end

do
    local reasonsThatDisableCreateButton =
    {
        [CHARACTER_CREATE_EDIT_ERROR_INVALID_NAME] = true,
        [CHARACTER_CREATE_EDIT_ERROR_DUPLICATE_NAME] = true,
        [CHARACTER_CREATE_EDIT_ERROR_NAME_TOO_SHORT] = true,
        [CHARACTER_CREATE_EDIT_ERROR_NAME_TOO_LONG] = true,
    }

    function ZO_CharacterCreate_Keyboard:OnCharacterCreateFailed(reason)
        local errorReason = GetString("SI_CHARACTERCREATEEDITERROR", reason)

        -- Show the fact that the character could not be created.
        ZO_Dialogs_ShowDialog("CHARACTER_CREATE_FAILED_REASON", nil, { mainTextParams = { errorReason } })

        if reasonsThatDisableCreateButton[reason] then
            self.createButton:SetEnabled(false)
        end
    end
end

do
    local function CreateTriangle(control, setterFn, updaterFn, triangleStringId, topStringId, leftStringId, rightStringId)
        local triangle = ZO_CharacterCreateTriangle_Keyboard:New(control, setterFn, updaterFn, triangleStringId, topStringId, leftStringId, rightStringId)
        triangle:SetOnValueChangedCallback(OnCharacterCreateOptionChanged)
        return triangle
    end

    function ZO_CharacterCreate_Keyboard:InitializeControls()
        self.control.fadeTimeline = ANIMATION_MANAGER:CreateTimelineFromVirtual("CharacterCreateMainControlsFade", self.control)

        self.bucketsControl = self.control:GetNamedChild("Buckets")

        self.sliderManager = CharacterCreateSliderManager:New(self.bucketsControl)

        -- create the radio button groups
        self.allianceRadioGroup = ZO_RadioButtonGroup:New()
        self.raceRadioGroup = ZO_RadioButtonGroup:New()
        self.classRadioGroup = ZO_RadioButtonGroup:New()
        self.genderRadioGroup = ZO_RadioButtonGroup:New()

        -- create the triangle controls
        self.physiqueTriangleControl = CreateControlFromVirtual("$(parent)PhysiqueSelection", self.control, "ZO_CharacterCreateTriangleTemplate_Keyboard")
        self.physiqueTriangle = CreateTriangle(self.physiqueTriangleControl, SetPhysique, GetPhysique, SI_CREATE_CHARACTER_BODY_TRIANGLE_LABEL, SI_CREATE_CHARACTER_TRIANGLE_MUSCULAR, SI_CREATE_CHARACTER_TRIANGLE_FAT, SI_CREATE_CHARACTER_TRIANGLE_THIN)

        self.faceTriangleControl = CreateControlFromVirtual("$(parent)FaceSelection", self.control, "ZO_CharacterCreateTriangleTemplate_Keyboard")
        self.faceTriangle = CreateTriangle(self.faceTriangleControl, SetFace, GetFace, SI_CREATE_CHARACTER_FACE_TRIANGLE_LABEL, SI_CREATE_CHARACTER_TRIANGLE_FACE_MUSCULAR, SI_CREATE_CHARACTER_TRIANGLE_FACE_FAT, SI_CREATE_CHARACTER_TRIANGLE_FACE_THIN)

        -- setup the create button and link it to the name control
        self.createButton = self.control:GetNamedChild("CreateButton")
        self.createButton:SetEnabled(false)

        self.instructionsControl = self.control:GetNamedChild("NameInstructions")

        local VALIDATOR_RULES =
        {
            NAME_RULE_TOO_SHORT,
            NAME_RULE_CANNOT_START_WITH_SPACE,
            NAME_RULE_MUST_END_WITH_LETTER,
            NAME_RULE_TOO_MANY_IDENTICAL_ADJACENT_CHARACTERS,
            NAME_RULE_NO_NUMBERS,
            NAME_RULE_NO_ADJACENT_PUNCTUATION_CHARACTERS,
            NAME_RULE_TOO_MANY_PUNCTUATION_CHARACTERS,
            NAME_RULE_INVALID_CHARACTERS
        }
        local DEFAULT_TEMPLATE = nil
        self.nameInstructionsObject = ZO_ValidNameInstructions:New(self.instructionsControl, DEFAULT_TEMPLATE, VALIDATOR_RULES)

        self.nameControl = self.control:GetNamedChild("CharacterName")
        SetupEditControlForNameValidation(self.nameControl)
        self.nameControl.linkedButton = self.createButton
        self.nameControl.linkedInstructions = self.nameInstructionsObject

        self.saveButton = self.control:GetNamedChild("SaveButton")

        self.templateControl = self.control:GetNamedChild("Template")

        self.raceSelectionControl = self.control:GetNamedChild("Race")
        self.classSelectionControl = self.control:GetNamedChild("ClassSelection")
        self.genderSelectionControl = self.control:GetNamedChild("GenderSelection")
    end
end

function ZO_CharacterCreate_Keyboard:InitializeSelectorButtonTextures(buttonControl, data)
    buttonControl:SetNormalTexture(data.normalIcon)
    buttonControl:SetPressedTexture(data.pressedIcon)
    buttonControl:SetMouseOverTexture(data.mouseoverIcon)
end

function ZO_CharacterCreate_Keyboard:InitializeSelectors()
    self:InitializeGenderSelector()
    self:InitializeAllianceSelectors()
    self:InitializeRaceSelectors()
    self:InitializeClassSelectors()
    self:InitializeTemplateList()
end

function ZO_CharacterCreate_Keyboard:InitializeAllianceSelectors()
    local layoutTable =
    {
        ZO_CharacterCreateRaceAllianceSelector1,
        ZO_CharacterCreateRaceAllianceSelector2,
        ZO_CharacterCreateRaceAllianceSelector3,
    }

    local alliances = self.characterData:GetAllianceInfo()
    for _, alliance in ipairs(alliances) do
        local selector = layoutTable[alliance.position]
        self:InitializeAllianceSelector(selector, alliance)
    end
end

function ZO_CharacterCreate_Keyboard:InitializeRaceSelectors()
    local layoutTable =
    {
        ZO_CharacterCreateRaceColumn11,
        ZO_CharacterCreateRaceColumn21,
        ZO_CharacterCreateRaceColumn31,
        ZO_CharacterCreateRaceColumn12,
        ZO_CharacterCreateRaceColumn22,
        ZO_CharacterCreateRaceColumn32,
        ZO_CharacterCreateRaceColumn13,
        ZO_CharacterCreateRaceColumn23,
        ZO_CharacterCreateRaceColumn33,
        ZO_CharacterCreateRaceSingleButton,
    }

    local races = self.characterData:GetRaceInfo()
    for _, race in ipairs(races) do
        local raceButton = layoutTable[race.position]
        self:InitializeSelectorButton(raceButton, race, self.raceRadioGroup)
        self:AddRaceSelectionDataToSelector(raceButton, race)
    end
end

function ZO_CharacterCreate_Keyboard:InitializeGenderSelector()
    local maleSelector = self.genderSelectionControl:GetNamedChild("Male")
    maleSelector:GetNamedChild("Label"):SetText(GetString("SI_GENDER", GENDER_MALE))

    local femaleSelector = self.genderSelectionControl:GetNamedChild("Female")
    femaleSelector:GetNamedChild("Label"):SetText(GetString("SI_GENDER", GENDER_FEMALE))

    local maleSelectorButton = maleSelector:GetNamedChild("Button")
    maleSelectorButton.gender = GENDER_MALE
    local femaleSelectorButton = femaleSelector:GetNamedChild("Button")
    femaleSelectorButton.gender = GENDER_FEMALE

    self.genderRadioGroup:Add(maleSelectorButton)
    self.genderRadioGroup:Add(femaleSelectorButton)
end

function ZO_CharacterCreate_Keyboard:InitializeTemplateList()
    local comboBox = ZO_ComboBox_ObjectFromContainer(self.templateControl)
    comboBox:ClearItems()
    comboBox:SetSortsItems(false)
    comboBox:SetFont("ZoFontGame")
    comboBox:SetSpacing(4)

    local function OnTemplateChanged(comboBox, entryText, entry)
        if self:SetTemplate(entry.templateData.template) then
            KEYBOARD_BUCKET_MANAGER:UpdateControlsFromData()
        end
    end

    local defaultTemplate = GetDefaultTemplate()

    local function SelectDefaultTemplate(entry)
        return entry.templateData.template == defaultTemplate
    end

    local templatesAllowed = GetTemplateStatus()
    if templatesAllowed then
        local templates = self.characterData:GetTemplateInfo()

        for _, templateData in ipairs(templates) do
            if templateData.isSelectable then
                local entry = comboBox:CreateItemEntry(templateData.name, OnTemplateChanged)
                entry.templateData = templateData
                comboBox:AddItem(entry)
            end
        end

        if not comboBox:SetSelectedItemByEval(SelectDefaultTemplate) then
            comboBox:SelectFirstItem()
        end

        self.templateControl:SetHidden(false)
    else
        self.templateControl:SetHidden(true)
    end
end

function ZO_CharacterCreate_Keyboard:SetTemplate(templateId)
    local templateData = self.characterData:GetTemplate(templateId)
    if not templateData then
        return false
    end

    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
    if not templateData.isSelectable or CharacterCreateGetTemplate(characterMode) == templateId then
        return false
    end

    CharacterCreateSetTemplate(templateId)

    local validRaces = {}
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateRaceSelectorsForTemplate(...) end, self.characterData:GetRaceInfo(), templateData, self.raceRadioGroup, validRaces)
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateClassSelectorsForTemplate(...) end, self.characterData:GetClassInfo(), templateData, self.classRadioGroup)

    local validAlliances = {}
    self.characterData:UpdateAllianceSelectability()
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateAllianceSelectorsForTemplate(...) end, self.characterData:GetAllianceInfo(), templateData, self.allianceRadioGroup, validAlliances)

    self:UpdateGenderSelectorsForTemplate(templateData)

    -- Pick a race
    if templateData.race ~= 0 then
        CharacterCreateSetRace(templateData.race)
    else
        CharacterCreateSetRace(self.characterData:PickRandomRace(validRaces))
    end

    -- Pick an alliance 
    if templateData.alliance ~= ALLIANCE_NONE then
        ZO_CharacterCreate_SetAlliance(templateData.alliance)
    else
        -- (never random unless a race without a fixed alliance is picked)
        local alliance = self.characterData:GetRaceForRaceDef(CharacterCreateGetRace(characterMode)).alliance
        if alliance ~= ALLIANCE_NONE then
            ZO_CharacterCreate_SetAlliance(alliance)
        else
            ZO_CharacterCreate_SetAlliance(self.characterData:PickRandomAlliance(validAlliances))
        end
    end

    -- Pick a class
    if templateData.class ~= 0 then
        self:SetClass(templateData.class)
    else
        -- UpdateSelectorsForTemplate() should be called prior to this function or unselectable classes might be set (which would result in no class being set).
        self:PickRandomSelectableClass()
    end

    -- Pick a gender
    if templateData.gender ~= GENDER_NEUTER then
        self:SetGender(templateData.gender)
    else
        self:PickRandomGender()
    end

    -- Make the controls match what you picked...
    self:ResetControls()
    if not templateData.overrideAppearance then
        ZO_CharacterCreate_RandomizeAppearance("initial")
    else
        InitializeAppearanceFromTemplate(templateId)
    end

    -- Disable appearance related controls if the appearance is overridden in the template.
    -- buckets need to be disabled after resetting the controls
    local enabled = not templateData.overrideAppearance
    KEYBOARD_BUCKET_MANAGER:EnableBucketTab(CREATE_BUCKET_BODY, enabled)
    KEYBOARD_BUCKET_MANAGER:EnableBucketTab(CREATE_BUCKET_FACE, enabled)

    ZO_CharacterCreateRandomizeAppearance:SetEnabled(enabled)

    return true
end

function ZO_CharacterCreate_Keyboard:InitializeClassSelectors()
        local classes = self.characterData:GetClassInfo()
        local numClasses = #classes
        -- This is how many classes overflow into the last line without filling it up
        local numOverflowClasses = numClasses % 3
        -- This is how many classes fill up our class grid without spilling onto an additional unfilled line
        local numGridClasses = numClasses - numOverflowClasses

        local parent = self.classSelectionControl:GetNamedChild("ButtonArea")
        local stride = 3
        local padX = 0
        local padY = 0
        local controlWidth = 120
        local controlHeight = 80
        local initialX = 0
        local initialY = 0

        local anchor = ZO_Anchor:New(TOPLEFT, parent, TOPLEFT, initialX, initialY)

        for i, classData in ipairs(classes) do
            local selectorName = "SelectClass" .. classData.class
            local selector = GetControl(selectorName)
            if not selector then
                selector = CreateControlFromVirtual(selectorName, parent, "ClassSelectorButton")
            end

            selector.nameFn = GetClassName
            selector.defId = classData.class

            self:InitializeSelectorButton(selector, classData, self.classRadioGroup)

            local controlIndex = i - 1
            if i > numGridClasses then
                -- If the last line has only one class in it, we want to center that class
                if numOverflowClasses % 2 == 1 then
                    controlIndex = i
                -- If the last line has two classes, we want to offset them by half their width so the whole thing is still symmetrical.
                else
                    -- The decimal value is valid because this value is only used to calculate anchor offsets.
                    controlIndex = i - 0.5
                end
            end

            ZO_Anchor_BoxLayout(anchor, selector, controlIndex, stride, padX, padY, controlWidth, controlHeight, initialX, initialY, GROW_DIRECTION_DOWN_RIGHT)
        end
    end

do
    -- This table defines how sliders should be ordered within their sub-categories.
    -- It's not needed for sub-categories where all the controls are added manually,
    -- but when sliders are added in loops (like characterSlider and appearanceSlider)
    -- this is used to assist building the ordering table which will be passed over
    -- to actually add the sliders.

    local CONTROL_ORDERING =
    {
        [SLIDER_SUBCAT_BODY_FEATURES] =
        {
            APPEARANCE_NAME_SKIN_TINT,
            APPEARANCE_NAME_BODY_MARKING,
            SLIDER_NAME_CHARACTER_HEIGHT,
        },

        [SLIDER_SUBCAT_BODY_UPPER] =
        {
            SLIDER_NAME_TORSO_SIZE,
            SLIDER_NAME_CHEST_SIZE,
            SLIDER_NAME_GUT_SIZE,
            SLIDER_NAME_WAIST_SIZE,
            SLIDER_NAME_ARM_SIZE,
            SLIDER_NAME_HAND_SIZE,
        },

        [SLIDER_SUBCAT_BODY_LOWER] =
        {
            SLIDER_NAME_TAIL_SIZE,
            SLIDER_NAME_HIP_SIZE,
            SLIDER_NAME_BUTTOCKS_SIZE,
            SLIDER_NAME_LEG_SIZE,
            SLIDER_NAME_FOOT_SIZE,
        },

        [SLIDER_SUBCAT_VOICE] =
        {
            APPEARANCE_NAME_VOICE,
        },

        [SLIDER_SUBCAT_FACE_HAIR] =
        {
            APPEARANCE_NAME_HAIR_STYLE,
            APPEARANCE_NAME_HAIR_TINT,
        },

        [SLIDER_SUBCAT_FACE_FEATURES] =
        {
            APPEARANCE_NAME_AGE,
            APPEARANCE_NAME_ACCESSORY,
            APPEARANCE_NAME_HEAD_MARKING,
        },

        [SLIDER_SUBCAT_FACE_FACE] =
        {
            SLIDER_NAME_FOREHEAD_SLOPE,
            SLIDER_NAME_CHEEK_BONE_SIZE,
            SLIDER_NAME_CHEEK_BONE_HEIGHT,
            SLIDER_NAME_JAW_SIZE,
            SLIDER_NAME_CHIN_SIZE,
            SLIDER_NAME_CHIN_HEIGHT,
            SLIDER_NAME_NECK_SIZE,
            SLIDER_NAME_TOOTH_SIZE,
        },

        [SLIDER_SUBCAT_FACE_EYES] =
        {
            APPEARANCE_NAME_EYE_TINT,
            SLIDER_NAME_EYE_SIZE,
            SLIDER_NAME_EYE_ANGLE,
            SLIDER_NAME_EYE_SEPARATION,
            SLIDER_NAME_EYE_HEIGHT,
            SLIDER_NAME_EYE_SQUINT,
        },

        [SLIDER_SUBCAT_FACE_BROW] =
        {
            APPEARANCE_NAME_EYEBROW,
            SLIDER_NAME_EYEBROW_HEIGHT,
            SLIDER_NAME_EYEBROW_ANGLE,
            SLIDER_NAME_EYEBROW_SKEW,
            SLIDER_NAME_EYEBROW_DEPTH,
        },

        [SLIDER_SUBCAT_FACE_NOSE] =
        {
            SLIDER_NAME_NOSE_SHAPE,
            SLIDER_NAME_NOSE_HEIGHT,
            SLIDER_NAME_NOSE_WIDTH,
            SLIDER_NAME_NOSE_LENGTH,
        },

        [SLIDER_SUBCAT_FACE_MOUTH] =
        {
            SLIDER_NAME_MOUTH_HEIGHT,
            SLIDER_NAME_MOUTH_WIDTH,
            SLIDER_NAME_MOUTH_CURVE,
            SLIDER_NAME_LIP_FULLNESS,
        },

        [SLIDER_SUBCAT_FACE_EARS] =
        {
            SLIDER_NAME_EAR_SIZE,
            SLIDER_NAME_EAR_ROTATION,
            SLIDER_NAME_EAR_HEIGHT,
            SLIDER_NAME_EAR_TIP_FLARE,
        },
    }

    local function SliderComparator(data1, data2)
        local name1 = data1.name
        local name2 = data2.name

        local subCat1 = data1.subCat
        local subCat2 = data2.subCat

        if subCat1 ~= subCat2 then
            return subCat1 < subCat2
        end

        return FindOrdering(CONTROL_ORDERING[subCat1], name1) < FindOrdering(CONTROL_ORDERING[subCat2], name2)
    end

    local SLIDER_CATEGORY_TO_CREATE_BUCKET =
    {
        [SLIDER_CATEGORY_AGE] = CREATE_BUCKET_BODY,
        [SLIDER_CATEGORY_BODY] = CREATE_BUCKET_BODY,
        [SLIDER_CATEGORY_FACE] = CREATE_BUCKET_FACE,
    }

    local APPEARANCE_NAME_TO_CREATE_BUCKET =
    {
        [APPEARANCE_NAME_HAIR_STYLE] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_HAIR_TINT] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_SKIN_TINT] = CREATE_BUCKET_BODY,
        [APPEARANCE_NAME_ACCESSORY] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_HEAD_MARKING] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_BODY_MARKING] = CREATE_BUCKET_BODY,
        [APPEARANCE_NAME_EYE_TINT] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_AGE] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_EYEBROW] = CREATE_BUCKET_FACE,
        [APPEARANCE_NAME_VOICE] = CREATE_BUCKET_FACE,
    }

    local SUBCATEGORY_FOR_SLIDER =
    {
        [SLIDER_NAME_FOREHEAD_SLOPE] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_CHEEK_BONE_SIZE] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_CHEEK_BONE_HEIGHT] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_CHIN_HEIGHT] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_CHIN_SIZE] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_JAW_SIZE] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_NECK_SIZE] = SLIDER_SUBCAT_FACE_FACE,
        [SLIDER_NAME_TOOTH_SIZE] = SLIDER_SUBCAT_FACE_FACE,

        [SLIDER_NAME_NOSE_SHAPE] = SLIDER_SUBCAT_FACE_NOSE,
        [SLIDER_NAME_NOSE_LENGTH] = SLIDER_SUBCAT_FACE_NOSE,
        [SLIDER_NAME_NOSE_HEIGHT] = SLIDER_SUBCAT_FACE_NOSE,
        [SLIDER_NAME_NOSE_WIDTH] = SLIDER_SUBCAT_FACE_NOSE,

        [SLIDER_NAME_EYE_HEIGHT] = SLIDER_SUBCAT_FACE_EYES,
        [SLIDER_NAME_EYE_SIZE] = SLIDER_SUBCAT_FACE_EYES,
        [SLIDER_NAME_EYE_SEPARATION] = SLIDER_SUBCAT_FACE_EYES,
        [SLIDER_NAME_EYE_ANGLE] = SLIDER_SUBCAT_FACE_EYES,
        [SLIDER_NAME_EYE_SQUINT] = SLIDER_SUBCAT_FACE_EYES,

        [SLIDER_NAME_MOUTH_HEIGHT] = SLIDER_SUBCAT_FACE_MOUTH,
        [SLIDER_NAME_MOUTH_WIDTH] = SLIDER_SUBCAT_FACE_MOUTH,
        [SLIDER_NAME_LIP_FULLNESS] = SLIDER_SUBCAT_FACE_MOUTH,
        [SLIDER_NAME_MOUTH_CURVE] = SLIDER_SUBCAT_FACE_MOUTH,

        [SLIDER_NAME_EAR_HEIGHT] = SLIDER_SUBCAT_FACE_EARS,
        [SLIDER_NAME_EAR_SIZE] = SLIDER_SUBCAT_FACE_EARS,
        [SLIDER_NAME_EAR_TIP_FLARE] = SLIDER_SUBCAT_FACE_EARS,
        [SLIDER_NAME_EAR_ROTATION] = SLIDER_SUBCAT_FACE_EARS,

        [SLIDER_NAME_EYEBROW_HEIGHT] = SLIDER_SUBCAT_FACE_BROW,
        [SLIDER_NAME_EYEBROW_ANGLE] = SLIDER_SUBCAT_FACE_BROW,
        [SLIDER_NAME_EYEBROW_SKEW] = SLIDER_SUBCAT_FACE_BROW,
        [SLIDER_NAME_EYEBROW_DEPTH] = SLIDER_SUBCAT_FACE_BROW,

        [SLIDER_NAME_LEG_SIZE] = SLIDER_SUBCAT_BODY_LOWER,
        [SLIDER_NAME_HIP_SIZE] = SLIDER_SUBCAT_BODY_LOWER,
    
        [SLIDER_NAME_FOOT_SIZE] = SLIDER_SUBCAT_BODY_LOWER,
        [SLIDER_NAME_BUTTOCKS_SIZE] = SLIDER_SUBCAT_BODY_LOWER,
        [SLIDER_NAME_TAIL_SIZE] = SLIDER_SUBCAT_BODY_LOWER,

        [SLIDER_NAME_TORSO_SIZE] = SLIDER_SUBCAT_BODY_UPPER,
        [SLIDER_NAME_HAND_SIZE] = SLIDER_SUBCAT_BODY_UPPER,
        [SLIDER_NAME_GUT_SIZE] = SLIDER_SUBCAT_BODY_UPPER,
        [SLIDER_NAME_ARM_SIZE] = SLIDER_SUBCAT_BODY_UPPER,
        [SLIDER_NAME_CHEST_SIZE] = SLIDER_SUBCAT_BODY_UPPER,
        [SLIDER_NAME_WAIST_SIZE] = SLIDER_SUBCAT_BODY_UPPER,

        [SLIDER_NAME_CHARACTER_HEIGHT] = SLIDER_SUBCAT_BODY_TYPE,
    }

    local SUBCATEGORY_FOR_APPEARANCE =
    {
        [APPEARANCE_NAME_HAIR_STYLE] = SLIDER_SUBCAT_FACE_HAIR,
        [APPEARANCE_NAME_HAIR_TINT] = SLIDER_SUBCAT_FACE_HAIR,
        [APPEARANCE_NAME_ACCESSORY] = SLIDER_SUBCAT_FACE_FEATURES,
        [APPEARANCE_NAME_HEAD_MARKING] = SLIDER_SUBCAT_FACE_FEATURES,
        [APPEARANCE_NAME_AGE] = SLIDER_SUBCAT_FACE_FEATURES,
        [APPEARANCE_NAME_BODY_MARKING] = SLIDER_SUBCAT_BODY_FEATURES,
        [APPEARANCE_NAME_SKIN_TINT] = SLIDER_SUBCAT_BODY_FEATURES,
        [APPEARANCE_NAME_EYE_TINT] = SLIDER_SUBCAT_FACE_EYES,
        [APPEARANCE_NAME_EYEBROW] = SLIDER_SUBCAT_FACE_BROW,
        [APPEARANCE_NAME_VOICE] = SLIDER_SUBCAT_VOICE,
    }

    local function UpdateSlider(slider)
        slider.sliderObject:Update()
    end

    function ZO_CharacterCreate_Keyboard:ResetControls()
        -- If this was being suppressed changes MUST be applied now or there will be no slider data to build
        SetSuppressCharacterChanges(false)

        self.sliderManager:ReleaseAllObjects()

        KEYBOARD_BUCKET_MANAGER:Reset()
        KEYBOARD_BUCKET_MANAGER:AddSubCategories()
        KEYBOARD_BUCKET_MANAGER:AddControl(self.genderSelectionControl, CREATE_BUCKET_RACE, function() self:UpdateGenderControl() end)
        KEYBOARD_BUCKET_MANAGER:AddControl(self.raceSelectionControl, CREATE_BUCKET_RACE, function() self:UpdateRaceControl() end)
        KEYBOARD_BUCKET_MANAGER:AddControl(self.classSelectionControl, CREATE_BUCKET_CLASS, function() self:UpdateClassControl() end)
        KEYBOARD_BUCKET_MANAGER:AddControl(self.physiqueTriangleControl, CREATE_BUCKET_BODY, UpdateSlider, RandomizeSlider, SLIDER_SUBCAT_BODY_TYPE)
        KEYBOARD_BUCKET_MANAGER:AddControl(self.faceTriangleControl, CREATE_BUCKET_FACE, UpdateSlider, RandomizeSlider, SLIDER_SUBCAT_FACE_TYPE)

        local _, physiqueIsLocked = ZO_CHARACTERCREATE_MANAGER:GetCharacterUnsavedSetting("triangle", "physique")
        if physiqueIsLocked ~= nil then
            self.physiqueTriangle:SetLocked(physiqueIsLocked)
        end

        local _, faceIsLocked = ZO_CHARACTERCREATE_MANAGER:GetCharacterUnsavedSetting("triangle", "face")
        if faceIsLocked ~= nil then
            self.faceTriangle:SetLocked(faceIsLocked)
        end

        -- TODO: this fixes a bug where the triangles don't reflect the correct data...there will be more fixes to pregameCharacterManager to address the real issue
        -- (where the triangle data needs to live on its own rather than being tied to the unit)
        self.physiqueTriangle:Update()
        self.faceTriangle:Update()

        self.controlsByNameCategory = {}
        local sliderData = {}

        for i = 1, GetNumSliders() do
            local name, category, steps, value, defaultValue = GetSliderInfo(i)

            if name then
                local slider = self.sliderManager:AcquireObject(CHARACTER_CREATE_SLIDER_TYPE_SLIDER)
                local unsavedValue, unsavedIsLocked = ZO_CHARACTERCREATE_MANAGER:GetCharacterUnsavedSetting(name, category)
                if unsavedValue and unsavedIsLocked then
                    slider:SetLocked(unsavedIsLocked)
                end
                slider:SetData(i, name, category, steps, unsavedValue or value, defaultValue)

                local bucket = SLIDER_CATEGORY_TO_CREATE_BUCKET[category]
                local subCat = SUBCATEGORY_FOR_SLIDER[name]

                sliderData[#sliderData + 1] =
                {
                    bucket = bucket,
                    subCat = subCat,
                    name = name,
                    control = slider.control,
                }

                if not self.controlsByNameCategory[name] then
                    self.controlsByNameCategory[name] = {}
                end

                self.controlsByNameCategory[name][category] = slider
            end
        end

        for i = 1, GetNumAppearances() do
            local appearanceName, appearanceType, numValues, displayName = GetAppearanceInfo(i)

            if numValues > 0 then
                local appearanceSlider = self.sliderManager:AcquireObject(appearanceType)
                local unsavedValue, unsavedIsLocked = ZO_CHARACTERCREATE_MANAGER:GetCharacterUnsavedSetting(appearanceName, appearanceType)
                if unsavedValue and unsavedIsLocked then
                    appearanceSlider:SetLocked(unsavedIsLocked)
                end
                appearanceSlider:SetData(appearanceName, numValues, displayName)

                local bucket = APPEARANCE_NAME_TO_CREATE_BUCKET[appearanceName]
                local subCat = SUBCATEGORY_FOR_APPEARANCE[appearanceName]

                sliderData[#sliderData + 1] =
                {
                    bucket = bucket,
                    subCat = subCat,
                    name = appearanceName,
                    control = appearanceSlider.control,
                }

                if not self.controlsByNameCategory[appearanceName] then
                    self.controlsByNameCategory[appearanceName] = {}
                end

                self.controlsByNameCategory[appearanceName][appearanceType] = appearanceSlider
            end
        end

        table.sort(sliderData, SliderComparator)

        for _, orderingData in ipairs(sliderData) do
            KEYBOARD_BUCKET_MANAGER:AddControl(orderingData.control, orderingData.bucket, UpdateSlider, RandomizeSlider, orderingData.subCat)
        end

        KEYBOARD_BUCKET_MANAGER:RemoveUnusedSubCategories()
        KEYBOARD_BUCKET_MANAGER:SwitchBuckets(CREATE_BUCKET_RACE)

        local appearanceControlsEnabled = self:DoesCurrentCharacterCreateModeAllowAppearanceChange()
        KEYBOARD_BUCKET_MANAGER:EnableBucketTab(CREATE_BUCKET_BODY, appearanceControlsEnabled)
        KEYBOARD_BUCKET_MANAGER:EnableBucketTab(CREATE_BUCKET_FACE, appearanceControlsEnabled)
        
        local classControlsEnabled = self:DoesCurrentCharacterCreateModeAllowClassChange()
        KEYBOARD_BUCKET_MANAGER:EnableBucketTab(CREATE_BUCKET_CLASS, classControlsEnabled)

        -- Update Gender Text
        self:UpdateGenderSpecificText()
    end
end

function ZO_CharacterCreate_Keyboard:ResetNameEdit()
    local nameEdit = self.nameControl
    nameEdit:TakeFocus() -- Fix an issue where the animated name text wouldn't display
    nameEdit:SetText("")
    nameEdit:LoseFocus()
    self.nameControl:GetNamedChild("Instructions"):SetHidden(false)
end

function ZO_CharacterCreate_Keyboard:Reset()
    -- Sanity check
    if not IsPregameCharacterConstructionReady() then
        return
    end

    SetCharacterCameraZoomAmount(-1) -- zoom all the way out when a reset happens
    SetSuppressCharacterChanges(true) -- this will be disabled later, right before controls are reset

    self:ResetNameEdit()

    local controlsInitialized = false

    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
    if characterMode == CHARACTER_MODE_CREATION then
        self:InitializeTemplateList()

        -- assume that we have already initialized the controls because we have set the default template
        controlsInitialized = true
        local defaultTemplate = GetDefaultTemplate()
        if defaultTemplate == 0 then
            -- default template was undefined so see if we generated a new random character
            controlsInitialized = self:GenerateRandomCharacter()
        end
    end

    if not controlsInitialized then
        self:ResetControls()
    end

    KEYBOARD_BUCKET_MANAGER:SwitchBuckets(CREATE_BUCKET_RACE)
    KEYBOARD_BUCKET_MANAGER:UpdateControlsFromData()

    self.characterStartLocation = nil
    ZO_CHARACTERCREATE_MANAGER:SetShouldPromptForTutorialSkip(true)
    self.characterCreateOption = CHARACTER_CREATE_DEFAULT_LOCATION
end

function ZO_CharacterCreate_Keyboard:UpdateGenderSpecificText(currentGender)
    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
    currentGender = currentGender or CharacterCreateGetGender(characterMode)

    self.raceSelectionControl:GetNamedChild("Name"):SetText(zo_strformat(SI_RACE_NAME, GetRaceName(currentGender, CharacterCreateGetRace(characterMode))))
    self.classSelectionControl:GetNamedChild("Name"):SetText(zo_strformat(SI_CLASS_NAME, GetClassName(currentGender, CharacterCreateGetClass(characterMode))))
end

function ZO_CharacterCreate_Keyboard:UpdateRaceControl()
    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
    local currentRace = CharacterCreateGetRace(characterMode)

    local function IsRaceClicked(button)
        return button.defId == currentRace
    end

    self.raceRadioGroup:UpdateFromData(IsRaceClicked)

    local currentAlliance = CharacterCreateGetAlliance(characterMode)

    local function IsAllianceClicked(button)
        return button.defId == currentAlliance
    end

    self.allianceRadioGroup:UpdateFromData(IsAllianceClicked)

    local race = self.characterData:GetRaceForRaceDef(currentRace)
    if race then
        local allianceName, backdropTop, backdropBottom = self:GetCurrentAllianceData()

        self:UpdateGenderSpecificText()

        self.raceSelectionControl:GetNamedChild("Alliance"):SetText(zo_strformat(SI_ALLIANCE_NAME, allianceName))
        self.raceSelectionControl:GetNamedChild("Description"):SetText(race.lore)

        self.raceSelectionControl:GetNamedChild("AllianceBG"):SetTexture(backdropTop)
        self.raceSelectionControl:GetNamedChild("AllianceBGBottom"):SetTexture(backdropBottom)
    end
end

function ZO_CharacterCreate_Keyboard:UpdateGenderControl()
    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
    local currentGender = CharacterCreateGetGender(characterMode)

    local function IsGenderClicked(button)
        return button.gender == currentGender
    end

    self.genderRadioGroup:UpdateFromData(IsGenderClicked)
end

function ZO_CharacterCreate_Keyboard:UpdateClassControl()
    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
    local currentClass = CharacterCreateGetClass(characterMode)

    local function IsClassClicked(button)
        return button.defId == currentClass
    end

    self.classRadioGroup:UpdateFromData(IsClassClicked)

    local class = self.characterData:GetClassForClassDef(currentClass)
    if class then
        self:UpdateGenderSpecificText()
        self.classSelectionControl:GetNamedChild("Description"):SetText(class.lore)
    end
end

function ZO_CharacterCreate_Keyboard:OnGenerateRandomCharacter()
    ZO_CharacterCreate_RandomizeAppearance("initial")
end

function ZO_CharacterCreate_Keyboard:CreateCharacter(startLocation, createOption)
    self.characterName = self.nameControl:GetText()
    ZO_CharacterCreate_Base.CreateCharacter(self, startLocation, createOption)
end

function ZO_CharacterCreate_Keyboard:OnCreateButtonClicked(startLocation)
    local characterName = self.nameControl:GetText()

    if characterName and #characterName > 0 then
        local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
        local currentTemplateId = CharacterCreateGetTemplate(characterMode)
        local templateSkipsTutorial = GetTemplateSkipsTutorial(currentTemplateId)

        if ZO_CHARACTERCREATE_MANAGER:GetShouldPromptForTutorialSkip() and CanSkipTutorialArea() and (currentTemplateId == 0 or not templateSkipsTutorial) and startLocation ~= CHARACTER_OPTION_CLEAN_TEST_AREA and startLocation ~= "CharacterSelect_FromIngame" then
            ZO_CHARACTERCREATE_MANAGER:SetShouldPromptForTutorialSkip(false)
            -- color the character name white so it's highlighted in the dialog
            local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
            local genderDecoratedCharacterName = ZO_SELECTED_TEXT:Colorize(GetGrammarDecoratedName(characterName, CharacterCreateGetGender(characterMode)))
            ZO_Dialogs_ShowDialog("CHARACTER_CREATE_SKIP_TUTORIAL", { startLocation = startLocation }, {mainTextParams = { genderDecoratedCharacterName }})
        else
            if currentTemplateId ~= 0 and templateSkipsTutorial then 
                self.characterCreateOption = CHARACTER_CREATE_SKIP_TUTORIAL
            end
            self:CreateCharacter(startLocation, self.characterCreateOption)
        end
    end
end

function ZO_CharacterCreate_Keyboard:InitializeForEditChanges(characterInfo, mode)
    self:SetCharacterCreateMode(mode)

    local raceTemplate = characterInfo
    if mode == CHARACTER_CREATE_MODE_EDIT_RACE then
        raceTemplate =
        {
            race = 0,
            alliance = characterInfo.alliance,
        }
    end

    local genderTemplate =
    {
        gender = GENDER_NEUTER
    }

    local allianceTemplate = characterInfo
    if mode == CHARACTER_CREATE_MODE_EDIT_ALLIANCE then
        allianceTemplate =
        {
            alliance = ALLIANCE_NONE,
        }
        genderTemplate = characterInfo
    end

    self:UpdateGenderSelectorsForTemplate(genderTemplate)

    self:UpdateSelectorsForTemplate(function(...) return self:UpdateRaceSelectorsForTemplate(...) end, self.characterData:GetRaceInfo(), raceTemplate, self.raceRadioGroup)
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateClassSelectorsForTemplate(...) end, self.characterData:GetClassInfo(), characterInfo, self.classRadioGroup)
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateAllianceSelectorsForTemplate(...) end, self.characterData:GetAllianceInfo(), allianceTemplate, self.allianceRadioGroup)

    self:Reset()

    local appearanceControlsEnabled = self:DoesCurrentCharacterCreateModeAllowAppearanceChange()
    ZO_CharacterCreateRandomizeAppearance:SetEnabled(appearanceControlsEnabled)

    self.templateControl:SetHidden(true)

    local name = zo_strformat(SI_CHARACTER_SELECT_NAME, characterInfo.name)
    self.nameControl:SetText(name)
    self.nameControl:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGB())
    self.nameControl:GetNamedChild("Instructions"):SetHidden(true)
    self.nameControl:SetEditEnabled(false)
    self.nameControl:SetMouseEnabled(false)

    self.saveButton:SetHidden(false)
    self.createButton:SetHidden(true)
end

function ZO_CharacterCreate_Keyboard:InitializeForCharacterCreate()
    self:SetCharacterCreateMode(CHARACTER_CREATE_MODE_CREATE)
    local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()

    local templateData = self.characterData:GetTemplate(CharacterCreateGetTemplate(characterMode))
    -- we may not have any template selected or we have no templates
    -- so create a default template with no restrictions
    if templateData == nil then
        templateData = self.characterData:GetNoneTemplate()
    end

    ZO_CharacterCreateRandomizeAppearance:SetEnabled(true)

    self:UpdateGenderSelectorsForTemplate(templateData)

    self:UpdateSelectorsForTemplate(function(...) return self:UpdateRaceSelectorsForTemplate(...) end, self.characterData:GetRaceInfo(), templateData, self.raceRadioGroup)
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateClassSelectorsForTemplate(...) end, self.characterData:GetClassInfo(), templateData, self.classRadioGroup)

    self.characterData:UpdateAllianceSelectability()
    self:UpdateSelectorsForTemplate(function(...) return self:UpdateAllianceSelectorsForTemplate(...) end, self.characterData:GetAllianceInfo(), templateData, self.allianceRadioGroup)

    self.nameControl:SetEditEnabled(true)
    self.nameControl:SetMouseEnabled(true)
    self.nameControl:SetColor(ZO_NORMAL_TEXT:UnpackRGB())

    self.saveButton:SetHidden(true)
    self.createButton:SetHidden(false)

    local currentAlliance = CharacterCreateGetAlliance(characterMode)
    ZO_CharacterCreate_SetChromaColorForAlliance(currentAlliance)
end


function ZO_CharacterCreate_Keyboard:UpdateGenderSelectorsForTemplate(templateData)
    for button in self.genderRadioGroup:IterateButtons() do
        local enabled = templateData.gender == GENDER_NEUTER or templateData.gender == button.gender
        self:SetSelectorButtonEnabled(button, self.genderRadioGroup, enabled)
    end
end

function ZO_CharacterCreate_Keyboard:ClickGenderButton(genderButton)
    self.genderRadioGroup:SetClickedButton(genderButton)
end

--
--[[ XML Handlers and global functions ]]--
--

function ZO_CharacterCreate_RandomizeAppearance(randomizeType)
    KEYBOARD_BUCKET_MANAGER:RandomizeAppearance(randomizeType)
end

function ZO_CharacterCreate_RandomizeAppearance_OnMouseEnter(control)
    -- If a button is disabled, add any disable reasons to the tooltip
    if control:GetState() == BSTATE_DISABLED or control:GetState() == BSTATE_DISABLED_PRESSED then
        InitializeTooltip(InformationTooltip, control, BOTTOM, 0, -5, TOP)

        -- Check if disabled due to a barbershop mode
        local characterCreateMode = KEYBOARD_CHARACTER_CREATE_MANAGER:GetCharacterCreateMode()
        if characterCreateMode ~= CHARACTER_CREATE_MODE_CREATE then
            local tokenType = KEYBOARD_CHARACTER_CREATE_MANAGER:GetTokenTypeForCharacterCreateMode(characterCreateMode)
            local tokenString = GetString("SI_SERVICETOKENTYPE", tokenType)
            InformationTooltip:AddLine(zo_strformat(SI_CREATE_CHARACTER_SELECTOR_TOKEN_DISABLED, tokenString), "", ZO_NORMAL_TEXT:UnpackRGB())
        end
    end
end

function ZO_CharacterCreate_RandomizeAppearance_OnMouseExit(control)
    ClearTooltip(InformationTooltip)
end

function ZO_CharacterCreate_Initialize(control)
    KEYBOARD_CHARACTER_CREATE_MANAGER = ZO_CharacterCreate_Keyboard:New(control)
    SYSTEMS:RegisterKeyboardObject(ZO_CHARACTER_CREATE_SYSTEM_NAME, KEYBOARD_CHARACTER_CREATE_MANAGER)

    KEYBOARD_BUCKET_MANAGER = ZO_CharacterCreateBucketManager_Keyboard:New(ZO_CharacterCreateBuckets)
end

function ZO_CharacterCreateSkipTutorialDialog_Initialize(control)
    ZO_Dialogs_RegisterCustomDialog("CHARACTER_CREATE_SKIP_TUTORIAL",
    {
        customControl = control,
        canQueue = true,
        title =
        {
            text = SI_PROMPT_TITLE_SKIP_TUTORIAL,
        },
        mainText = 
        {
            text = SI_PROMPT_BODY_SKIP_TUTORIAL,
        },
        noChoiceCallback = function(dialog)
            ZO_CHARACTERCREATE_MANAGER:SetShouldPromptForTutorialSkip(true)
        end,
        buttons =
        {
            {
                control = control:GetNamedChild("Play"),
                text = SI_PROMPT_PLAY_TUTORIAL_BUTTON,
                keybind = "DIALOG_PRIMARY",
                callback = function(dialog)
                    KEYBOARD_CHARACTER_CREATE_MANAGER:CreateCharacter(dialog.data.startLocation, CHARACTER_CREATE_DEFAULT_LOCATION)
                end,
            },

            {
                control = control:GetNamedChild("Skip"),
                text = SI_PROMPT_SKIP_TUTORIAL_BUTTON,
                keybind = "DIALOG_SECONDARY",
                callback = function(dialog)
                    KEYBOARD_CHARACTER_CREATE_MANAGER:CreateCharacter(dialog.data.startLocation, CHARACTER_CREATE_SKIP_TUTORIAL)
                end,
            },

            {
                control = control:GetNamedChild("Back"),
                text = SI_PROMPT_BACK_TUTORIAL_BUTTON,
                keybind = "DIALOG_NEGATIVE",
                callback = function(dialog)
                    ZO_CHARACTERCREATE_MANAGER:SetShouldPromptForTutorialSkip(true)
                end,
            },
        }
    })
end

function ZO_CharacterCreate_OnCreateButtonClicked(startLocation)
    KEYBOARD_CHARACTER_CREATE_MANAGER:OnCreateButtonClicked(startLocation)
end

function ZO_CharacterCreate_OnSaveButtonClicked()
    KEYBOARD_CHARACTER_CREATE_MANAGER:SaveCharacterChanges()
end

local function ValidateNameText(editControl)
    local nameText = editControl:GetText()
    local nameViolations = { IsValidCharacterName(nameText) }
    local nameIsValid = (#nameViolations == 0)

    if nameIsValid then
        editControl:SetColor(GetInterfaceColor(INTERFACE_COLOR_TYPE_TEXT_COLORS, INTERFACE_TEXT_COLOR_NORMAL))
    else
        editControl:SetColor(GetInterfaceColor(INTERFACE_COLOR_TYPE_TEXT_COLORS, INTERFACE_TEXT_COLOR_FAILED))
    end

    local oldPos = editControl:GetCursorPosition()
    editControl:SetText(CorrectCharacterNameCase(nameText))
    editControl:SetCursorPosition(oldPos)

    return nameIsValid, nameViolations
end

function ZO_CharacterCreate_CheckEnableCreateButton(editControl)
    -- Validation changes the text in the edit control, which causes this to be called again, bail if we're already validating to avoid recursion.
    if editControl.validating then
        return
    end

    editControl.validating = true

    local isValidName, nameViolations = ValidateNameText(editControl)

    if isValidName then
        editControl.linkedButton:SetState(BSTATE_NORMAL, false)
    else
        editControl.linkedButton:SetState(BSTATE_DISABLED, true)
    end

    if editControl:HasFocus() then
        editControl.linkedInstructions:Show(editControl, nameViolations)
    else
        editControl.linkedInstructions:Hide()
    end

    CALLBACK_MANAGER:FireCallbacks("OnCharacterCreateNameChanged", isValidName)
    editControl.validating = nil
end

function ZO_CharacterCreate_OnNameFieldFocusGained(editControl)
    editControl:GetNamedChild("Instructions"):SetHidden(true)
    editControl.linkedInstructions:Show(editControl)
    ZO_CharacterCreate_CheckEnableCreateButton(editControl)

    if WINDOW_MANAGER:IsHandlingHardwareEvent() then
        PlaySound(SOUNDS.EDIT_CLICK)
    end
end

function ZO_CharacterCreate_OnNameFieldFocusLost(editControl)
    if #editControl:GetText() == 0 then
        editControl:GetNamedChild("Instructions"):SetHidden(false)
    end
    editControl.linkedInstructions:Hide()
end

do
    local SELECTOR_CLICK_HANDLERS =
    {
        [CHARACTER_CREATE_SELECTOR_RACE] = function(button)
            KEYBOARD_CHARACTER_CREATE_MANAGER:SetRace(button.defId)
            KEYBOARD_CHARACTER_CREATE_MANAGER:UpdateRaceControl()
        end,

        [CHARACTER_CREATE_SELECTOR_CLASS] = function(button)
            KEYBOARD_CHARACTER_CREATE_MANAGER:SetClass(button.defId)
            KEYBOARD_CHARACTER_CREATE_MANAGER:UpdateClassControl()
        end,

        [CHARACTER_CREATE_SELECTOR_ALLIANCE] = function(button)
            KEYBOARD_CHARACTER_CREATE_MANAGER:SetAlliance(button.defId)
            KEYBOARD_CHARACTER_CREATE_MANAGER:UpdateRaceControl()
        end,

        [CHARACTER_CREATE_SELECTOR_GENDER] = function(button)
            KEYBOARD_CHARACTER_CREATE_MANAGER:SetGender(button.gender)
        end,
    }

    function ZO_CharacterCreate_OnSelectorClicked(button)
        local clickHandler = SELECTOR_CLICK_HANDLERS[button.selectorType]
        if clickHandler then
            OnCharacterCreateOptionChanged()
            clickHandler(button)
        end
    end
end

function ZO_CharacterCreate_MouseEnterNamedSelector(button)
    InitializeTooltip(InformationTooltip, button, TOPRIGHT, 0, 0, TOPLEFT)

    if button.name then
        SetTooltipText(InformationTooltip, zo_strformat(button.tooltipFormatter, button.name))
    elseif button.nameFn then
        local characterMode = ZO_CHARACTERCREATE_MANAGER:GetCharacterMode()
        SetTooltipText(InformationTooltip, zo_strformat(button.tooltipFormatter, button.nameFn(CharacterCreateGetGender(characterMode), button.defId)))
    end

    -- If a button is disabled, add any disable reasons to the tooltip
    if button:GetState() == BSTATE_DISABLED or button:GetState() == BSTATE_DISABLED_PRESSED then
        -- Check if disabled due to a barbershop mode
        local characterCreateMode = KEYBOARD_CHARACTER_CREATE_MANAGER:GetCharacterCreateMode()
        if characterCreateMode ~= CHARACTER_CREATE_MODE_CREATE then
            local selectorType = button.selectorType

            local addDisableReason = false
            local tokenType = KEYBOARD_CHARACTER_CREATE_MANAGER:GetTokenTypeForCharacterCreateMode(characterCreateMode)
            if tokenType == SERVICE_TOKEN_APPEARANCE_CHANGE then
                addDisableReason = selectorType == CHARACTER_CREATE_SELECTOR_RACE or selectorType == CHARACTER_CREATE_SELECTOR_CLASS or selectorType == CHARACTER_CREATE_SELECTOR_ALLIANCE
            elseif tokenType == SERVICE_TOKEN_RACE_CHANGE then
                addDisableReason = selectorType == CHARACTER_CREATE_SELECTOR_CLASS or selectorType == CHARACTER_CREATE_SELECTOR_ALLIANCE
            elseif tokenType == SERVICE_TOKEN_ALLIANCE_CHANGE then
                addDisableReason = selectorType == CHARACTER_CREATE_SELECTOR_RACE or selectorType == CHARACTER_CREATE_SELECTOR_CLASS or selectorType == CHARACTER_CREATE_SELECTOR_GENDER
            end

            if addDisableReason then
                local tokenString = GetString("SI_SERVICETOKENTYPE", tokenType)
                InformationTooltip:AddLine(zo_strformat(SI_CREATE_CHARACTER_SELECTOR_TOKEN_DISABLED, tokenString), "", ZO_NORMAL_TEXT:UnpackRGB())
            end
        end

        local raceSelector = button.selectorType == CHARACTER_CREATE_SELECTOR_RACE
        local classSelector = button.selectorType == CHARACTER_CREATE_SELECTOR_CLASS

        -- Check for race/class specific disable reasons
        if raceSelector or classSelector then
            local restrictionReasonFunction = raceSelector and GetRaceRestrictionReason or GetClassRestrictionReason
            local restrictionReason, restrictingCollectible = restrictionReasonFunction(button.defId)
            local restrictionString = ZO_CHARACTERCREATE_MANAGER.GetOptionRestrictionString(restrictionReason, restrictingCollectible)
            if restrictionString ~= "" then
                InformationTooltip:AddLine(restrictionString, "", ZO_NORMAL_TEXT:UnpackRGB())

                if restrictingCollectible ~= 0 and IsCollectiblePurchasable(restrictingCollectible) then
                    InformationTooltip:AddLine(GetString(SI_CHARACTER_CREATE_RESTRICTION_COLLECTIBLE_PURCHASABLE), "", ZO_NORMAL_TEXT:UnpackRGB())
                end
            end
        end
    end
end

function ZO_CharacterCreate_MouseExitNamedSelector(button)
    ClearTooltip(InformationTooltip)
end

function ZO_CharacterCreateGenderSelector_OnMouseEnter(button)
    local characterCreateMode = KEYBOARD_CHARACTER_CREATE_MANAGER:GetCharacterCreateMode()
    if characterCreateMode == CHARACTER_CREATE_MODE_EDIT_ALLIANCE then
        if button:GetState() == BSTATE_DISABLED or button:GetState() == BSTATE_DISABLED_PRESSED then
            InitializeTooltip(InformationTooltip, button, TOPRIGHT, 0, 0, TOPLEFT)
            local tokenString = GetString("SI_SERVICETOKENTYPE", SERVICE_TOKEN_ALLIANCE_CHANGE)
            InformationTooltip:AddLine(zo_strformat(SI_CREATE_CHARACTER_SELECTOR_TOKEN_DISABLED, tokenString), "", ZO_NORMAL_TEXT:UnpackRGB())
        end
    end
end

function ZO_CharacterCreateGenderSelector_OnMouseExit(button)
    ClearTooltip(InformationTooltip)
end

function ZO_CharacterCreateGenderSelectorLabel_OnMouseClicked(label, mouseButton, upInside)
    if mouseButton == MOUSE_BUTTON_INDEX_LEFT and upInside then
        local genderButton = label:GetParent():GetNamedChild("Button")
        KEYBOARD_CHARACTER_CREATE_MANAGER:ClickGenderButton(genderButton)
    end
end

function ZO_CharacterCreate_ChangeSlider(slider, changeAmount)
    OnCharacterCreateOptionChanged()
    slider:GetParent().sliderObject:ChangeValue(changeAmount)
end

function ZO_CharacterCreate_ChangePanel(direction)
    local currentTab = KEYBOARD_BUCKET_MANAGER:GetCurrentTab()
    if currentTab then
        KEYBOARD_BUCKET_MANAGER:SwitchBuckets(currentTab.windowData[direction])
    end
end

function ZO_CharacterCreate_PreviewClicked(previewButton)
    local slider = previewButton:GetParent()
    slider.sliderObject:Preview()
end

function ZO_PaperdollManipulation_OnEffectivelyShown(self)
    --While we need a mouse down over the paper doll area to start spinning, the mouse up may not be delivered to this same control. If we press mouse left to start spinning (which starts
    --mouse tracking) then press mouse right this will release mouse left but it won't stop mouse tracking because tracking is locked when the up is delivered. So we catch it on the event instead
    --when tracking isn't locked. ESO-546877
    self:RegisterForEvent(EVENT_GLOBAL_MOUSE_UP, CharacterCreateStopMouseSpin)
end

function ZO_PaperdollManipulation_OnEffectivelyHidden(self)
    CharacterCreateStopMouseSpin()
    self:UnregisterForEvent(EVENT_GLOBAL_MOUSE_UP)
end
