ALCHEMY_TRAIT_STRIDE = 5

ZO_ALCHEMY_MODE_NONE = 0
ZO_ALCHEMY_MODE_CREATION = 1
ZO_ALCHEMY_MODE_RECIPES = 2

local REQUIRED_SLOTTED_REAGENTS = 2

ZO_ALCHEMY_DATA_TYPE_SOLVENT = 1
ZO_ALCHEMY_DATA_TYPE_REAGENT = 2

ZO_ALCHEMY_PIN_STATE_HIDDEN = 1
ZO_ALCHEMY_PIN_STATE_INVALID = 2
ZO_ALCHEMY_PIN_STATE_VALID = 3 

ZO_ALCHEMY_SYSTEM_NAME = "alchemy"

function ZO_Alchemy_DoesAlchemyItemPassFilter(bagId, slotIndex, filterType, isQuestFilterChecked, questInfo)
    if isQuestFilterChecked then
        --If no there is no valid combination at all, then everything fails the filter
        if not questInfo.validCombinationFound then
            return false
        end
        local itemId = GetItemId(bagId, slotIndex)
        --If this item does not match any solvents or reagents that are quest related, then it does not pass the filter
        if (not questInfo.reagents or questInfo.reagents[itemId] == nil) and (not questInfo.solvent or questInfo.solvent.itemId ~= itemId) then
            return false
        end
    end

    if filterType == nil then
        return true
    end

    local _, craftingSubItemType = GetItemCraftingInfo(bagId, slotIndex)

    if type(filterType) == "function" then
        return filterType(craftingSubItemType)
    end

    return filterType == craftingSubItemType
end

function ZO_Alchemy_GetTraitInfo(traitIndex, ...)
    return select(traitIndex * ALCHEMY_TRAIT_STRIDE - (ALCHEMY_TRAIT_STRIDE - 1), ...)
end

function ZO_Alchemy_IsAlchemyItem(bagId, slotIndex)
    local usedInCraftingType, craftingSubItemType = GetItemCraftingInfo(bagId, slotIndex)
    if usedInCraftingType == CRAFTING_TYPE_ALCHEMY then
        return craftingSubItemType == ITEMTYPE_REAGENT or IsAlchemySolvent(craftingSubItemType)
    end
end

function ZO_Alchemy_IsThirdAlchemySlotUnlocked()
    return GetNonCombatBonus(NON_COMBAT_BONUS_ALCHEMY_THIRD_SLOT) ~= 0
end

ZO_SharedAlchemy = ZO_CraftingCreateScreenBase:Subclass()

ZO_SharedAlchemy.initializedEvents = false

function ZO_SharedAlchemy:Initialize(control)
    self.control = control
    self.skillInfo = self.control:GetNamedChild("SkillInfo")
    self.cachedValidItemIds = nil
    self.questItems = {}
    self.slottedSolvent = nil
    self.slottedReagents = {}

    self:InitializeInventory()
    self:InitializeTooltip()
    self:InitializeSlots()
    self:InitializeSharedEvents()

    self.alchemyStationInteraction =
    {
        type = "Alchemy Station",
        OnInteractSwitch = function()
            internalassert(false, "OnInteractSwitch is being called.")
            SCENE_MANAGER:ShowBaseScene()
        end,
        interactTypes = { INTERACTION_CRAFT },
    }

    ZO_Skills_TieSkillInfoHeaderToCraftingSkill(self.skillInfo, CRAFTING_TYPE_ALCHEMY)

    self:InitializeScenes()

    CALLBACK_MANAGER:RegisterCallback("CraftingAnimationsStopped", function() self:OnCraftCompleted() end)
end

function ZO_SharedAlchemy:InitializeSharedEvents()
    if not ZO_SharedAlchemy.initializedEvents then
        ZO_SharedAlchemy.initializedEvents = true
    
        EVENT_MANAGER:RegisterForEvent("ZO_SharedAlchemy", EVENT_CRAFTING_STATION_INTERACT, function(eventCode, craftingType, isCraftingSameAsPrevious)
            if craftingType == CRAFTING_TYPE_ALCHEMY then
                if not isCraftingSameAsPrevious then
                    self:ResetSelectedTab()
                end
                SYSTEMS:ShowScene(ZO_ALCHEMY_SYSTEM_NAME)
            end
        end)

        EVENT_MANAGER:RegisterForEvent("ZO_SharedAlchemy", EVENT_END_CRAFTING_STATION_INTERACT, function(eventCode, craftingType)
            if craftingType == CRAFTING_TYPE_ALCHEMY then
                SYSTEMS:HideScene(ZO_ALCHEMY_SYSTEM_NAME)
            end
        end)
    end
end

function ZO_SharedAlchemy:InitializeInventory()
    -- Should be overridden
end

function ZO_SharedAlchemy:InitializeTooltip()
    -- Should be overridden
end

function ZO_SharedAlchemy:InitializeKeybindStripDescriptors()
    -- Should be overridden
end

function ZO_SharedAlchemy:InitializeScenes()
    -- Should be overridden
end

function ZO_SharedAlchemy:UpdateTooltip()
    -- Should be overridden
end

function ZO_SharedAlchemy:UpdateTooltipLayout()
    -- Should be overridden
end

function ZO_SharedAlchemy:InitializeSlots()
    -- Should be overridden
end

function ZO_SharedAlchemy:UpdateThirdAlchemySlot()
    -- Should be overridden
end

function ZO_SharedAlchemy:ResetSelectedTab()
    -- Should be overridden
end

function ZO_SharedAlchemy:UpdateQuestPins()
    -- Should be overridden
end

-- Overrides ZO_CraftingCreateScreenBase
function ZO_SharedAlchemy:GetMultiCraftMaxIterations()
    if not self:IsCraftable() then
        return 0
    end

    local maxIterations = GetMaxIterationsPossibleForAlchemyItem(self:GetAllCraftingBagAndSlots())
    if maxIterations > 1 then
        -- If a player doesn't already know all the traits that would go into the
        -- final effect of this alchemy item, they will need to craft with them at
        -- least once to learn what the final result will be. Let's restrict them to
        -- single crafts until they've done that.
        local _, prospectiveAlchemyResult = GetAlchemyResultingItemLink(self:GetAllCraftingBagAndSlots())
        if prospectiveAlchemyResult ~= PROSPECTIVE_ALCHEMY_RESULT_KNOWN then
            return 1
        end

        -- The player may be using a reagent that could have no effect on the final
        -- craft. For the same reason we prevent multicrafting unknown potions, lets
        -- prevent multicrafting here too
        if self:DoesAnyReagentHaveNoKnownTraits() then
            return 1
        end
    end

    return maxIterations
end

function ZO_SharedAlchemy:GetResultItemLink()
    return GetAlchemyResultingItemLink(self:GetAllCraftingBagAndSlots())
end

function ZO_SharedAlchemy:GetMultiCraftNumResults(numIterations)
    local solventBagId, solventSlotIndex = self.solventSlot:GetBagAndSlot()
    return GetAlchemyResultQuantity(solventBagId, solventSlotIndex, numIterations)
end

function ZO_SharedAlchemy:UpdateMultiCraft()
    -- Should be overidden
end

function ZO_SharedAlchemy:OnCraftCompleted()
    if not self.control:IsHidden() then
        self:UpdateTooltip()
        self:UpdateMultiCraft()
    end
end

function ZO_SharedAlchemy:CanItemBeAddedToCraft(bagId, slotIndex)
    local usedInCraftingType, craftingSubItemType, rankRequirement = GetItemCraftingInfo(bagId, slotIndex)
    if usedInCraftingType == CRAFTING_TYPE_ALCHEMY then
        if IsAlchemySolvent(craftingSubItemType) and rankRequirement <= GetNonCombatBonus(NON_COMBAT_BONUS_ALCHEMY_LEVEL) then
            return true
        elseif craftingSubItemType == ITEMTYPE_REAGENT then
            return true
        end
    end
    return false
end

function ZO_SharedAlchemy:CreateInteractScene(name)
    return ZO_InteractScene:New(name, SCENE_MANAGER, self.alchemyStationInteraction)
end

function ZO_SharedAlchemy:IsItemAlreadySlottedToCraft(bagId, slotIndex)
    local itemId = GetItemInstanceId(bagId, slotIndex)
    if self.solventSlot:IsItemId(itemId) then
        return true
    else
        for i, slot in ipairs(self.reagentSlots) do
            if slot:IsItemId(itemId) then
                return true
            end
        end
    end
    return false
end

function ZO_SharedAlchemy:AddItemToCraft(bagId, slotIndex)
    local usedInCraftingType, craftingSubItemType, rankRequirement = GetItemCraftingInfo(bagId, slotIndex)
    if usedInCraftingType == CRAFTING_TYPE_ALCHEMY then
        if IsAlchemySolvent(craftingSubItemType) and rankRequirement <= GetNonCombatBonus(NON_COMBAT_BONUS_ALCHEMY_LEVEL) then
            self:SetSolventItem(bagId, slotIndex)
        elseif craftingSubItemType == ITEMTYPE_REAGENT and not self:FindAlreadySlottedReagent(bagId, slotIndex) then
            self:SetReagentItem(nil, bagId, slotIndex)
        end
    end
end

function ZO_SharedAlchemy:RemoveItemFromCraft(bagId, slotIndex)
    local itemId = GetItemInstanceId(bagId, slotIndex)
    if self.solventSlot:IsItemId(itemId) then
        self:SetSolventItem(nil)
    else
        for i, slot in ipairs(self.reagentSlots) do
            if slot:IsItemId(itemId) then
                self:SetReagentItem(i, nil)
                break
            end
        end
    end
end

function ZO_SharedAlchemy:SetSolventItem(bagId, slotIndex)
    self.solventSlot:SetItem(bagId, slotIndex)
end

function ZO_SharedAlchemy:FindNextSlotToInsertReagent()
    for i, slot in ipairs(self.reagentSlots) do
        if not slot:HasItem() and slot:MeetsUsabilityRequirement() then
            return i
        end
    end

    local reagentSlots = ZO_Alchemy_IsThirdAlchemySlotUnlocked() and 3 or 2
    return self.lastReagentIndexAdded % reagentSlots + 1
end

function ZO_SharedAlchemy:SetReagentItem(reagentSlot, bagId, slotIndex)
    if reagentSlot == nil then
        reagentSlot = self:FindNextSlotToInsertReagent()
    end

    self.reagentSlots[reagentSlot]:SetItem(bagId, slotIndex)
    self.lastReagentIndexAdded = reagentSlot
end

local function AddTraitCounts(traitCounts, cancellingTraitCounts, ...)
    for i = 1, NUM_ALCHEMY_TRAITS_PER_REAGENT do
        local traitName, _, _, cancellingTraitName = ZO_Alchemy_GetTraitInfo(i, ...)
        if traitName then
            traitCounts[traitName] = (traitCounts[traitName] or 0) + 1
        end
        if cancellingTraitName then
            cancellingTraitCounts[cancellingTraitName] = (cancellingTraitCounts[cancellingTraitName] or 0) + 1
        end
    end
end

function ZO_SharedAlchemy:UpdateReagentTraits()
    self.matchingTraits = {}
    self.cancelledTraits = {}

    local addedTraitCounts = {}
    local cancellingTraitCounts = {}
    for i, slot in ipairs(self.reagentSlots) do
        if slot:HasItem() then
            local bagId, slotIndex = slot:GetBagAndSlot()
            AddTraitCounts(addedTraitCounts, cancellingTraitCounts, GetAlchemyItemTraits(bagId, slotIndex))
        end
    end

    for traitName, addCount in pairs(addedTraitCounts) do
        if addCount > 1 then
            self.matchingTraits[traitName] = true
        end

        local cancellCount = cancellingTraitCounts[traitName] or 0
        if(cancellCount > 0) then
            self.cancelledTraits[traitName] = true
        end
    end

    for i, slot in ipairs(self.reagentSlots) do
        slot:UpdateTraits()
    end
end

function ZO_SharedAlchemy:HasTraitMatch(traitName)
    return self.matchingTraits and self.matchingTraits[traitName]
end

function ZO_SharedAlchemy:HasTraitCancelled(traitName)
    return self.cancelledTraits and self.cancelledTraits[traitName]
end

function ZO_SharedAlchemy:DoesAnyReagentHaveNoKnownTraits()
    for i, slot in ipairs(self.reagentSlots) do
        if slot:HasItem() then
            local bagId, slotIndex = slot:GetBagAndSlot()
            local noKnownTraits = true
            for traitIndex = 1, NUM_ALCHEMY_TRAITS_PER_REAGENT do
                if IsAlchemyItemTraitKnown(bagId, slotIndex, traitIndex) then
                    noKnownTraits = false
                    break
                end
            end
            if noKnownTraits then
                return true
            end
        end
    end
    return false
end

function  ZO_SharedAlchemy:SetupTraitIcon(textureControl, name, icon, matchIcon, conflictIcon, unknownTexture)
    if self:HasTraitCancelled(name) and conflictIcon then
        textureControl:SetTexture(conflictIcon)
        textureControl:SetAlpha(1)    
    elseif self:HasTraitMatch(name) and matchIcon then
        textureControl:SetTexture(matchIcon)
        textureControl:SetAlpha(1)
    elseif icon then
        textureControl:SetTexture(icon)
        textureControl:SetAlpha(.5)
    else
        textureControl:SetTexture(unknownTexture)
        textureControl:SetAlpha(1)
    end
end

function ZO_SharedAlchemy:FindAlreadySlottedReagent(bagId, slotIndex)
    local itemId = GetItemInstanceId(bagId, slotIndex)
    for i, slot in ipairs(self.reagentSlots) do
        if slot:IsItemId(itemId) then
            return i
        end
    end
    return nil
end

function ZO_SharedAlchemy:ShowAppropriateSlotDropCallouts(craftingSubItemType, rankRequirement)
    self.solventSlot:ShowDropCallout(IsAlchemySolvent(craftingSubItemType) and rankRequirement <= GetNonCombatBonus(NON_COMBAT_BONUS_ALCHEMY_LEVEL))
    for i, slot in ipairs(self.reagentSlots) do
        slot:ShowDropCallout(craftingSubItemType == ITEMTYPE_REAGENT)
    end
end

function ZO_SharedAlchemy:HideAllSlotDropCallouts()
    self.solventSlot:HideDropCallout()
    for i, slot in ipairs(self.reagentSlots) do
        slot:HideDropCallout()
    end
end

function ZO_SharedAlchemy:GetPinStateForItem(itemId, alchemyQuestInfo, dataType)
    local slottedSolvent = self.slottedSolvent
    local slottedReagents = self.slottedReagents
    --First, check if we even have any valid combinations
    if self:HasValidCombinationForQuest() then
        --Check to see if this item is one that will need a quest pin
        if self.questItems.reagents[itemId] or self.questItems.solvent.itemId == itemId then
            --Now we need to determine what type of pin
            local isInvalid = false

            --If we have an incorrect solvent slotted, then the item is invalid
            if dataType == ZO_ALCHEMY_DATA_TYPE_SOLVENT then
                isInvalid = slottedSolvent ~= nil and slottedSolvent.itemId ~= itemId
            elseif dataType == ZO_ALCHEMY_DATA_TYPE_REAGENT then
                isInvalid = slottedSolvent ~= nil and slottedSolvent.itemId ~= self.questItems.solvent.itemId
            end

            --The checks here vary depending on the number of reagents that are already slotted
            if #slottedReagents == 1 then
                local reagent1 = slottedReagents[1]
                local slottedItemId = GetItemId(reagent1.bagId, reagent1.slotIndex)

                if dataType == ZO_ALCHEMY_DATA_TYPE_SOLVENT then
                    --If we are checking a solvent, simply see if the slotted reagent is part of a valid combo
                    isInvalid = isInvalid or (not self.questItems.reagents[slottedItemId])
                elseif dataType == ZO_ALCHEMY_DATA_TYPE_REAGENT then
                    --If we are checking a reagent, see if the slotted reagent and the reagent we are checking form a valid combination together 
                    if slottedItemId ~= itemId then
                        isInvalid = isInvalid or (not self.questItems.reagents[itemId] or not self.questItems.reagents[itemId][slottedItemId])
                    end
                end
            elseif #slottedReagents == 2 then
                local reagent1 = slottedReagents[1]
                local reagent2 = slottedReagents[2]

                itemIdReagent1 = GetItemId(reagent1.bagId, reagent1.slotIndex)
                itemIdReagent2 = GetItemId(reagent2.bagId, reagent2.slotIndex)

                if alchemyQuestInfo.isMasterWrit then
                    if dataType == ZO_ALCHEMY_DATA_TYPE_SOLVENT then
                        --If we are checking a solvent, simply see if the slotted reagents are part of a valid combo
                        isInvalid = isInvalid or (not self.questItems.reagents[itemIdReagent1] or not self.questItems.reagents[itemIdReagent1][itemIdReagent2])
                    elseif dataType == ZO_ALCHEMY_DATA_TYPE_REAGENT then
                        --If we are checking a reagent, see if the slotted reagents and the reagent we are checking form a valid combination together 
                        if itemIdReagent1 ~= itemId and itemIdReagent2 ~= itemId then
                            isInvalid = isInvalid or (not self.questItems.reagents[itemIdReagent1] or not self.questItems.reagents[itemIdReagent1][itemIdReagent2] or not self.questItems.reagents[itemIdReagent1][itemIdReagent2][itemId])
                        else
                            isInvalid = isInvalid or (not self.questItems.reagents[itemIdReagent1] or not self.questItems.reagents[itemIdReagent1][itemIdReagent2])
                        end
                    end
                else
                    --If there are 2 reagents slotted, check and see if they make the desired potion when used with the solvent
                    local result = GetAlchemyResultingItemIdIfKnown(self.questItems.solvent.bagId, self.questItems.solvent.slotIndex, reagent1.bagId, reagent1.slotIndex, reagent2.bagId, reagent2.slotIndex)
                    isInvalid = isInvalid or alchemyQuestInfo.basePotionItemId ~= result
                    if dataType == ZO_ALCHEMY_DATA_TYPE_REAGENT then
                        --If we are checking a reagent, it is only valid if it is one of the slotted items 
                        isInvalid = isInvalid or (itemIdReagent1 ~= itemId and itemIdReagent2 ~= itemId) 
                    end
                end
            elseif #slottedReagents == 3 then
                local reagent1 = slottedReagents[1]
                local reagent2 = slottedReagents[2]
                local reagent3 = slottedReagents[3]

                itemIdReagent1 = GetItemId(reagent1.bagId, reagent1.slotIndex)
                itemIdReagent2 = GetItemId(reagent2.bagId, reagent2.slotIndex)
                itemIdReagent3 = GetItemId(reagent3.bagId, reagent3.slotIndex)

                local encodedTraits = nil

                if alchemyQuestInfo.isMasterWrit then
                    encodedTraits = alchemyQuestInfo.encodedTraits
                end

                --If all 3 reagents are slotted, check and see if they make the desired potion when used with the solvent
                local result = GetAlchemyResultingItemIdIfKnown(self.questItems.solvent.bagId, self.questItems.solvent.slotIndex, reagent1.bagId, reagent1.slotIndex, reagent2.bagId, reagent2.slotIndex, reagent3.bagId, reagent3.slotIndex, encodedTraits)
                isInvalid = isInvalid or alchemyQuestInfo.basePotionItemId ~= result
                if dataType == ZO_ALCHEMY_DATA_TYPE_REAGENT then
                    --If we are checking a reagent, it is only valid if it is one of the slotted items 
                    isInvalid = isInvalid or (itemIdReagent1 ~= itemId and itemIdReagent2 ~= itemId and itemIdReagent3 ~= itemId) 
                end
            end

            if isInvalid then
                return ZO_ALCHEMY_PIN_STATE_INVALID
            else
                return ZO_ALCHEMY_PIN_STATE_VALID
            end
        else
            return ZO_ALCHEMY_PIN_STATE_HIDDEN
        end
    else
        return ZO_ALCHEMY_PIN_STATE_HIDDEN
    end
end

function ZO_SharedAlchemy:UpdatePotentialQuestItems(validItemIds, alchemyQuestInfo)
    self.slottedSolvent, self.slottedReagents = self:GetAllCraftingBagAndSlotsFormatted()
    --TODO: Determine if the self.cachedValidItemIds variable is even needed
    local filteredItemData = validItemIds or self.cachedValidItemIds
    local solvents = {}
    local reagents = {}

    --Check each item
    for _, data in pairs(filteredItemData) do
        local typeId = self:GetDataType(data.bag, data.index)
        --If this is a solvent
        if typeId == ZO_ALCHEMY_DATA_TYPE_SOLVENT then
            --If this is a valid solvent for the item and material id, then add it to the list of potential solvents
            if alchemyQuestInfo.basePotionItemId and alchemyQuestInfo.materialItemId and IsAlchemySolventForItemAndMaterialId(data.bag, data.index, alchemyQuestInfo.basePotionItemId, alchemyQuestInfo.materialItemId) then
                local item = 
                {
                    bagId = data.bag,
                    slotIndex = data.index,
                    itemId = GetItemId(data.bag, data.index)
                }
                table.insert(solvents, item)
            end
        --If this is a reagent
        elseif typeId == ZO_ALCHEMY_DATA_TYPE_REAGENT then
            --If this reagent has the (known) trait that we want, add it to the list of potential reagents
            local hasMasterWritTrait = alchemyQuestInfo.isMasterWrit and DoesAlchemyItemHaveKnownEncodedTrait(data.bag, data.index, alchemyQuestInfo.encodedTraits)
            local hasNormalWritTrait = alchemyQuestInfo.desiredTrait ~= nil and DoesAlchemyItemHaveKnownTrait(data.bag, data.index, alchemyQuestInfo.desiredTrait)
            if hasMasterWritTrait or hasNormalWritTrait then
                local item = 
                {
                    bagId = data.bag,
                    slotIndex = data.index,
                    itemId = GetItemId(data.bag, data.index)
                }
                table.insert(reagents, item)
            end
        end
    end

    self.cachedValidItemIds = filteredItemData
    
    --Now we need to narrow it down further
    self:DetermineQuestPinInfo(solvents, reagents, alchemyQuestInfo)
end

function ZO_SharedAlchemy:DetermineQuestPinInfo(solvents, reagents, alchemyQuestInfo)
    local requiredSolvent = nil
    local validCombinations = {}
    local validCombinationFound = false

    --If we didn't find any solvents in the first place, don't bother
    if #solvents > 0 then
        if #solvents > 1 then
            --This should never happen, but in case it does, we should know about it
            internalassert(false, "Multiple solvents found that match the crafting goal")
        end

        requiredSolvent = solvents[1]

        --Master writs get to look for combinations of 3 instead of combinations of 2
        if alchemyQuestInfo.isMasterWrit then
            --First, determine what the valid combinations are
            for _, reagent1 in ipairs(reagents) do
                for _, reagent2 in ipairs(reagents) do
                    --Skip over combinations of the same reagent
                    if reagent1.itemId ~= reagent2.itemId then
                        for _, reagent3 in ipairs(reagents) do
                            --Skip over combinations of the same reagent
                            if reagent1.itemId ~= reagent3.itemId and reagent2.itemId ~= reagent3.itemId then
                                --Form a table of potential combinations
                                local resultItemId = GetAlchemyResultingItemIdIfKnown(requiredSolvent.bagId, requiredSolvent.slotIndex, reagent1.bagId, reagent1.slotIndex, reagent2.bagId, reagent2.slotIndex, reagent3.bagId, reagent3.slotIndex, alchemyQuestInfo.encodedTraits)
                                if alchemyQuestInfo.basePotionItemId == resultItemId then
                                    if validCombinations[reagent1.itemId] == nil then
                                        validCombinations[reagent1.itemId] = {}
                                    end

                                    if validCombinations[reagent1.itemId][reagent2.itemId] == nil then
                                        validCombinations[reagent1.itemId][reagent2.itemId] = {}
                                    end 
                                    validCombinations[reagent1.itemId][reagent2.itemId][reagent3.itemId] = true
                                    validCombinationFound = true
                                end
                            end
                        end
                    end
                end
            end
        else
            --First, determine what the valid combinations are
            for _, reagent1 in ipairs(reagents) do
                for _, reagent2 in ipairs(reagents) do
                    --Skip over combinations of the same reagent
                    if reagent1.itemId ~= reagent2.itemId then
                        --Form a table of potential combinations
                        local resultItemId = GetAlchemyResultingItemIdIfKnown(requiredSolvent.bagId, requiredSolvent.slotIndex, reagent1.bagId, reagent1.slotIndex, reagent2.bagId, reagent2.slotIndex)
                        if alchemyQuestInfo.basePotionItemId == resultItemId then
                            if validCombinations[reagent1.itemId] == nil then
                                validCombinations[reagent1.itemId] = {}
                            end
                            validCombinations[reagent1.itemId][reagent2.itemId] = true
                            validCombinationFound = true
                        end
                    end
                end
            end
        end
    end
    self.questItems = {solvent = requiredSolvent, reagents = validCombinations, validCombinationFound = validCombinationFound}
    self:UpdateQuestPins()
    CALLBACK_MANAGER:FireCallbacks("AlchemyInfoReady")
end

function ZO_SharedAlchemy:GetAllCraftingBagAndSlotsFormatted()
    local slottedSolvent = nil
    local slottedReagents = {}

    --Basically take this data and return it as something slightly more organized for use with the quest pin stuff
    local slottedSolventBagId, slottedSolventIndex, slottedReagentBagId1, slottedReagentIndex1, slottedReagentBagId2, slottedReagentIndex2, slottedReagentBagId3, slottedReagentIndex3 = self:GetAllCraftingBagAndSlots()

    if slottedSolventBagId then
        slottedSolvent =
        {
            bagId = slottedSolventBagId,
            slotIndex = slottedSolventIndex,
            itemId = GetItemId(slottedSolventBagId, slottedSolventIndex)
        }
    end

    --Created a table of the slotted reagents
    if slottedReagentBagId1 then
        local reagent1 =                 
        {
            bagId = slottedReagentBagId1, 
            slotIndex = slottedReagentIndex1,
            itemId = GetItemId(slottedReagentBagId1, slottedReagentIndex1)
        }
        table.insert(slottedReagents, reagent1)
        if slottedReagentBagId2 then
            local reagent2 =                 
            { 
                bagId = slottedReagentBagId2, 
                slotIndex = slottedReagentIndex2,
                itemId = GetItemId(slottedReagentBagId2, slottedReagentIndex2)
            }  
            table.insert(slottedReagents, reagent2)
            if slottedReagentBagId3 then
                local reagent3 =                 
                {
                    bagId = slottedReagentBagId3, 
                    slotIndex = slottedReagentIndex3,
                    itemId = GetItemId(slottedReagentBagId3, slottedReagentIndex3)
                }
                table.insert(slottedReagents, reagent3)
            end
        end
    end

    return slottedSolvent, slottedReagents
end

function ZO_SharedAlchemy:OnInventoryUpdate(validItemIds)
    local changed = false
    self.solventSlot:ValidateItemId(validItemIds)
    for i, slot in ipairs(self.reagentSlots) do
        slot:ValidateItemId(validItemIds)
    end

    self:UpdateMultiCraft()
    self:UpdateReagentTraits()
end

function ZO_SharedAlchemy:IsSlotted(bagId, slotIndex)
    if self.solventSlot:IsBagAndSlot(bagId, slotIndex) then
        return true
    end
    for i, slot in ipairs(self.reagentSlots) do
        if slot:IsBagAndSlot(bagId, slotIndex) then
            return true
        end
    end
    return false
end

function ZO_SharedAlchemy:ClearSelections(suppressSound, ignoreUsabilityRequirement)
    local NO_BAG = nil
    local NO_INDEX = nil
    self.solventSlot:SetItem(NO_BAG, NO_INDEX, suppressSound, ignoreUsabilityRequirement)

    for i, slot in ipairs(self.reagentSlots) do
        slot:SetItem(NO_BAG, NO_INDEX, suppressSound, ignoreUsabilityRequirement)
    end
end

function ZO_SharedAlchemy:HasSelections()
    if self.solventSlot:HasItem() then
        return true
    end
    for i, slot in ipairs(self.reagentSlots) do
        if slot:HasItem() then
            return true
        end
    end
    return false
end

-- Overrides ZO_CraftingCreateScreenBase
function ZO_SharedAlchemy:IsCraftable()
    if self.solventSlot:HasItem() then
        local numSlotsWithItems = 0
        for i, slot in ipairs(self.reagentSlots) do
            if slot:HasItem() and slot:MeetsUsabilityRequirement() then
                numSlotsWithItems = numSlotsWithItems + 1
                if numSlotsWithItems >= REQUIRED_SLOTTED_REAGENTS then
                    return true
                end
            end
        end
    end
    return false
end

function ZO_SharedAlchemy:ShouldCraftButtonBeEnabled()
    if ZO_CraftingUtils_IsPerformingCraftProcess() then
        return false
    end

    if not self.solventSlot:HasItem() then
        return false, GetString("SI_TRADESKILLRESULT", CRAFTING_RESULT_INVALID_BASE)
    end

    local numSlotsWithItems = 0
    for i, slot in ipairs(self.reagentSlots) do
        if slot:HasItem() and slot:MeetsUsabilityRequirement() then
            numSlotsWithItems = numSlotsWithItems + 1
        end
    end

    if numSlotsWithItems < REQUIRED_SLOTTED_REAGENTS then
        return false, GetString("SI_TRADESKILLRESULT", CRAFTING_RESULT_TOO_FEW_REAGENTS)
    end

    local _, prospectiveAlchemyResult = GetAlchemyResultingItemLink(self:GetAllCraftingBagAndSlots())
    if prospectiveAlchemyResult == PROSPECTIVE_ALCHEMY_RESULT_UNCRAFTABLE then
        -- allow invalid crafts, even if there isn't inventory space for it
        return true
    end

    local maxIterations, craftingResult = GetMaxIterationsPossibleForAlchemyItem(self:GetAllCraftingBagAndSlots())
    return maxIterations ~= 0, GetString("SI_TRADESKILLRESULT", craftingResult)
end

-- Overrides ZO_CraftingCreateScreenBase
function ZO_SharedAlchemy:Create(numIterations)
    CraftAlchemyItem(self:GetAllCraftingParameters(numIterations))
end

local function CollapseBagAndSlots(bagId, slotIndex, ...)
    if select("#", ...) > 0 then
        if bagId and slotIndex then
            return bagId, slotIndex, CollapseBagAndSlots(...)
        end
        return CollapseBagAndSlots(...)
    end
    if bagId and slotIndex then
        return bagId, slotIndex
    end
end

function ZO_SharedAlchemy:GetAllCraftingBagAndSlots()
    local solventBagId, solventSlotIndex = self.solventSlot:GetBagAndSlot()
    local reagent1BagId, reagent1SlotIndex = self.reagentSlots[1]:GetBagAndSlot()
    local reagent2BagId, reagent2SlotIndex = self.reagentSlots[2]:GetBagAndSlot()
    local reagent3BagId, reagent3SlotIndex = nil, nil
    if #self.reagentSlots >= 3 then
        reagent3BagId, reagent3SlotIndex = self.reagentSlots[3]:GetBagAndSlot()
    end
    local bagId1, slotIndex1, bagId2, slotIndex2, bagId3, slotIndex3 = CollapseBagAndSlots(reagent1BagId, reagent1SlotIndex, reagent2BagId, reagent2SlotIndex, reagent3BagId, reagent3SlotIndex)
    return solventBagId, solventSlotIndex, bagId1, slotIndex1, bagId2, slotIndex2, bagId3, slotIndex3
end

-- Overrides ZO_CraftingCreateScreenBase
function ZO_SharedAlchemy:GetAllCraftingParameters(numIterations)
    -- Tricky behavior here: since bag/slot 3 can be nil, you _cannot_ put these crafting parameters into a table safely.
    local solventBagId, solventSlotIndex = self.solventSlot:GetBagAndSlot()
    local reagent1BagId, reagent1SlotIndex = self.reagentSlots[1]:GetBagAndSlot()
    local reagent2BagId, reagent2SlotIndex = self.reagentSlots[2]:GetBagAndSlot()
    local reagent3BagId, reagent3SlotIndex = nil, nil
    if #self.reagentSlots >= 3 then
        reagent3BagId, reagent3SlotIndex = self.reagentSlots[3]:GetBagAndSlot()
    end
    local bagId1, slotIndex1, bagId2, slotIndex2, bagId3, slotIndex3 = CollapseBagAndSlots(reagent1BagId, reagent1SlotIndex, reagent2BagId, reagent2SlotIndex, reagent3BagId, reagent3SlotIndex)
    return solventBagId, solventSlotIndex, bagId1, slotIndex1, bagId2, slotIndex2, bagId3, slotIndex3, numIterations
end

function ZO_SharedAlchemy:OnSolventSlotted(bagId, slotIndex)
    local _, craftingSubItemType = GetItemCraftingInfo(bagId, slotIndex)
    if craftingSubItemType == ITEMTYPE_POISON_BASE then
        TriggerTutorial(TUTORIAL_TRIGGER_ALCHEMY_STATION_OIL_SLOTTED)
    end
end

function ZO_SharedAlchemy:OnSlotChanged(bagId, slotIndex)
    KEYBIND_STRIP:UpdateKeybindButtonGroup(self.keybindStripDescriptor)
    self:UpdateTooltip()
    self:UpdateMultiCraft()
    self.slottedSolvent, self.slottedReagents = self:GetAllCraftingBagAndSlotsFormatted()
    if self.inventory then
        self.inventory:HandleVisibleDirtyEvent()
    end
end

function ZO_SharedAlchemy:FindReagentSlotIndexBySlotControl(slotControl)
    for i, slot in ipairs(self.reagentSlots) do
        if slot:IsSlotControl(slotControl) then
            return i
        end
    end
end

function ZO_SharedAlchemy:HasValidCombinationForQuest()
    return self.questItems.validCombinationFound
end

function ZO_SharedAlchemy:GetDataType(bagId, slotIndex)
    local usedInCraftingType, craftingSubItemType = GetItemCraftingInfo(bagId, slotIndex)
    if IsAlchemySolvent(craftingSubItemType) then
        return ZO_ALCHEMY_DATA_TYPE_SOLVENT
    elseif craftingSubItemType == ITEMTYPE_REAGENT then
        return ZO_ALCHEMY_DATA_TYPE_REAGENT
    end
end

function ZO_SharedAlchemy:DoesCurrentModeHaveSlotAnimations()
    return self.mode == ZO_ALCHEMY_MODE_CREATION
end

--
-- ZO_AlchemySlot
--

ZO_AlchemySlot = ZO_CraftingSlotBase:Subclass()

function ZO_AlchemySlot:New(...)
    return ZO_CraftingSlotBase.New(self, ...)
end

function ZO_AlchemySlot:Initialize(owner, control, emptyTexture, placeSound, removeSound, usabilityPredicate, craftingInventory, emptySlotIcon)
    ZO_CraftingSlotBase.Initialize(self, owner, control, SLOT_TYPE_PENDING_CRAFTING_COMPONENT, emptyTexture, craftingInventory, emptySlotIcon)

    self.createsLevelLabel = self.control:GetNamedChild("CreatesLevel")

    self.placeSound = placeSound
    self.removeSound = removeSound

    self.usabilityPredicate = usabilityPredicate

    self.dropCallout:SetDrawLayer(DL_BACKGROUND)
    self:UpdateTraits()
end

function ZO_AlchemySlot:ShouldBeVisible()
    return self:MeetsUsabilityRequirement()
end

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

    if isCorrectType then
        self.dropCallout:SetTexture("EsoUI/Art/Crafting/crafting_alchemy_goodSlot.dds")
    else
        self.dropCallout:SetTexture("EsoUI/Art/Crafting/crafting_alchemy_badSlot.dds")
    end
end

function ZO_AlchemySlot:SetItem(bagId, slotIndex, suppressSound, ignoreUsabilityRequirement)
    if not ignoreUsabilityRequirement then
        if not self:MeetsUsabilityRequirement() then
            return
        end
    end

    self:SetupItem(bagId, slotIndex)

    if self:HasItem() then
        if not suppressSound then
            PlaySound(self.placeSound)
        end
    else
        if not suppressSound then
            PlaySound(self.removeSound)
        end
    end
end

function ZO_AlchemySlot:Refresh()
    ZO_CraftingSlotBase.Refresh(self)
    if self:HasItem() then
        if self.createsLevelLabel then
            local bagId, slotIndex = self:GetBagAndSlot()
            local craftingSubItemType, _, resultingItemLevel, requiredChampionPoints = select(2, GetItemCraftingInfo(bagId, slotIndex))
            local itemTypeString = GetString((craftingSubItemType == ITEMTYPE_POTION_BASE) and SI_ITEM_FORMAT_STR_POTION or SI_ITEM_FORMAT_STR_POISON)

            if requiredChampionPoints and requiredChampionPoints > 0 then
                self.createsLevelLabel:SetText(zo_strformat(SI_ALCHEMY_CREATES_ITEM_OF_CHAMPION_POINTS, requiredChampionPoints, itemTypeString))
            else
                self.createsLevelLabel:SetText(zo_strformat(SI_ALCHEMY_CREATES_ITEM_OF_LEVEL, resultingItemLevel, itemTypeString))
            end
            self.createsLevelLabel:SetHidden(false)
        end
        self:ShowSlotTraits(true)
    else
        if self.createsLevelLabel then
            self.createsLevelLabel:SetHidden(true)
        end
        self:ShowSlotTraits(false)
    end
end

function ZO_AlchemySlot:UpdateTraits()
    if self:HasItem() then
        self:SetTraits(GetAlchemyItemTraits(self:GetBagAndSlot()))
    else
        self:ClearTraits()
    end
end

function ZO_AlchemySlot:GetUnknownTraitTexture()
    return  "EsoUI/Art/Crafting/crafting_alchemy_trait_slot.dds"
end

function ZO_AlchemySlot:SetTraits(...)
    if self.control.traits then
        local unknownTraitTexture = self:GetUnknownTraitTexture()
        for i, traitTexture in ipairs(self.control.traits) do
            local traitName, traitIcon, traitMatchIcon, _, traitConflictIcon = ZO_Alchemy_GetTraitInfo(i, ...)
            traitTexture.traitName = traitName
            self.owner:SetupTraitIcon(traitTexture, traitName, traitIcon, traitMatchIcon, traitConflictIcon, unknownTraitTexture)
        end
    end
end

function ZO_AlchemySlot:ClearTraits()
    if self.control.traits then
        local unknownTraitTexture = self:GetUnknownTraitTexture()
        for i, traitTexture in ipairs(self.control.traits) do
            traitTexture.traitName = nil
            traitTexture:SetTexture(unknownTraitTexture)
            traitTexture:SetAlpha(1)
        end
    end
end

function ZO_AlchemySlot:MeetsUsabilityRequirement()
    return self.usabilityPredicate == nil or self.usabilityPredicate()
end

function ZO_AlchemySlot:ShowSlotTraits(showTraits)
    if self.emptySlotIcon and self.control.traits then
        for i, trait in ipairs(self.control.traits) do
            trait:SetHidden(not showTraits)
        end
    end
end