ZO_CRAFTING_RESULT_TYPE =
{
    ITEM = 1,
    CURRENCY = 2,
    KNOWLEDGE = 3,
}

local CONTEXTUAL_ANIMATION_NAME_BY_CONTEXT =
{
    [CRAFTING_PROCESS_CONTEXT_CONSUME_ATTUNABLE_STATIONS] = "CraftingResultsConsumeStation_Animation",
}

local g_forceCenterResultsText = false

function ZO_CraftingResults_Base_PlayPulse(control)
    if not control.pulseAnimation then
        local pulseTexture = CreateControlFromVirtual("$(parent)PulseTexture", control, "ZO_CraftingResultPulseTexture")
        control.pulseAnimation = ANIMATION_MANAGER:CreateTimelineFromVirtual("CraftingResultPulse", pulseTexture)
    end

    if not control.pulseAnimation:IsPlaying() then
        control.pulseAnimation:PlayFromStart()
    end
end

ZO_CraftingResults_Base = ZO_InitializingObject:Subclass()

function ZO_CraftingResults_Base:Initialize(control, showInGamepadPreferredModeOnly)
    self.control = control

    self:InitializeResultBuffer()

    control:RegisterForEvent(EVENT_CRAFT_STARTED, function(eventCode, ...) self:OnCraftStarted(...) end)
    control:AddFilterForEvent(EVENT_CRAFT_STARTED, REGISTER_FILTER_IS_IN_GAMEPAD_PREFERRED_MODE, showInGamepadPreferredModeOnly)

    control:RegisterForEvent(EVENT_CRAFT_COMPLETED, function(eventCode, ...) self:OnCraftCompleted(...) end)
    control:AddFilterForEvent(EVENT_CRAFT_COMPLETED, REGISTER_FILTER_IS_IN_GAMEPAD_PREFERRED_MODE, showInGamepadPreferredModeOnly)

    control:RegisterForEvent(EVENT_RETRAIT_STARTED, function(eventCode, ...) self:OnRetraitStarted(...) end)
    control:AddFilterForEvent(EVENT_RETRAIT_STARTED, REGISTER_FILTER_IS_IN_GAMEPAD_PREFERRED_MODE, showInGamepadPreferredModeOnly)

    control:RegisterForEvent(EVENT_RETRAIT_RESPONSE, function(eventCode, ...) self:OnRetraitCompleted(...) end)
    control:AddFilterForEvent(EVENT_RETRAIT_RESPONSE, REGISTER_FILTER_IS_IN_GAMEPAD_PREFERRED_MODE, showInGamepadPreferredModeOnly)

    control:RegisterForEvent(EVENT_RECONSTRUCT_STARTED, function(eventCode, ...) self:OnReconstructStarted(...) end)
    control:AddFilterForEvent(EVENT_RECONSTRUCT_STARTED, REGISTER_FILTER_IS_IN_GAMEPAD_PREFERRED_MODE, showInGamepadPreferredModeOnly)

    control:RegisterForEvent(EVENT_RECONSTRUCT_RESPONSE, function(eventCode, ...) self:OnReconstructCompleted(...) end)
    control:AddFilterForEvent(EVENT_RECONSTRUCT_RESPONSE, REGISTER_FILTER_IS_IN_GAMEPAD_PREFERRED_MODE, showInGamepadPreferredModeOnly)

    self.enchantSoundPlayer = ZO_QueuedSoundPlayer:New()
    self.enchantSoundPlayer:SetFinishedAllSoundsCallback(function() self:OnAllEnchantSoundsFinished() end)

    -- Create a pool of CraftingResultTooltipAnimation_Base to be applied to controls that are
    -- not the "primary" crafting tooltip (self.tooltipControl), but are being displayed alongside of it
    -- This allows us to have additional controls animate in-sync with the crafting tooltip
    self.secondaryTooltipAnimationPool = ZO_AnimationPool:New("CraftingResultTooltipAnimation_Base")

    self.contextualAnimationControls = {}
    self.contextualAnimations = {}
end

function ZO_CraftingResults_Base:SetCraftingTooltip(tooltipControl)
    if self.tooltipControl ~= tooltipControl then
        self.tooltipAnimationSuccessSound = nil
        self.tooltipAnimationFailureSound = nil
        self.tooltipControl = tooltipControl
        self:ForceStop()
        if self.tooltipControl then
            self:AssociateAnimations(self.tooltipControl)
        end
    end

    if not tooltipControl then
        self:FadeAll()
        self:ClearSecondaryTooltipAnimationControls()
    end
end

function ZO_CraftingResults_Base:SetContextualAnimationControl(context, control)
    if CONTEXTUAL_ANIMATION_NAME_BY_CONTEXT[context] ~= nil then
        if self.contextualAnimationControls[context] ~= control then
            self.contextualAnimationControls[context] = control
            self:ForceStop()
            if self.contextualAnimationControls[context] then
                self:AssociateContextualAnimations(context, self.contextualAnimationControls[context])
            end
        end
    end
end

function ZO_CraftingResults_Base:SetTooltipAnimationSounds(tooltipAnimationSuccessSound, tooltipAnimationFailureSound)
    self.tooltipAnimationSuccessSound = tooltipAnimationSuccessSound
    self.tooltipAnimationFailureSound = tooltipAnimationFailureSound
end

function ZO_CraftingResults_Base:PlayCraftedSound(itemSoundCategory)
    if not ZO_Enchanting_IsInCreationMode() then
        PlayItemSound(itemSoundCategory, ITEM_SOUND_ACTION_CRAFTED)
    end
end

function ZO_CraftingResults_Base:AssociateAnimations(tooltip)
    if not self.resultTooltipAnimation then
        self.resultTooltipAnimation = ANIMATION_MANAGER:CreateTimelineFromVirtual("CraftingResultTooltipAnimation")
    end

    self.resultTooltipAnimation:GetAnimation(1):SetAnimatedControl(tooltip)
    self.resultTooltipAnimation:GetAnimation(2):SetAnimatedControl(tooltip)

    local tooltipGlow = tooltip:GetNamedChild("Glow")
    self.tooltipGlow = tooltipGlow
    self.resultTooltipAnimation:GetAnimation(3):SetAnimatedControl(tooltipGlow)

    local tooltipBurst1 = tooltip:GetNamedChild("IconBurst1")
    self.tooltipBurst1 = tooltipBurst1
    self.resultTooltipAnimation:GetAnimation(4):SetAnimatedControl(tooltipBurst1)

    local tooltipBurst2 = tooltip:GetNamedChild("IconBurst2")
    self.tooltipBurst2 = tooltipBurst2
    self.resultTooltipAnimation:GetAnimation(5):SetAnimatedControl(tooltipBurst2)

    self.resultTooltipAnimation:GetAnimationTimeline(1):GetAnimation(1):SetAnimatedControl(tooltipBurst1)
    self.resultTooltipAnimation:GetAnimationTimeline(1):GetAnimation(2):SetAnimatedControl(tooltipBurst2)

    local function OnStop(animation)
        tooltipGlow:SetAlpha(0)
        if tooltipBurst1 then
            tooltipBurst1:SetAlpha(0)
        end
        if tooltipBurst2 then
            tooltipBurst2:SetAlpha(0)
        end

        self:OnTooltipAnimationStopped(self.resultTooltipAnimation.craftingType)
    end

    self.forceStopHandler = function()
        tooltip:SetAlpha(1)
        OnStop()
    end

    self.resultTooltipAnimation:GetAnimation(1):SetHandler("OnStop", OnStop)
end

function ZO_CraftingResults_Base:AssociateContextualAnimations(context, control)
    --Check to see if we have created an animation for this context yet
    if not self.contextualAnimations[context] then
        local animationName = CONTEXTUAL_ANIMATION_NAME_BY_CONTEXT[context]
        if animationName then
            self.contextualAnimations[context] = ANIMATION_MANAGER:CreateTimelineFromVirtual(animationName)
        else
            internalassert(false, "Context does not have a contextual animation")
            return
        end
    end

    self.contextualAnimations[context]:GetAnimation(1):SetAnimatedControl(control)
    local function OnStop(animation)
        self:OnContextualAnimationStopped(self.contextualAnimations[context].craftingType)
    end

    self.contextualAnimations[context]:GetAnimation(1):SetHandler("OnStop", OnStop)
    self.contextualAnimations[context].forceStopHandler = function()
        OnStop()
    end
end

function ZO_CraftingResults_Base:ForceStop()
    if self.resultTooltipAnimation and self.resultTooltipAnimation:IsPlaying() then
        self.resultTooltipAnimation:Stop()
        self.forceStopHandler()
    end

    for _, animation in pairs(self.contextualAnimations) do
        if animation and animation:IsPlaying() then
            animation:Stop()
            animation.forceStopHandler()
        end
    end
end

-- Add a control that we want to have animate alongside the main crafting tooltip (self.tooltipControl)
-- These will not play the full set of animations, but will have the same alpha animations
function ZO_CraftingResults_Base:AddSecondaryTooltipAnimationControl(control)
    if control then
        local animation, key = self.secondaryTooltipAnimationPool:AcquireObject()
        animation:GetAnimation(1):SetAnimatedControl(control)
        animation:GetAnimation(2):SetAnimatedControl(control)
    end
end

function ZO_CraftingResults_Base:ClearSecondaryTooltipAnimationControls()
    self.secondaryTooltipAnimationPool:ReleaseAllObjects()
end

local function DoesCraftingTypeHaveSlotAnimations(craftingType)
    if craftingType == CRAFTING_TYPE_PROVISIONING then
        return false
    elseif craftingType == CRAFTING_TYPE_ENCHANTING then
        return SYSTEMS:GetObject(ZO_ENCHANTING_SYSTEM_NAME):DoesCurrentModeHaveSlotAnimations()
    elseif craftingType == CRAFTING_TYPE_ALCHEMY then
        return SYSTEMS:GetObject(ZO_ALCHEMY_SYSTEM_NAME):DoesCurrentModeHaveSlotAnimations()
    elseif ZO_Smithing_IsSmithingStation(craftingType) then
        local activeSmithing = ZO_Smithing_GetActiveObject()
        if activeSmithing then
            return activeSmithing:DoesCurrentModeHaveSlotAnimations()
        else
            return true
        end
    end
end

function ZO_CraftingResults_Base:StartCraftProcess(playStopTooltipAnimation, context)
    self.playContextualAnimation = CONTEXTUAL_ANIMATION_NAME_BY_CONTEXT[context] ~= nil

    --The contextual and tooltip animations are mutually exclusive
    if self.playContextualAnimation then
        self.tooltipAnimationCompleted = true
        self.contextualAnimationCompleted = false
    else
        self.tooltipAnimationCompleted = false
        self.contextualAnimationCompleted = true
    end

    self.craftingProcessCompleted = false

    --The result of DoesCraftingTypeHaveSlotAnimations for smithing depends on having an active smithing object. There is no such object if the smithing UI
    --is closed. This means that the result of the function can change between OnCraftStarted and OnCraftEnded if the smithing UI is closed during a crafting
    --operation. So store the results off here and don't call it later on.
    self.playStopTooltipAnimation = playStopTooltipAnimation and not self.playContextualAnimation
    self.playStartTooltipAnimation = not playStopTooltipAnimation and not self.playContextualAnimation

    -- Suppress item count-related messaging for Smithing Tradeskills' Improvement and Research actions.
    if ZO_Smithing_IsSceneShowing() then
        local mode = ZO_Smithing_GetActiveObject():GetMode()
        self.craftingProcessProducesItems = not (mode == SMITHING_MODE_IMPROVEMENT or mode == SMITHING_MODE_RESEARCH or mode == SMITHING_MODE_CONSOLIDATED_SET_SELECTION or mode == SMITHING_MODE_ROOT)
    elseif SCENE_MANAGER:GetCurrentSceneName() == "gamepad_smithing_research_confirm" then -- Separate case due to ancillary confirmation Scene
        self.craftingProcessProducesItems = false
    else
        self.craftingProcessProducesItems = true
    end

    self.craftingProcessProducesItems = self.craftingProcessProducesItems and context ~= CRAFTING_PROCESS_CONTEXT_CONSUME_ATTUNABLE_STATIONS

    if ZO_Enchanting_IsInCreationMode() then
        local potencySound, potencyLength, essenceSound, essenceLength, aspectSound, aspectLength = ZO_Enchanting_GetVisibleEnchanting():GetLastRunestoneSoundParams()

        self.enchantSoundPlayer:PlaySound(potencySound, potencyLength)
        self.enchantSoundPlayer:PlaySound(essenceSound, essenceLength)
        self.enchantSoundPlayer:PlaySound(aspectSound, aspectLength)
    end

    CALLBACK_MANAGER:FireCallbacks("CraftingAnimationsStarted")

    if self.playStartTooltipAnimation then
        self:PlayTooltipAnimation()
    elseif self.playContextualAnimation then
        self:PlayContextualAnimation(context)
    end
end

function ZO_CraftingResults_Base:OnCraftStarted(craftingType, context)
    local playStopTooltipAnimation = DoesCraftingTypeHaveSlotAnimations(craftingType)
    self:StartCraftProcess(playStopTooltipAnimation, context)
end

function ZO_CraftingResults_Base:OnRetraitStarted()
    local PLAY_STOP_TOOLTIP_ANIMATION = true
    self:StartCraftProcess(PLAY_STOP_TOOLTIP_ANIMATION)
end

function ZO_CraftingResults_Base:OnReconstructStarted()
    local PLAY_STOP_TOOLTIP_ANIMATION = true
    self:StartCraftProcess(PLAY_STOP_TOOLTIP_ANIMATION)
end

function ZO_CraftingResults_Base:PlayTooltipAnimation(isFailure, isExceptionalResult, craftingType)
    if self.tooltipControl and not self.tooltipControl:IsHidden() then
        self:ForceStop()

        local edgeTexture = "EsoUI/Art/Crafting/crafting_toolTip_glow_edge_blue64.dds"
        local burstTexture = "EsoUI/Art/Crafting/burst_blue.dds"
        if isFailure then
            edgeTexture = "EsoUI/Art/Crafting/crafting_toolTip_glow_edge_red64.dds"
            -- no burst texture for failue since we hide the bursts
        elseif isExceptionalResult then
            edgeTexture = "EsoUI/Art/Crafting/crafting_toolTip_glow_edge_gold64.dds"
            burstTexture = "EsoUI/Art/Crafting/burst_gold.dds"
        end

        self.tooltipGlow:SetEdgeTexture(edgeTexture, 512, 64)
        if isFailure then
            self.resultTooltipAnimation:GetAnimation(3):SetDuration(200)
            self.resultTooltipAnimation:GetAnimation(3):SetAlphaValues(.25, 1)
        else
            self.resultTooltipAnimation:GetAnimation(3):SetDuration(500)
            self.resultTooltipAnimation:GetAnimation(3):SetAlphaValues(0, 1)
            if self.tooltipBurst1 then
                self.tooltipBurst1:SetTexture(burstTexture)
            end
            if self.tooltipBurst2 then
                self.tooltipBurst2:SetTexture(burstTexture)
            end
        end

        if self.tooltipBurst1 then
            self.tooltipBurst1:SetHidden(isFailure)
        end
        if self.tooltipBurst2 then
            self.tooltipBurst2:SetHidden(isFailure)
        end

        self.resultTooltipAnimation.craftingType = craftingType
        self.resultTooltipAnimation:PlayFromStart()

        local secondaryTooltipAnimations = self.secondaryTooltipAnimationPool:GetActiveObjects()
        for _, animation in pairs(secondaryTooltipAnimations) do
            animation:PlayFromStart()
        end
    else
        self:OnTooltipAnimationStopped(craftingType)
    end
    PlaySound(isFailure and self.tooltipAnimationFailureSound or self.tooltipAnimationSuccessSound)
end

function ZO_CraftingResults_Base:PlayContextualAnimation(context, craftingType)
    if self.contextualAnimationControls[context] and not self.contextualAnimationControls[context]:IsHidden() then
        self:ForceStop()
        
        self.contextualAnimations[context].craftingType = craftingType
        self.contextualAnimations[context]:PlayFromStart()
    else
        self:OnContextualAnimationStopped(craftingType)
    end
end

function ZO_CraftingResults_Base:CompleteCraftProcess(craftFailed, isExceptionalResult, craftingType)
    if not self.craftingProcessCompleted then
        if self.enchantSoundPlayer:IsPlaying() then
            self.processCompletedArguments = {craftFailed = craftFailed, isExceptionalResult = isExceptionalResult, craftingType = craftingType}
        else
            self.craftingProcessCompleted = true

            if self.playStopTooltipAnimation then
                self:PlayTooltipAnimation(craftFailed, isExceptionalResult, craftingType)
            else
                self:CheckCraftProcessCompleted(craftingType)
            end
        end
    end
end

function ZO_CraftingResults_Base:OnCraftCompleted(craftingType)
    local numItemsGained = GetNumLastCraftingResultItemsAndPenalty()
    local craftFailed = numItemsGained == 0
    local NOT_EXCEPTIONAL_RESULT = false
    self:CompleteCraftProcess(craftFailed, NOT_EXCEPTIONAL_RESULT, craftingType)
end

function ZO_CraftingResults_Base:OnRetraitCompleted(result)
    local craftFailed = result ~= RETRAIT_RESPONSE_SUCCESS
    local EXCEPTIONAL_RESULT = true
    self:CompleteCraftProcess(craftFailed, EXCEPTIONAL_RESULT)
end

function ZO_CraftingResults_Base:OnReconstructCompleted(result)
    local craftFailed = result ~= RECONSTRUCT_RESPONSE_SUCCESS
    local EXCEPTIONAL_RESULT = true
    self:CompleteCraftProcess(craftFailed, EXCEPTIONAL_RESULT)
end

function ZO_CraftingResults_Base:OnAllEnchantSoundsFinished()
    if not self.craftingProcessCompleted then
        if self.processCompletedArguments then
            local arguments = self.processCompletedArguments
            self.processCompletedArguments = nil
            self:CompleteCraftProcess(arguments.craftFailed, arguments.isExceptionalResult, arguments.craftingType)
        else
            self.craftingProcessCompleted = true
            self:PlayTooltipAnimation()
        end
    end
end

function ZO_CraftingResults_Base:OnTooltipAnimationStopped(craftingType)
    if self.tooltipAnimationCompleted == false then
        self.tooltipAnimationCompleted = true
        self:CheckCraftProcessCompleted(craftingType)
    end
end

function ZO_CraftingResults_Base:OnContextualAnimationStopped(craftingType)
    if self.contextualAnimationCompleted == false then
        self.contextualAnimationCompleted = true
        self:CheckCraftProcessCompleted(craftingType)
    end
end

local function GetBoosterFoundSoundForCraftingType()
    local craftingType = GetCraftingInteractionType()
    if craftingType == CRAFTING_TYPE_BLACKSMITHING then
        return SOUNDS.BLACKSMITH_EXTRACTED_BOOSTER
    elseif craftingType == CRAFTING_TYPE_CLOTHIER then
        return SOUNDS.CLOTHIER_EXTRACTED_BOOSTER
    elseif craftingType == CRAFTING_TYPE_WOODWORKING then
        return SOUNDS.WOODWORKER_EXTRACTED_BOOSTER
    elseif craftingType == CRAFTING_TYPE_JEWELRYCRAFTING then
        return SOUNDS.JEWELRYCRAFTER_EXTRACTED_BOOSTER
    end
end

local SMITHING_TYPE_TO_FAILED_EXTRACTION_SOUND =
{
    [CRAFTING_TYPE_BLACKSMITHING] = SOUNDS.BLACKSMITH_FAILED_EXTRACTION,
    [CRAFTING_TYPE_CLOTHIER] = SOUNDS.CLOTHIER_FAILED_EXTRACTION,
    [CRAFTING_TYPE_WOODWORKING] = SOUNDS.WOODWORKER_FAILED_EXTRACTION,
    [CRAFTING_TYPE_JEWELRYCRAFTING] = SOUNDS.JEWELRYCRAFTER_FAILED_EXTRACTION,
}
local function GetFailedSmithingExtractionResultInfo(craftingType)
    local failedExtractionSound = internalassert(SMITHING_TYPE_TO_FAILED_EXTRACTION_SOUND[craftingType])
    return SI_SMITHING_EXTRACTION_FAILED, failedExtractionSound
end

local function DidLastCraftGainBooster(numItemsGained)
    local smithingObject = ZO_Smithing_GetActiveObject()
    if smithingObject and smithingObject:IsExtracting() then
        local craftingType = GetCraftingInteractionType()
        local boosterItemType = ZO_CraftingUtils_GetBoosterItemType(craftingType)
        for i = 1, numItemsGained do
            local name, icon, stack, sellPrice, meetsUsageRequirement, equipType, itemType = GetLastCraftingResultItemInfo(i)
            if itemType == boosterItemType then
                return true
            end
        end
    end

    return false
end

function ZO_CraftingResults_Base:SetForceCenterResultsText(forceCenterResultsText)
    g_forceCenterResultsText = forceCenterResultsText
end

function ZO_CraftingResults_Base:ModifyAnchor(control, newAnchor)
    local _, point, relTo, relPoint, offsX, offsY = control:GetAnchor(0)
    self.savedCraftingAnchor = {point, relTo, relPoint, offsX, offsY}
    control:ClearAnchors()
    newAnchor:Set(control)
end

function ZO_CraftingResults_Base:RestoreAnchor(control)
    control:ClearAnchors()
    local restoredAnchor = ZO_Anchor:New(unpack(self.savedCraftingAnchor))
    restoredAnchor:Set(control)
end

do
    local CRAFTING_RESULT_SORT_ORDER =
    {
        displayQuality = { tiebreaker = "quality", isNumeric = true, tieBreakerSortOrder = ZO_SORT_ORDER_DOWN },
        -- quality is deprecated, included here for addon backwards compatibility
        quality = { tiebreaker = "stack", isNumeric = true, tieBreakerSortOrder = ZO_SORT_ORDER_DOWN },
        stack = { tiebreaker = "resultIndex", isNumeric = true, tieBreakerSortOrder = ZO_SORT_ORDER_UP },
        resultIndex = { isNumeric = true },
    }
    local function CompareCraftingResultItems(left, right)
        return ZO_TableOrderingFunction(left, right, "displayQuality", CRAFTING_RESULT_SORT_ORDER, ZO_SORT_ORDER_DOWN)
    end

    function ZO_CraftingResults_Base:CheckCraftProcessCompleted(craftingType)
        if self:IsActive() and not self:IsCraftInProgress() then
            if GetNumLastCraftingResultLearnedTraits() > 0 then
                self:DisplayDiscoveredTraits()
            end

            local abilityId = GetLastCraftingResultAbilityId()
            local playerSkillProgressionData = SKILLS_DATA_MANAGER:GetProgressionDataByAbilityId(abilityId)
            if playerSkillProgressionData then
                local skillData = playerSkillProgressionData:GetSkillData()
                local skillLineData = skillData:GetSkillLineData()
                local skillTypeData = skillLineData:GetSkillTypeData()

                local messageParams = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_CATEGORY_LARGE_TEXT, SOUNDS.SCRIBING_SCRIBE_COMPLETE_ANNOUNCE)
                messageParams:SetCSAType(CENTER_SCREEN_ANNOUNCE_TYPE_CRAFTING_RESULTS)
                local bodyText = zo_strformat(SI_CRAFTED_ABILITY_NAME_AND_SKILL_LINE_FORMATTER, GetAbilityName(abilityId), skillTypeData:GetName(), skillLineData:GetName())
                messageParams:SetText(GetString(SI_SCRIBING_ABILITY_CRAFTED_ANNOUNCEMENT_TITLE), bodyText)
                local abilityIcon = GetAbilityIcon(abilityId)
                messageParams:SetIconData(abilityIcon)
                messageParams:MarkSuppressIconFrame()
                CENTER_SCREEN_ANNOUNCE:AddMessageWithParams(messageParams)
            end

            local numResultCurrencies = GetNumLastCraftingResultCurrencies()
            local numItemsGained, penaltyApplied = GetNumLastCraftingResultItemsAndPenalty()

            if penaltyApplied then
                TriggerTutorial(TUTORIAL_TRIGGER_DECONSTRUCTION_LEVEL_PENALTY)
            end

            if numItemsGained == 0 and numResultCurrencies == 0 then
                if craftingType == CRAFTING_TYPE_ALCHEMY then
                    -- Crafted inert potion
                    ZO_AlertNoSuppression(UI_ALERT_CATEGORY_ALERT, nil, SI_ALCHEMY_NO_YIELD)
                elseif craftingType == CRAFTING_TYPE_ENCHANTING then
                    -- No extraction results
                    ZO_AlertNoSuppression(UI_ALERT_CATEGORY_ALERT, nil, SI_ENCHANT_NO_YIELD)
                elseif IsSmithingCraftingType(craftingType) then
                    -- Only display extraction-related messaging for operations that produce items.
                    if self.craftingProcessProducesItems then
                        -- No results from gear deconstruction
                        local failedExtractionStringId, failedExtractionSoundName = GetFailedSmithingExtractionResultInfo(craftingType)
                        if penaltyApplied then
                            ZO_AlertNoSuppression(UI_ALERT_CATEGORY_ALERT, failedExtractionSoundName, SI_SMITHING_DECONSTRUCTION_LEVEL_PENALTY)
                        else
                            ZO_AlertNoSuppression(UI_ALERT_CATEGORY_ALERT, failedExtractionSoundName, failedExtractionStringId)
                        end
                    end
                end
            else
                local shouldDisplayMessages = self:ShouldDisplayMessages()
                local finalItemSoundCategory = ITEM_SOUND_CATEGORY_NONE
                local resultIndex = 0
                local resultItems = {}
                for i = 1, numItemsGained do
                    local name, icon, stack, sellPrice, meetsUsageRequirement, equipType, itemType, itemStyle, displayQuality, itemSoundCategory, itemInstanceId = GetLastCraftingResultItemInfo(i)

                    -- Don't save messages if we can't display them immediately
                    if shouldDisplayMessages then
                        resultIndex = resultIndex + 1
                        table.insert(resultItems,
                        {
                            resultType = ZO_CRAFTING_RESULT_TYPE.ITEM,
                            resultIndex = resultIndex,
                            name = name,
                            icon = icon,
                            stack = stack,
                            color = GetItemQualityColor(displayQuality),
                            sellPrice = sellPrice,
                            meetsUsageRequirement = meetsUsageRequirement,
                            equipType = equipType,
                            itemType = itemType,
                            itemStyle = itemStyle,
                            displayQuality = displayQuality,
                            -- quality is deprecated, included here for addon backwards compatibility
                            quality = displayQuality,
                            itemSoundCategory = itemSoundCategory,
                            itemInstanceId = itemInstanceId,
                            itemLink = GetLastCraftingResultItemLink(i),
                        })
                    end

                    if finalItemSoundCategory == ITEM_SOUND_CATEGORY_NONE then
                        finalItemSoundCategory = itemSoundCategory
                    end
                end

                table.sort(resultItems, CompareCraftingResultItems)
                for _, resultItem in ipairs(resultItems) do
                    self:DisplayCraftingResult(resultItem)
                end

                if numResultCurrencies ~= 0 and shouldDisplayMessages then
                    -- Append any resulting currencies after any resulting items.
                    for currencyIndex = 1, numResultCurrencies do
                        resultIndex = resultIndex + 1
                        local currencyType, currencyAmount = GetLastCraftingResultCurrencyInfo(currencyIndex)
                        local IS_SINGULAR = true
                        local IS_NOT_UPPERCASE = false
                        local currencyName = GetCurrencyName(currencyType, IS_SINGULAR, IS_NOT_UPPERCASE)
                        local resultData =
                        {
                            resultType = ZO_CRAFTING_RESULT_TYPE.CURRENCY,
                            resultIndex = resultIndex,
                            currencyType = currencyType,
                            name = currencyName,
                            icon = ZO_Currency_GetPlatformCurrencyIcon(currencyType),
                            stack = currencyAmount,
                            color = ZO_Currency_GetPlatformColor(currencyType),
                        }
                        self:DisplayCraftingResult(resultData)
                    end
                end

                local gainedBooster = DidLastCraftGainBooster(numItemsGained)
                if gainedBooster then
                    PlaySound(GetBoosterFoundSoundForCraftingType())
                else
                    self:PlayCraftedSound(finalItemSoundCategory)
                end

                if penaltyApplied then
                    ZO_AlertNoSuppression(UI_ALERT_CATEGORY_ALERT, nil, SI_SMITHING_DECONSTRUCTION_LEVEL_PENALTY)
                end
            end

            local totalInspiration = GetLastCraftingResultTotalInspiration()
            if totalInspiration > 0 then
                PlaySound(SOUNDS.CRAFTING_GAINED_INSPIRATION)
            end

            CALLBACK_MANAGER:FireCallbacks("CraftingAnimationsStopped")

            if GetNumLastCraftingResultLearnedTranslations() > 0 then
                self:DisplayTranslatedRunes()
            end
        end
    end
end

function ZO_CraftingResults_Base:ForceCompleteCraftProcess()
    if self:IsCraftInProgress() then
        self.craftingProcessCompleted = true
        self.tooltipAnimationCompleted = true
        self.contextualAnimationCompleted = true
        CALLBACK_MANAGER:FireCallbacks("CraftingAnimationsStopped")
    end
end

function ZO_CraftingResults_Base:IsCraftInProgress()
    return self.craftingProcessCompleted == false or self.tooltipAnimationCompleted == false or self.contextualAnimationCompleted == false
end

function ZO_CraftingResults_Base:HasEntries()
    return false
end

function ZO_CraftingResults_Base:AreCraftingResultsEqual(left, right)
    local resultType = left.resultType
    if resultType ~= right.resultType then
        return false
    end

    if resultType == ZO_CRAFTING_RESULT_TYPE.ITEM then
        return left.itemInstanceId == right.itemInstanceId
    elseif resultType == ZO_CRAFTING_RESULT_TYPE.CURRENCY then
        return left.currencyType == right.currencyType
    elseif resultType == ZO_CRAFTING_RESULT_TYPE.KNOWLEDGE then
        --Knowledge will never be the same, as you can only learn a trait or translation once
        return false
    else
        internalassert(false, "No equality has been defined for crafting result type %d.", resultType)
        return false
    end
end

ZO_CraftingResults_Base:MUST_IMPLEMENT("DisplayCraftingResult", resultData)

ZO_CraftingResults_Base:MUST_IMPLEMENT("DisplayDiscoveredTraits")

ZO_CraftingResults_Base:MUST_IMPLEMENT("DisplayTranslatedRunes")

ZO_CraftingResults_Base:MUST_IMPLEMENT("FadeAll")

ZO_CraftingResults_Base:MUST_IMPLEMENT("IsActive")

ZO_CraftingResults_Base:MUST_IMPLEMENT("ShouldDisplayMessages")