function LeoTrainer.IsInUsableStation()
    local station = GetCraftingInteractionType()

    return station == CRAFTING_TYPE_BLACKSMITHING or
        station == CRAFTING_TYPE_WOODWORKING or
        station == CRAFTING_TYPE_CLOTHIER or
        station == CRAFTING_TYPE_JEWELRYCRAFTING or
        station == CRAFTING_TYPE_ENCHANTING
end

function LeoTrainer.LLC_Completed(event, station, result)
    if event == LLC_NO_FURTHER_CRAFT_POSSIBLE then
        LeoTrainer.debug("Nothing more to craft at this station.")
        if LeoTrainer.stage == LEOTRAINER_STAGE_CRAFT then
            LeoTrainer.nextStage()
        end
        return
    end

    if event ~= LLC_CRAFT_SUCCESS then return end

    if result.Requester ~= LeoTrainer.name then return end

    LeoTrainer.data.craftQueue[result.reference].crafted = true
    LeoTrainer.ui.queueScroll:RefreshData()

    LeoTrainer.craft.MarkItem(result.bag, result.slot)

    if LeoTrainer.craft.StillHaveCraftToDo(station) then return end

    LeoTrainer.debug("All crafts done at this station.")
    LeoTrainer.nextStage()
end

function LeoTrainer.OnCraftComplete(event, station)
    EVENT_MANAGER:UnregisterForUpdate(LeoTrainer.name .. ".ResearchItemTimeout")

    if LeoTrainer.research.isResearching then
        local myName = GetUnitName("player")
        local researching, total = LeoAltholic.GetResearchCounters(station)
        table.remove(LeoTrainer.research.queue, 1)
        LeoTrainer.research.isResearching = false
        if #LeoTrainer.research.queue > 0 then
            zo_callLater(function() LeoTrainer.ResearchNext(true) end, 400)
            return
        end
    end

    if LeoTrainer.isCrafting == 0 then return end

    local data = LeoTrainer.RemoveFromQueue(LeoTrainer.isCrafting)
    LeoTrainer.isCrafting = 0

    data.crafted = GetLastCraftingResultItemLink(1)
    table.insert(craftedItems, data)

    if GetCraftingInteractionType() ~= CRAFTING_TYPE_INVALID and LeoTrainer.continueCrating == true then
        zo_callLater(function() LeoTrainer.CraftNext() end, 200)
    end
end

function LeoTrainer.nextStage(moreDelay)
    LeoTrainer.stage = LeoTrainer.stage + 1
    if LeoTrainer.stage > LEOTRAINER_STAGE_DONE then
        LeoTrainer.stage = LEOTRAINER_STAGE_START
    end
    local craftSkill = GetCraftingInteractionType()
    if craftSkill > 0 then
        zo_callLater(function() LeoTrainer.continueAtStation(craftSkill) end, moreDelay and 200 or 10)
    end
end

function LeoTrainer.continueAtStation(craftSkill)
    if LeoTrainer.stage == LEOTRAINER_STAGE_RESEARCH then
        LeoTrainer.ui.OnStationEnter(craftSkill)
        zo_callLater(function() LeoTrainer.research.OnStationEnter(craftSkill) end, 10)
        return
    end

    if LeoTrainer.stage == LEOTRAINER_STAGE_DECONSTRUCT then
        zo_callLater(function() LeoTrainer.deconstruct.OnStationEnter(craftSkill) end, 10)
        return
    end

    if LeoTrainer.stage == LEOTRAINER_STAGE_CRAFT then
        zo_callLater(function() LeoTrainer.craft.OnStationEnter(craftSkill) end, 10)
        return
    end

    if LeoTrainer.stage == LEOTRAINER_STAGE_DONE then
        zo_callLater(function() LeoTrainer.craft.TryWritCreator(craftSkill) end, 10)
        return
    end
end

function LeoTrainer.stationEnter(craftSkill)
    if not LeoTrainer.IsInUsableStation() then
        LeoTrainer.craft.TryWritCreator(craftSkill)
        return
    end

    if WritCreater then
        EVENT_MANAGER:UnregisterForEvent(WritCreater.name, EVENT_CRAFT_COMPLETED)
    end

    LeoTrainer.ScanBags(craftSkill)

    LeoTrainer.stage = LEOTRAINER_STAGE_RESEARCH

    LeoTrainer.continueAtStation(craftSkill)
    -- LeoTrainerStation:SetHidden(false)
end

function LeoTrainer.stationExit(_, craftSkill)
    EVENT_MANAGER:UnregisterForEvent(LeoTrainer.name, EVENT_CRAFT_COMPLETED)
    if WritCreater then
        EVENT_MANAGER:UnregisterForEvent(WritCreater.name, EVENT_CRAFTING_STATION_INTERACT)
        EVENT_MANAGER:UnregisterForEvent(WritCreater.name, EVENT_CRAFT_COMPLETED)
    end

    LeoTrainer.ui.OnStationExit(craftSkill)
    LeoTrainer.research.OnStationExit(craftSkill)
    LeoTrainer.deconstruct.OnStationExit(craftSkill)
    LeoTrainer.craft.OnStationExit(craftSkill)
end

local craftSkillsBySound = {
    [ITEM_SOUND_CATEGORY_BOW]             = CRAFTING_TYPE_WOODWORKING,
    [ITEM_SOUND_CATEGORY_DAGGER]          = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_HEAVY_ARMOR]     = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_LIGHT_ARMOR]     = CRAFTING_TYPE_CLOTHIER,
    [ITEM_SOUND_CATEGORY_MEDIUM_ARMOR]    = CRAFTING_TYPE_CLOTHIER,
    [ITEM_SOUND_CATEGORY_NECKLACE]        = CRAFTING_TYPE_JEWELRYCRAFTING,
    [ITEM_SOUND_CATEGORY_ONE_HAND_AX]     = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_ONE_HAND_HAMMER] = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_ONE_HAND_SWORD]  = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_RING]            = CRAFTING_TYPE_JEWELRYCRAFTING,
    [ITEM_SOUND_CATEGORY_SHIELD]          = CRAFTING_TYPE_WOODWORKING,
    [ITEM_SOUND_CATEGORY_STAFF]           = CRAFTING_TYPE_WOODWORKING,
    [ITEM_SOUND_CATEGORY_TWO_HAND_AX]     = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_TWO_HAND_HAMMER] = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_TWO_HAND_SWORD]  = CRAFTING_TYPE_BLACKSMITHING,
    [ITEM_SOUND_CATEGORY_ENCHANTMENT]     = CRAFTING_TYPE_ENCHANTING
}

function LeoTrainer.CraftToLineSkill(craftSkill)
    if craftSkill == CRAFTING_TYPE_BLACKSMITHING then return 2
    elseif craftSkill == CRAFTING_TYPE_CLOTHIER then return 3
    elseif craftSkill == CRAFTING_TYPE_WOODWORKING then return 7
    elseif craftSkill == CRAFTING_TYPE_JEWELRYCRAFTING then return 5
    elseif craftSkill == CRAFTING_TYPE_ENCHANTING then return 4 end

    return 0
end

function LeoTrainer.ItemLinkToCraftskill(itemLink)
    return craftSkillsBySound[GetItemSoundCategoryFromLink(itemLink)] or 0
end

local function isLineBeingResearched(research, craft, line)
    for _, researching in pairs(research.doing[craft]) do
        if researching.line == line then
            if researching.doneAt and researching.doneAt - GetTimeStamp() > 0 then
                return true
            end
        end
    end
    return false
end

local function get(tbl, k, ...)
    if tbl == nil or tbl[k] == nil then return nil end
    if select('#', ...) == 0 then return tbl[k] end
    return get(tbl[k], ...)
end

local function set(tbl, k, maybeValue, ...)
    if select('#', ...) == 0 then
      -- this will throw if the top-level tbl is nil, which is the desired behavior
      tbl[k] = maybeValue
      return
    end
    if tbl[k] == nil then tbl[k] = {} end
    set(tbl[k], maybeValue, ...)
end

function LeoTrainer.GetPriorityLineList(charName, craftSkillFilter)
    local charList
    if charName == nil then
        charList = LeoAltholic.ExportCharacters()
    else
        charList = {LeoAltholic.GetCharByName(charName)}
    end

    local craftSKillList = LeoAltholic.craftResearch
    if craftSkillFilter ~= nil then craftSKillList = {craftSkillFilter} end
    local knownCount = {}
    local unknownTraits = {}
    local lineList = {}
    for _, char in pairs(charList) do
        for _, craftSkill in pairs(craftSKillList) do
            set(lineList, char.bio.name, craftSkill, {})

            if LeoTrainer.isTrackingSkill(char.bio.name, craftSkill) and LeoTrainer.canFillSlotWithSkill(char.bio.name, craftSkill) then

                for line = 1, GetNumSmithingResearchLines(craftSkill) do
                    local lineName, _, numTraits = GetSmithingResearchLineInfo(craftSkill, line)

                    set(unknownTraits, char.bio.name, craftSkill, line, {})
                    set(knownCount, char.bio.name, craftSkill, line, 0)

                    for trait = 1, numTraits do
                        local isKnown, isResearching = LeoAltholic.ResearchStatus(craftSkill, line, trait, char.bio.name)
                        if isKnown or isResearching then
                            local numKnown = get(knownCount, char.bio.name, craftSkill, line)
                            numKnown = numKnown + 1
                            set(knownCount, char.bio.name, craftSkill, line, numKnown)
                        elseif not isKnown then
                            table.insert(unknownTraits[char.bio.name][craftSkill][line], trait)
                        end
                    end

                    table.insert(lineList[char.bio.name][craftSkill], {
                        line = line,
                        lineName = lineName,
                        count = knownCount[char.bio.name][craftSkill][line],
                        unknownTraits = unknownTraits[char.bio.name][craftSkill][line],
                        isResearching = isLineBeingResearched(char.research, craftSkill, line)
                    })
                end

                table.sort(lineList[char.bio.name][craftSkill], function(a, b)
                    return a.count < b.count
                end)

            end
        end
    end

    return lineList
end

function LeoTrainer.ScanBags(craftSkill)
    local shouldResearch = LeoTrainer.research.ShouldResearch(craftSkill)
    local shouldDeconstruct = LeoTrainer.deconstruct.ShouldDeconstruct(craftSkill)
    local list = {}
    local items = SHARED_INVENTORY:GenerateFullSlotData(nil, BAG_BACKPACK, BAG_BANK, BAG_SUBSCRIBER_BANK)
    for _, data in pairs(items) do
        local itemLink = GetItemLink(data.bagId, data.slotIndex, LINK_STYLE_BRACKETS)

        local itemCraftSkill = LeoTrainer.ItemLinkToCraftskill(itemLink)
        if itemCraftSkill ~= nil and itemCraftSkill > 0 and itemCraftSkill == craftSkill then
            local handled = false
            if shouldResearch then
                handled = LeoTrainer.research.HandleItem(data.bagId, data.slotIndex, itemLink, craftSkill)
            end
            if not handled and shouldDeconstruct then
                handled = LeoTrainer.deconstruct.HandleItem(data.bagId, data.slotIndex, itemLink, craftSkill)
            end
        end
    end

    return list
end

function LeoTrainer.isTrackingSkill(charName, craftId)
    return LeoTrainer.data.trackedTraits[charName][craftId]
end

function LeoTrainer.setTrackingSkill(charName, craftId, tracking)
    LeoTrainer.data.trackedTraits[charName][craftId] = tracking
end

function LeoTrainer.canFillSlotWithSkill(charName, craftId)
    return LeoTrainer.data.fillSlot[charName][craftId]
end

function LeoTrainer.setFillSlotWithSkill(charName, craftId, tracking)
    LeoTrainer.data.fillSlot[charName][craftId] = tracking
end

function LeoTrainer.Initialize()

    LeoTrainer.settings = LibSavedVars
        :NewAccountWide( "LeoTrainer_Settings", "Account", LeoTrainer.settingsDefaults )
        :AddCharacterSettingsToggle( "LeoTrainer_Settings", "Characters" )
    LeoTrainer.settings.loaded = true

    LeoTrainer.data = LibSavedVars:NewAccountWide( "LeoTrainer_Data", LeoTrainer.dataDefaults )
    LeoTrainer.data.loaded = true

    if LeoTrainer.data.silent == nil then LeoTrainer.data.silent = false end

    if LeoTrainer.settings.deconstruct.maxQuality[CRAFTING_TYPE_ENCHANTING] == nil then LeoTrainer.settings.deconstruct.maxQuality[CRAFTING_TYPE_ENCHANTING] = ITEM_FUNCTIONAL_QUALITY_ARCANE end

    LeoTrainer.stage = LEOTRAINER_STAGE_START

    LeoTrainer.craft.Initialize()
    LeoTrainer.research.Initialize()
    LeoTrainer.bank.Initialize()
    LeoTrainer.ui.Initialize()

    SLASH_COMMANDS["/leotrainer"] = function(cmd)
        if cmd == nil or cmd == "" then
            LeoTrainer.ui:ToggleUI()
            return
        end

        if cmd == "debug" then
            if not LeoTrainer.isDebug then
                LeoTrainer.isDebug = true
                LeoTrainer.debug("Debug mode ON")
            else
                LeoTrainer.isDebug = false
                LeoTrainer.log("Debug mode OFF")
            end
            return
        end

        if cmd == "decon" and LeoTrainer.IsInUsableStation() then
            LeoTrainer.deconstruct.DoDeconstruct()
        end
    end

    if GetDisplayName() == "@LeandroSilva" then
        SLASH_COMMANDS["/rr"] = function(cmd)
            ReloadUI()
        end
    end
end

function LeoTrainer.log(message, force)
    if force == nil then force = false end
    if LeoTrainer.data.silent == true and not force then return end

    d(LeoTrainer.chatPrefix .. message)
end

function LeoTrainer.debug(message)
    if not LeoTrainer.isDebug then return end
    LeoTrainer.log('[D] ' .. message, true)
end

local function onNewMovementInUIMode(eventCode)
    if not LeoTrainerWindow:IsHidden() then LeoTrainer.ui:CloseUI() end
end

local function onChampionPerksSceneStateChange(oldState,newState)
    if newState == SCENE_SHOWING then
        if not LeoTrainerWindow:IsHidden() then LeoTrainer.ui:CloseUI() end
    end
end

local function onLeoAltholicInitialized()
    CALLBACK_MANAGER:UnregisterCallback("LeoAltholicInitialized", onLeoAltholicInitialized)
    SCENE_MANAGER:RegisterTopLevel(LeoTrainerWindow, false)

    LeoTrainer.Initialize()

    if WritCreater then
        EVENT_MANAGER:UnregisterForEvent(WritCreater.name, EVENT_CRAFTING_STATION_INTERACT)
        EVENT_MANAGER:UnregisterForEvent(WritCreater.name, EVENT_CRAFT_COMPLETED)
    end
    EVENT_MANAGER:UnregisterForEvent("LibLazyCrafting", EVENT_CRAFTING_STATION_INTERACT)

    EVENT_MANAGER:RegisterForEvent(LeoTrainer.name, EVENT_CRAFTING_STATION_INTERACT, function (_, ...) LeoTrainer.stationEnter(...) end)
    EVENT_MANAGER:RegisterForEvent(LeoTrainer.name, EVENT_END_CRAFTING_STATION_INTERACT, LeoTrainer.stationExit)
    CALLBACK_MANAGER:RegisterCallback("LAM-PanelControlsCreated", function(panel) LeoTrainer_SettingsMenu:OnSettingsControlsCreated(panel) end)
    EVENT_MANAGER:RegisterForEvent(LeoTrainer.name, EVENT_NEW_MOVEMENT_IN_UI_MODE, onNewMovementInUIMode)
    CHAMPION_PERKS_SCENE:RegisterCallback('StateChange', onChampionPerksSceneStateChange)

    LeoTrainer.LLC = LibLazyCrafting:AddRequestingAddon(LeoTrainer.name, true, LeoTrainer.LLC_Completed)

    LeoTrainer.log("started.", true)
end

function LeoTrainer.OnAddOnLoaded(event, addonName)
    if addonName == LeoTrainer.name then
        EVENT_MANAGER:UnregisterForEvent(LeoTrainer.name, EVENT_ADD_ON_LOADED)
        onLeoAltholicInitialized()
    end
end

CALLBACK_MANAGER:RegisterCallback("LeoAltholicInitialized", onLeoAltholicInitialized)