--[[ Crafting Create Screen Base ]]--
ZO_CraftingCreateScreenBase = ZO_InitializingObject:Subclass()

function ZO_CraftingCreateScreenBase:Create(numIterations)
    assert(false, "Override me")
end

-- To be used with the C APIs, eg. CreateTradeskillItem(tradeskillObject:GetAllCraftingParameters())
-- Lua limitation:
-- writing tradeskillObject:GetAllCraftingParameters(), numIterations would truncate the crafting
-- parameters to only the first return, so each implementation of
-- GetAllCraftingParameters should manually place that argument at the end themselves
function ZO_CraftingCreateScreenBase:GetAllCraftingParameters(numIterations)
    assert(false, "Override me")
    -- return implementation defined
end

function ZO_CraftingCreateScreenBase:GetResultItemLink()
    assert(false, "Override me")
    -- should return resultItemLink
end

function ZO_CraftingCreateScreenBase:GetMultiCraftNumResults(numIterations)
    assert(false, "Override me")
    -- should return numResults
end

function ZO_CraftingCreateScreenBase:IsCraftable()
    assert(false, "Override me")
    -- should return bool
end

function ZO_CraftingCreateScreenBase:GetMultiCraftMaxIterations()
    -- override me
    return 1
end

function ZO_CraftingCreateScreenBase:ShouldCraftButtonBeEnabled()
    assert(false, "Override me")
    -- should return bool, errorString
end

function ZO_CraftingCreateScreenBase:ShouldMultiCraftButtonBeEnabled()
    local enabled, errorString = self:ShouldCraftButtonBeEnabled()
    if not enabled then
        return enabled, errorString
    end

    return self:GetMultiCraftMaxIterations() > 1
end


--[[ Crafting Multi Slot Base ]]--
ZO_CraftingMultiSlotBase = ZO_InitializingCallbackObject:Subclass()

function ZO_CraftingMultiSlotBase:Initialize(owner, control, slotType, emptyTexture, multipleItemsTexture, craftingInventory, emptySlotIconTexture)
    self.owner = owner
    self.control = control
    self.slotType = slotType
    self.emptyTexture = emptyTexture
    self.multipleItemsTexture = multipleItemsTexture
    self.craftingInventory = craftingInventory

    if emptySlotIconTexture then
        self.emptyTexture = emptySlotIconTexture
        self.useEmptySlotIcon = true
        self.emptySlotOverrideIcon = self.control:GetNamedChild("EmptySlotIcon")
        internalassert(self.emptySlotOverrideIcon ~= nil)
    end

    -- required
    self.inventorySlotIcon = self.control:GetNamedChild("Icon")
    self.dropCallout = self.control:GetNamedChild("DropCallout")
    -- optional
    self.iconBg = self.control:GetNamedChild("IconBg")

    self.items = {}
    self:Refresh()
end

function ZO_CraftingMultiSlotBase:ShowDropCallout(isCorrectType)
    self.dropCallout:SetHidden(false)

    if isCorrectType then
        self.dropCallout:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
    else
        self.dropCallout:SetColor(ZO_ERROR_COLOR:UnpackRGBA())
    end
end

function ZO_CraftingMultiSlotBase:HideDropCallout()
    self.dropCallout:SetHidden(true)
end

-- Use to validate a list of virtually stacked items created from EnumerateInventorySlotsAndAddToScrollData
-- validItemFilterFn is optional
function ZO_CraftingMultiSlotBase:ValidateItemId(validItemIds, validItemFilterFn)
    local failed = false
    local itemsChanged = false

    for idx, item in ZO_NumericallyIndexedTableReverseIterator(self.items) do
        local bagId, slotIndex, itemInstanceId = item.bagId, item.slotIndex, item.itemInstanceId
        local itemInfo = validItemIds[itemInstanceId]
        if itemInfo and (validItemFilterFn == nil or validItemFilterFn(itemInfo.bag, itemInfo.index)) then
            itemsChanged = itemsChanged or item.bagId ~= itemInfo.bag or item.slotIndex ~= itemInfo.index
            item.bagId, item.slotIndex = itemInfo.bag, itemInfo.index
        else
            itemsChanged = true
            table.remove(self.items, idx)
            failed = true
        end
    end

    self:Refresh()
    if itemsChanged then
        self:FireCallbacks("ItemsChanged")
    end

    return not failed
end

do
    local function MatchesValidItemSlot(item, validItems)
        for i, validItem in ipairs(validItems) do
            if item.bagId == validItem.bagId and item.slotIndex == validItem.slotIndex then
                return true
            end
        end
        return false
    end

    -- Use to validate a list of slotDatas created from GetIndividualInventorySlotsAndAddToScrollData
    -- validItemFilterFn is optional
    function ZO_CraftingMultiSlotBase:ValidateSlottedItem(validItems, validItemFilterFn)
        local failed = false
        local needsValidation = {}
        for i = 1, #self.items do
            needsValidation[i] = true
        end

        for idx, item in ipairs(self.items) do
            if MatchesValidItemSlot(item, validItems) and (validItemFilterFn == nil or validItemFilterFn(item.bagId, item.slotIndex)) then
                needsValidation[idx] = nil
            end
        end

        if next(needsValidation) then
            -- There are items that were not validated, clear them out
            failed = true
            for i = #self.items, 1, -1 do
                if needsValidation[i] then
                    table.remove(self.items, i)
                end
            end
        end


        self:Refresh()
        if failed then
            self:FireCallbacks("ItemsChanged")
            return false
        else
            return true
        end
    end
end

function ZO_CraftingMultiSlotBase:ClearItemsInternal()
    ZO_ClearNumericallyIndexedTable(self.items)
end

function ZO_CraftingMultiSlotBase:ClearItems()
    if self:HasItems() then
        self:ClearItemsInternal()
        self:Refresh()
        self:FireCallbacks("ItemsChanged")
        return true
    end
    return false
end

-- Crafting slots work off of virtual inventories, so this item is used to represent all items with the instance id that is slotted here, not just this slot
function ZO_CraftingMultiSlotBase:AddItemInternal(bagId, slotIndex)
    if bagId and slotIndex and not self:ContainsBagAndSlot(bagId, slotIndex) then
        local item =
        {
            bagId = bagId,
            slotIndex = slotIndex,
            itemInstanceId = GetItemInstanceId(bagId, slotIndex),
        }
        table.insert(self.items, item)
        return true
    end
    return false
end

function ZO_CraftingMultiSlotBase:AddItem(bagId, slotIndex)
    local addedItem = self:AddItemInternal(bagId, slotIndex)
    self:Refresh()
    if addedItem then
        self:FireCallbacks("ItemSlotted", bagId, slotIndex)
        self:FireCallbacks("ItemsChanged")
    end
    return addedItem
end

function ZO_CraftingMultiSlotBase:RemoveItem(bagId, slotIndex)
    local removedItem = false
    for idx, item in ipairs(self.items) do
        if item.bagId == bagId and item.slotIndex == slotIndex then
            table.remove(self.items, idx)
            removedItem = true
            break
        end
    end
    self:Refresh()
    if removedItem then
        self:FireCallbacks("ItemsChanged")
    end
    return removedItem
end

function ZO_CraftingMultiSlotBase:Refresh()
    local numItems = #self.items

    local isEmpty = numItems == 0

    local icon
    local quantity
    if isEmpty then
        icon = self.emptyTexture
        quantity = 0
        ZO_Inventory_BindSlot(self.control, self.slotType, nil, nil)
    elseif numItems == 1 then
        local bagId, slotIndex = self:GetItemBagAndSlot(1)
        icon = GetItemInfo(bagId, slotIndex)
        quantity = self:GetStackCount()
        ZO_Inventory_BindSlot(self.control, self.slotType, slotIndex, bagId)
    else
        icon = self.multipleItemsTexture
        quantity = 0 -- hide quantity label
        ZO_Inventory_BindSlot(self.control, SLOT_TYPE_MULTIPLE_PENDING_CRAFTING_COMPONENTS, nil, nil)
    end

    local MEETS_REQUIREMENTS = nil
    local LOCKED = nil
    if self:HasAnimationRefs() then
        ZO_ItemSlot_SetupSlotBase(self.control, quantity, icon, MEETS_REQUIREMENTS, LOCKED, self:ShouldBeVisible())
    else
        ZO_ItemSlot_SetupSlot(self.control, quantity, icon, MEETS_REQUIREMENTS, LOCKED, self:ShouldBeVisible())
    end

    if self.iconBg then
        self.iconBg:SetHidden(not isEmpty)
    end

    if self.useEmptySlotIcon then
        if icon == "" then
            -- Empty string represents no icon, in this case we should just hide both inventory slot icon and our empty slot override
            self.emptySlotOverrideIcon:SetHidden(true)
            self.inventorySlotIcon:SetHidden(true)
        else
            self.emptySlotOverrideIcon:SetTexture(icon)
            self.emptySlotOverrideIcon:SetHidden(not isEmpty)
            self.inventorySlotIcon:SetHidden(isEmpty)
        end
    end

    self:UpdateTooltip()
end

function ZO_CraftingMultiSlotBase:GetStackCount()
    local quantity = 0
    if self.craftingInventory then
        for index = 1, self:GetNumItems() do
            -- non virtual items will have a stack count of 0, but we know that they represent exactly one in quantity
            quantity = quantity + zo_max(1, self.craftingInventory:GetStackCount(self:GetItemBagAndSlot(index)))
        end
    end
    return quantity
end

function ZO_CraftingMultiSlotBase:AddAnimationRef()
    self.animationRefs = (self.animationRefs or 0) + 1
end

function ZO_CraftingMultiSlotBase:RemoveAnimationRef()
    self.animationRefs = self.animationRefs - 1
    if self.animationRefs == 0 then
        self:Refresh()
    end
end

function ZO_CraftingMultiSlotBase:HasAnimationRefs()
    return self.animationRefs ~= nil and self.animationRefs > 0
end

function ZO_CraftingMultiSlotBase:HasItems()
    return #self.items > 0
end

function ZO_CraftingMultiSlotBase:HasOneItem()
    return #self.items == 1
end

function ZO_CraftingMultiSlotBase:HasMultipleItems()
    return #self.items > 1
end

function ZO_CraftingMultiSlotBase:GetNumItems()
    return #self.items
end

function ZO_CraftingMultiSlotBase:GetItemBagAndSlot(itemIndex)
    local item = itemIndex and self.items[itemIndex]
    if item then
        return item.bagId, item.slotIndex
    end
    return nil
end

function ZO_CraftingMultiSlotBase:GetItemInstanceId(itemIndex)
    local item = itemIndex and self.items[itemIndex]
    if item then
        return item.itemInstanceId
    end
    return nil
end

function ZO_CraftingMultiSlotBase:ContainsItemId(itemId)
    for _, item in ipairs(self.items) do
        if item.itemInstanceId == itemId then
            return true
        end
    end
    return false
end

function ZO_CraftingMultiSlotBase:ContainsBagAndSlot(bagId, slotIndex)
    for _, item in ipairs(self.items) do
        if item.bagId == bagId and item.slotIndex == slotIndex then
            return true
        end
    end
    return false
end

function ZO_CraftingMultiSlotBase:IsSlotControl(slotControl)
    return self.control == slotControl
end

function ZO_CraftingMultiSlotBase:GetControl()
    return self.control
end

function ZO_CraftingMultiSlotBase:UpdateTooltip()
    if self.control == WINDOW_MANAGER:GetMouseOverControl() then
        ZO_InventorySlot_OnMouseEnter(self.control)
    end
end

function ZO_CraftingMultiSlotBase:SetEmptyTexture(emptyTexture)
    self.emptyTexture = emptyTexture
    self:Refresh()
end

function ZO_CraftingMultiSlotBase:SetMultipleItemsTexture(multipleItemsTexture)
    self.multipleItemsTexture = multipleItemsTexture
    self:Refresh()
end

function ZO_CraftingMultiSlotBase:ShouldBeVisible()
    -- To be overridden if managing the visibility of the slot
    return true
end

--[[ Crafting Slot Base ]]--
-- This is a multislot that only permits single items. The multislot API can
-- be used, but the old single-slot api has been implemented on top.
-- Attempting to add multiple items using AddItem() will instead replace the
-- current item.
ZO_CraftingSlotBase = ZO_CraftingMultiSlotBase:Subclass()

function ZO_CraftingSlotBase:Initialize(owner, control, slotType, emptyTexture, craftingInventory, emptySlotIcon)
    return ZO_CraftingMultiSlotBase.Initialize(self, owner, control, slotType, emptyTexture, "", craftingInventory, emptySlotIcon)
end

function ZO_CraftingSlotBase:SetItem(bagId, slotIndex)
    -- can be overriden for custom functionality, but should still call base or SetupItem
    self:SetupItem(bagId, slotIndex)
end

function ZO_CraftingSlotBase:SetupItem(bagId, slotIndex)
    local oldBagId, oldSlotIndex = self:GetBagAndSlot()
    self:ClearItemsInternal()
    self:AddItemInternal(bagId, slotIndex)
    self:Refresh()

    if oldBagId ~= bagId or oldSlotIndex ~= slotIndex then
        self:FireCallbacks("ItemsChanged")
    end
end

function ZO_CraftingSlotBase:AddItem(bagId, slotIndex)
    -- instead of adding, replace existing items
    self:SetItem(bagId, slotIndex)
end

function ZO_CraftingSlotBase:GetBagAndSlot()
    return self:GetItemBagAndSlot(1)
end

function ZO_CraftingSlotBase:IsBagAndSlot(bagId, slotIndex)
    return self:ContainsBagAndSlot(bagId, slotIndex)
end

function ZO_CraftingSlotBase:HasItem()
    return self:HasItems()
end

function ZO_CraftingSlotBase:IsItemId(itemId)
    return self:ContainsItemId(itemId)
end

function ZO_CraftingSlotBase:GetItemId()
    return self:GetItemInstanceId(1)
end

function ZO_CraftingSlot_OnInitialized(self)
	-- Use the double click behavior as the single click behavior for consistency with all
	-- other aspects of the Crafting UI.
	self:SetHandler("OnClicked", self:GetHandler("OnMouseDoubleClick"))
	self:SetHandler("OnMouseDoubleClick", nil)

    self.animation = ANIMATION_MANAGER:CreateTimelineFromVirtual("CraftingGlowAlphaAnimation", self:GetNamedChild("Glow"))
    local icon = self:GetNamedChild("Icon")
    icon:ClearAnchors()
    icon:SetAnchor(CENTER, self, CENTER)
    icon:SetDimensions(self:GetDimensions())
end

--[[ Crafting Slot Animation Base ]]--
ZO_CraftingSlotAnimationBase = ZO_Object:Subclass()

function ZO_CraftingSlotAnimationBase:New(...)
    local craftingSlotAnimationBase = ZO_Object.New(self)
    craftingSlotAnimationBase:Initialize(...)
    return craftingSlotAnimationBase
end

function ZO_CraftingSlotAnimationBase:Initialize(sceneName, visibilityPredicate)
    self.slots = {}

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", function()
        if SCENE_MANAGER:IsShowing(sceneName) and (not visibilityPredicate or visibilityPredicate()) then
            self:Play(sceneName)
        end
    end)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", function() self:Stop() end)
end

function ZO_CraftingSlotAnimationBase:AddSlot(slot)
    self.slots[#self.slots + 1] = slot
end

function ZO_CraftingSlotAnimationBase:Clear()
    if #self.slots > 0 then
        self.slots = {}
    end
end

function ZO_CraftingSlotAnimationBase:Play(sceneName)
    -- intended to be overridden
end

function ZO_CraftingSlotAnimationBase:Stop(sceneName)
    -- intended to be overridden
end

--[[ Global Utils ]]--
function ZO_CraftingUtils_GetCostToCraftString(cost)
    if cost > 0 then
        if GetCurrencyAmount(CURT_MONEY, CURRENCY_LOCATION_CHARACTER) >= cost then
            return zo_strformat(SI_CRAFTING_PERFORM_CRAFT, ZO_Currency_FormatKeyboard(CURT_MONEY, cost, ZO_CURRENCY_FORMAT_AMOUNT_ICON))
        end
        return zo_strformat(SI_CRAFTING_PERFORM_CRAFT, ZO_Currency_FormatKeyboard(CURT_MONEY, cost, ZO_CURRENCY_FORMAT_ERROR_AMOUNT_ICON))
    end

    return GetString(SI_CRAFTING_PERFORM_FREE_CRAFT)
end

function ZO_CraftingUtils_ConnectMenuBarToCraftingProcess(menuBar)
    local function OnCraftStarted()
        if not menuBar:IsHidden() then
            ZO_MenuBar_SetAllButtonsEnabled(menuBar, false)
        end
    end

    local function OnCraftCompleted()
        ZO_MenuBar_SetAllButtonsEnabled(menuBar, true)
        ZO_MenuBar_UpdateButtons(menuBar, true)
    end

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", OnCraftStarted)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", OnCraftCompleted)
end

function ZO_CraftingUtils_ConnectKeybindButtonGroupToCraftingProcess(keybindStripDescriptor)
    local function UpdateKeyBindDescriptorGroup()
        KEYBIND_STRIP:UpdateKeybindButtonGroup(keybindStripDescriptor)
    end

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", UpdateKeyBindDescriptorGroup)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", UpdateKeyBindDescriptorGroup)
end

function ZO_CraftingUtils_ConnectEditBoxToCraftingProcess(editBox)
    local function OnCraftStarted()
        if not editBox:IsHidden() then
            editBox:SetEditEnabled(false)
        end
    end

    local function OnCraftCompleted()
        editBox:SetEditEnabled(true)
    end

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", OnCraftStarted)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", OnCraftCompleted)
end

local function ConnectStandardObjectToCraftingProcess(object)
    local function OnCraftStarted()
        if not object:GetControl():IsHidden() then
            object:SetEnabled(false)
        end
    end

    local function OnCraftCompleted()
        object:SetEnabled(true)
    end

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", OnCraftStarted)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", OnCraftCompleted)
end

function ZO_CraftingUtils_ConnectHorizontalScrollListToCraftingProcess(horizontalScrollList)
    ConnectStandardObjectToCraftingProcess(horizontalScrollList)
end

function ZO_CraftingUtils_ConnectComboBoxToCraftingProcess(comboBox)
    ConnectStandardObjectToCraftingProcess(comboBox)
end

function ZO_CraftingUtils_ConnectCheckBoxToCraftingProcess(checkBox)
    local function OnCraftStarted()
        if not checkBox:IsHidden() then
            ZO_CheckButton_SetEnableState(checkBox, false)
        end
    end

    local function OnCraftCompleted()
        ZO_CheckButton_SetEnableState(checkBox, true)
    end

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", OnCraftStarted)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", OnCraftCompleted)
end

function ZO_CraftingUtils_ConnectSpinnerToCraftingProcess(spinner)
    ConnectStandardObjectToCraftingProcess(spinner)
end

function ZO_CraftingUtils_ConnectTreeToCraftingProcess(tree)
    ConnectStandardObjectToCraftingProcess(tree)
end

function ZO_CraftingUtils_ConnectButtonToCraftingProcess(button, overrideSetEnabled)
    local function OnCraftStarted()
        if not button:IsHidden() then
            if overrideSetEnabled then
                overrideSetEnabled(false)
            else
                button:SetEnabled(false)
            end
        end
    end

    local function OnCraftCompleted()
        if overrideSetEnabled then
            overrideSetEnabled(true)
        else
            button:SetEnabled(true)
        end
    end

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", OnCraftStarted)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", OnCraftCompleted)
end

function ZO_CraftingUtils_GetSmithingTraitItemInfo()
    local traits = {}
    for traitType = ITEM_TRAIT_TYPE_ITERATION_BEGIN, ITEM_TRAIT_TYPE_ITERATION_END do
        local traitIndex = traitType + 1
        local _, name, icon, sellPrice, meetsUsageRequirement, itemStyle, quality = GetSmithingTraitItemInfo(traitIndex)
        local hasTrait = traitType ~= ITEM_TRAIT_TYPE_NONE
        if name ~= "" or not hasTrait then
            table.insert(traits, {
                type = traitType,
                index = traitIndex,
                name = name,
                icon = hasTrait and icon or "EsoUI/Art/Crafting/crafting_smithing_noTrait.dds",
                sellPrice = sellPrice,
                meetsUsageRequirement = meetsUsageRequirement,
                itemStyle = itemStyle,
                quality = quality,
            })
        end
    end
    return traits
end

do
    internalassert(SMITHING_FILTER_TYPE_MAX_VALUE == 7, "Update for new smithing filters")

    local ITEM_FILTER_TO_SMITHING_FILTER =
    {
       [ITEMFILTERTYPE_WEAPONS] = SMITHING_FILTER_TYPE_WEAPONS,
       [ITEMFILTERTYPE_ARMOR] = SMITHING_FILTER_TYPE_ARMOR,
       [ITEMFILTERTYPE_JEWELRY] = SMITHING_FILTER_TYPE_JEWELRY,
    }

    function ZO_CraftingUtils_GetSmithingFilterFromItem(bagId, slotIndex)
        local itemFilters = {GetItemFilterTypeInfo(bagId, slotIndex)}
        for _, itemFilter in ipairs(itemFilters) do
            local smithingFilter = ITEM_FILTER_TO_SMITHING_FILTER[itemFilter] 
            if smithingFilter then
                return smithingFilter
            end
        end
        return SMITHING_FILTER_TYPE_RAW_MATERIALS
    end

    function ZO_CraftingUtils_GetSmithingFilterFromItemFilter(itemFilter)
        return ITEM_FILTER_TO_SMITHING_FILTER[itemFilter] 
    end

    local SMITHING_FILTER_TO_ITEM_FILTER =
    {
        [SMITHING_FILTER_TYPE_WEAPONS] = ITEMFILTERTYPE_WEAPONS,
        [SMITHING_FILTER_TYPE_SET_WEAPONS] = ITEMFILTERTYPE_WEAPONS,
        [SMITHING_FILTER_TYPE_ARMOR] = ITEMFILTERTYPE_ARMOR,
        [SMITHING_FILTER_TYPE_SET_ARMOR] = ITEMFILTERTYPE_ARMOR,
        [SMITHING_FILTER_TYPE_JEWELRY] = ITEMFILTERTYPE_JEWELRY,
        [SMITHING_FILTER_TYPE_SET_JEWELRY] = ITEMFILTERTYPE_JEWELRY,
    }

    function ZO_CraftingUtils_GetItemFilterFromSmithingFilter(smithingFilter)
        return SMITHING_FILTER_TO_ITEM_FILTER[smithingFilter]
    end

    local TRAIT_CATEGORY_TO_SMITHING_FILTER =
    {
       [ITEM_TRAIT_TYPE_CATEGORY_WEAPON] = SMITHING_FILTER_TYPE_WEAPONS,
       [ITEM_TRAIT_TYPE_CATEGORY_ARMOR] = SMITHING_FILTER_TYPE_ARMOR,
       [ITEM_TRAIT_TYPE_CATEGORY_JEWELRY] = SMITHING_FILTER_TYPE_JEWELRY,
    }

    function ZO_CraftingUtils_GetSmithingFilterFromTrait(traitType)
        return TRAIT_CATEGORY_TO_SMITHING_FILTER[GetItemTraitTypeCategory(traitType)]
    end

    local SMITHING_FILTER_TO_BASE_FILTER =
    {
        [SMITHING_FILTER_TYPE_WEAPONS] = SMITHING_FILTER_TYPE_WEAPONS,
        [SMITHING_FILTER_TYPE_SET_WEAPONS] = SMITHING_FILTER_TYPE_WEAPONS,
        [SMITHING_FILTER_TYPE_ARMOR] = SMITHING_FILTER_TYPE_ARMOR,
        [SMITHING_FILTER_TYPE_SET_ARMOR] = SMITHING_FILTER_TYPE_ARMOR,
        [SMITHING_FILTER_TYPE_JEWELRY] = SMITHING_FILTER_TYPE_JEWELRY,
        [SMITHING_FILTER_TYPE_SET_JEWELRY] = SMITHING_FILTER_TYPE_JEWELRY,
    }

    function ZO_CraftingUtils_GetBaseSmithingFilter(smithingFilter)
        return SMITHING_FILTER_TO_BASE_FILTER[smithingFilter]
    end

    function ZO_CraftingUtils_IsBaseSmithingFilter(smithingFilter)
        if SMITHING_FILTER_TO_BASE_FILTER[smithingFilter] == nil then
            return true
        end
        return SMITHING_FILTER_TO_BASE_FILTER[smithingFilter] == smithingFilter
    end

    function ZO_CraftingUtils_CanSmithingFilterBeCraftedHere(smithingFilter)
        local baseFilter = ZO_CraftingUtils_GetBaseSmithingFilter(smithingFilter)
        if smithingFilter ~= baseFilter and not CanSmithingSetPatternsBeCraftedHere() then
            return false
        end
        if baseFilter == SMITHING_FILTER_TYPE_WEAPONS then
            return CanSmithingWeaponPatternsBeCraftedHere()
        elseif baseFilter == SMITHING_FILTER_TYPE_ARMOR then
            return CanSmithingApparelPatternsBeCraftedHere()
        elseif baseFilter == SMITHING_FILTER_TYPE_JEWELRY then
            return CanSmithingJewelryPatternsBeCraftedHere()
        end
    end
end

do
    local g_isCrafting = false

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStarted", function()
        g_isCrafting = true
    end)
    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", function()
        g_isCrafting = false
    end)

    function ZO_CraftingUtils_IsPerformingCraftProcess()
        return g_isCrafting or IsAwaitingCraftingProcessResponse()
    end
end

function ZO_CraftingUtils_IsCraftingWindowOpen()
    return ZO_Smithing_IsSceneShowing()
            or SYSTEMS:IsShowing(ZO_ALCHEMY_SYSTEM_NAME)
            or ZO_Enchanting_IsSceneShowing()
            or ZO_Provisioner_IsSceneShowing()
end

-- Crafting screens virtualize each item into stacks, but only refining items
-- can properly support deconstruction by virtual stack. In all other cases, eg.
-- enchanting, we need to send a message with each slot represented by the
-- virtual stack, which is what this function prepares for us.
function ZO_CraftingUtils_AddVirtualStackToDeconstructMessageAsRealStacks(virtualBagId, virtualSlotIndex, quantityToAdd)
    local instanceId = GetItemInstanceId(virtualBagId, virtualSlotIndex)
    local ALL_MATCHING_ITEMS = nil
    -- When there are more slots than items to add, we want to prioritize
    -- backpack items over bank items. Since the loop that adds these items pops
    -- from the end, we should generate our slots list so that the end of the list holds all the backpack items
    local slots = PLAYER_INVENTORY:GenerateAllSlotsInVirtualStackedItem(ALL_MATCHING_ITEMS, instanceId, INVENTORY_BANK, INVENTORY_BACKPACK)
    while quantityToAdd > 0 and slots[1] do
        local slot = table.remove(slots)
        local stackCount = zo_min(slot.stackCount, quantityToAdd)
        if not AddItemToDeconstructMessage(slot.bagId, slot.slotIndex, stackCount) then
            return false
        end
        quantityToAdd = quantityToAdd - stackCount
    end
    return quantityToAdd == 0 -- did we add as many items as we requested?
end

ZO_CRAFTING_TOOLTIP_STYLES = ZO_DeepTableCopy(ZO_TOOLTIP_STYLES)
for key,value in pairs(ZO_CRAFTING_TOOLTIP_STYLES) do
    value["horizontalAlignment"] = TEXT_ALIGN_CENTER

    if key ~= "topSection" then
        value["layoutPrimaryDirectionCentered"] = true
    end
end

do
    local CRAFTING_TYPE_TO_BOOSTER_ITEM_TYPE =
    {
        [CRAFTING_TYPE_BLACKSMITHING] = ITEMTYPE_BLACKSMITHING_BOOSTER,
        [CRAFTING_TYPE_CLOTHIER] = ITEMTYPE_CLOTHIER_BOOSTER,
        [CRAFTING_TYPE_WOODWORKING] = ITEMTYPE_WOODWORKING_BOOSTER,
        [CRAFTING_TYPE_JEWELRYCRAFTING] = ITEMTYPE_JEWELRYCRAFTING_BOOSTER,
    }

    function ZO_CraftingUtils_GetBoosterItemType(craftingType)
        return CRAFTING_TYPE_TO_BOOSTER_ITEM_TYPE[craftingType]
    end
end