LeoAltholic = {}
LeoAltholic.name = "LeoAltholic"
LeoAltholic.displayName = "Leo's Altholic"
LeoAltholic.version = "1.2.3"
LeoAltholic.chatPrefix = "|c39B027" .. LeoAltholic.name .. "|r: "

LeoAltholic.timerQueue = {}
LeoAltholic.charList = {}
LeoAltholic.myself = nil
LeoAltholic.myselfIndex = 0
LeoAltholic.initialized = false

LeoAltholic.maxTraits = select(3,GetSmithingResearchLineInfo(1,1))
LeoAltholic.jewelryMaxTraits = select(3,GetSmithingResearchLineInfo(7,1))
LeoAltholic.panelList = { "Bio", "Stats", "Skills", "Champion", "Daily", "Inventory", "Research" }
--LeoAltholic.panelList = { "Bio", "Stats", "Skills", "Champion", "Tracked", "Daily", "Inventory", "Research" }
LeoAltholic.craftResearch = {CRAFTING_TYPE_BLACKSMITHING,CRAFTING_TYPE_CLOTHIER,CRAFTING_TYPE_WOODWORKING,CRAFTING_TYPE_JEWELRYCRAFTING}

local function loadPlayerDataPart(skillType, baseElem)
    if skillType == nil then
        return
    end
    local numSkillLines = GetNumSkillLines(skillType)
    for i = 1, numSkillLines do
        local name, rank, discovered, lineId, advised, unlockText = GetSkillLineInfo(skillType,i)
        if name == nil then
            name = i;
        end
        if discovered then
            baseElem[i]    = {}
            local baseElemTable = baseElem[i]
            local qty = GetNumSkillAbilities(skillType, i)
            baseElemTable.name = name
            baseElemTable.id = i
            baseElemTable.qty = qty
            baseElemTable.rank = rank
            baseElemTable.lineId = lineId

            baseElemTable.list = {}

            for aj = 1, qty do
                local name2, icon, earnedRank, passive, ultimate, purchased, progressionIndex = GetSkillAbilityInfo(skillType, i, aj)
                local currentUpgradeLevel, maxUpgradeLevel = GetSkillAbilityUpgradeInfo(skillType, i, aj)
                if rank >= earnedRank then
                    local _, _, nextUpgradeEarnedRank = GetSkillAbilityNextUpgradeInfo(skillType, i, aj)
                    local plainName = zo_strformat(SI_ABILITY_NAME, name2)
                    name2 = ZO_Skills_GenerateAbilityName(SI_ABILITY_NAME_AND_UPGRADE_LEVELS, name2, currentUpgradeLevel, maxUpgradeLevel, progressionIndex)
                    baseElemTable.list[aj] = {}
                    local selL = baseElemTable.list[aj]
                    selL.plainName = plainName
                    selL.name = name2
                    selL.earnedRank = earnedRank or 0
                    selL.level = currentUpgradeLevel
                    selL.maxLevel = maxUpgradeLevel
                    selL.nextUpgradeEarnedRank = nextUpgradeEarnedRank
                    if passive then
                        selL.passive = passive
                    end
                    if ultimate then
                        selL.ultimate = ultimate
                    end
                end
            end
        end
    end
end

local function createQuestEntry(questId)
    local questName,backgroundText,activeStepText,activeStepType, activeStepTrackerOverrideText, completed, tracked, questLevel,pushed,questType,instanceDisplayType = GetJournalQuestInfo(questId)
    local repeatType = GetJournalQuestRepeatType(questId)
    local locationInfo
    if questType == QUEST_TYPE_GUILD then
        locationInfo = GetString(LEOALT_GUILD)
    elseif questType == QUEST_TYPE_MAIN_STORY then
        locationInfo = GetString(LEOALT_MAIN_STORY)
    else
        locationInfo = GetJournalQuestLocationInfo(questId)
    end
    local quest = {
        name = questName,
        backgroundText = backgroundText,
        activeStepText = activeStepText,
        activeStepType = activeStepType,
        activeStepTrackerOverrideText = activeStepTrackerOverrideText,
        questLevel = questLevel,
        questType = questType,
        instanceDisplayType = instanceDisplayType,
        location = locationInfo,
        repeatType = repeatType,
        isDaily = repeatType == QUEST_REPEAT_DAILY
    }
    return quest
end

local function initCharsList()

    if LeoAltholic.savedVariables.CharList == nil then LeoAltholic.savedVariables.CharList = {} end

    LeoAltholic.CharName = GetUnitName("player")

    local function getStat(stat) return GetPlayerStat(stat, STAT_BONUS_OPTION_APPLY_BONUS, STAT_SOFT_CAP_OPTION_APPLY_SOFT_CAP) end

    local numChars = GetNumCharacters()
    for k, v in pairs(LeoAltholic.savedVariables.CharList) do
        local deleted = true
        for i = 1, numChars do
            local charName = GetCharacterInfo(i)
            charName = charName:gsub("%^.+", "")
            if k == charName then
                deleted = false
                break
            end
        end
        if deleted then
            LeoAltholic.savedVariables.CharList[k] = nil
        end
    end

    LeoAltholic.CharNum = 0
    local char = LeoAltholic.savedVariables.CharList[LeoAltholic.CharName] or {
        bio = {},
        quests = {
            actives = {},
            tracked = {}
        }
    }

    char.bio.name = LeoAltholic.CharName
    char.bio.gender = GetUnitGender("player")
    if char.bio.gender == 1 then
        char.bio.genderName = GetString(LEOALT_FEMALE)
    else
        char.bio.genderName = GetString(LEOALT_MALE)
    end
    char.bio.level = GetUnitLevel("player")
    char.bio.effectiveLevel = GetUnitEffectiveLevel("player")
    char.bio.isChampion = IsUnitChampion("player")
    char.bio.canChampion = CanUnitGainChampionPoints("player")
    char.bio.championPoints = GetPlayerChampionPointsEarned("player")
    char.bio.race = GetUnitRace("player")
    char.bio.raceId = GetUnitRaceId("player")
    char.bio.class = GetUnitClass("player")
    char.bio.classId = GetUnitClassId("player")
    char.bio.alliance = {
        id = GetUnitAlliance("player"),
        name = GetAllianceName(GetUnitAlliance("player")),
        rank = GetUnitAvARank("player"),
        points = GetCarriedCurrencyAmount(CURT_ALLIANCE_POINTS)
    }
    char.secondsPlayed = GetSecondsPlayed()
    char.bounty = GetBounty()

    local riding = {GetRidingStats()}
    local ridetime = GetTimeUntilCanBeTrained()/1000 or 0
    if ridetime > 1 then ridetime = ridetime + GetTimeStamp() end

    char.attributes = {
        unspent = GetAttributeUnspentPoints(),
        health = {
            points = GetAttributeSpentPoints(ATTRIBUTE_HEALTH),
            max = getStat(STAT_HEALTH_MAX),
            recovery = getStat(STAT_HEALTH_REGEN_COMBAT)
        },
        magicka = {
            points = GetAttributeSpentPoints(ATTRIBUTE_MAGICKA),
            max = getStat(STAT_MAGICKA_MAX),
            recovery = getStat(STAT_MAGICKA_REGEN_COMBAT)
        },
        stamina = {
            points = GetAttributeSpentPoints(ATTRIBUTE_STAMINA),
            max = getStat(STAT_STAMINA_MAX),
            recovery = getStat(STAT_STAMINA_REGEN_COMBAT)
        },
        riding = {
            capacity = riding[1],
            capacityMax = riding[2],
            stamina = riding[3],
            staminaMax = riding[4],
            speed = riding[5],
            speedMax = riding[6],
            time = ridetime
        },
        weapon = {
            damage = getStat(STAT_WEAPON_POWER),
            critical = getStat(STAT_CRITICAL_STRIKE),
            criticalChance = GetCriticalStrikeChance(getStat(STAT_CRITICAL_STRIKE))
        },
        spell = {
            damage = getStat(STAT_SPELL_POWER),
            critical = getStat(STAT_SPELL_CRITICAL),
            criticalChance = GetCriticalStrikeChance(getStat(STAT_SPELL_CRITICAL))
        }
    }
    char.attributes.total = char.attributes.unspent + char.attributes.health.points + char.attributes.magicka.points + char.attributes.stamina.points

    char.resistances = {
        armor             = getStat(STAT_ARMOR_RATING),
        spell      = getStat(STAT_SPELL_RESIST),
        crit       = getStat(STAT_CRITICAL_RESISTANCE),
        cold       = getStat(STAT_DAMAGE_RESIST_COLD),
        disease    = getStat(STAT_DAMAGE_RESIST_DISEASE),
        drown      = getStat(STAT_DAMAGE_RESIST_DROWN),
        earth      = getStat(STAT_DAMAGE_RESIST_EARTH),
        fire       = getStat(STAT_DAMAGE_RESIST_FIRE),
        generic    = getStat(STAT_DAMAGE_RESIST_GENERIC),
        magic      = getStat(STAT_DAMAGE_RESIST_MAGIC),
        oblivion   = getStat(STAT_DAMAGE_RESIST_OBLIVION),
        physical   = getStat(STAT_DAMAGE_RESIST_PHYSICAL),
        poison     = getStat(STAT_DAMAGE_RESIST_POISON),
        shock      = getStat(STAT_DAMAGE_RESIST_SHOCK),
        start      = getStat(STAT_DAMAGE_RESIST_START),
        mitigation        = getStat(STAT_MITIGATION),
        physical   = getStat(STAT_PHYSICAL_RESIST),
        spell_mitigation  = getStat(STAT_SPELL_MITIGATION),
        miss              = getStat(STAT_MISS),
        parry             = getStat(STAT_PARRY),
        block             = getStat(STAT_BLOCK),
        dodge             = getStat(STAT_DODGE)
    }

    char.skills = { }
    char.skills.unspent = GetAvailableSkillPoints()
    char.skills.skyShards = GetNumSkyShards()

    local skillType = SKILL_TYPE_ARMOR
    char.skills.armor = {}
    local baseElem = char.skills.armor
    loadPlayerDataPart(skillType,baseElem)

    --
    skillType = SKILL_TYPE_WORLD
    char.skills.world = {}
    baseElem = char.skills.world
    loadPlayerDataPart(skillType,baseElem)

    --
    skillType = SKILL_TYPE_CLASS
    char.skills.class = {}
    baseElem = char.skills.class
    loadPlayerDataPart(skillType,baseElem)

    --
    skillType = SKILL_TYPE_GUILD
    char.skills.guild = {}
    baseElem = char.skills.guild
    loadPlayerDataPart(skillType,baseElem)

    --
    skillType = SKILL_TYPE_RACIAL
    char.skills.racial = {}
    baseElem = char.skills.racial
    loadPlayerDataPart(skillType,baseElem)

    --
    skillType = SKILL_TYPE_WEAPON
    char.skills.weapon = {}
    baseElem = char.skills.weapon
    loadPlayerDataPart(skillType,baseElem)

    --
    skillType = SKILL_TYPE_AVA
    char.skills.ava = {}
    baseElem = char.skills.ava
    loadPlayerDataPart(skillType,baseElem)

    skillType = SKILL_TYPE_TRADESKILL
    char.skills.craft = {}
    baseElem = char.skills.craft
    loadPlayerDataPart(skillType,baseElem)

    local function GetBonus(craft)
        local skillType0, skillId = GetCraftingSkillLineIndices(craft)
        local _, rank = GetSkillLineInfo(skillType0,skillId)
        return {rank = rank, max = GetMaxSimultaneousSmithingResearch(craft) or 1}
    end

    if char.research == nil then
        char.research = {}
        for _,craft in pairs(LeoAltholic.craftResearch) do
            char.research[craft] = GetBonus(craft)
            for line = 1, GetNumSmithingResearchLines(craft) do
                char.research[craft][line] = {}
                for trait = 1, LeoAltholic.maxTraits do
                    if not char.research[craft][line][trait] then
                        char.research[craft][line][trait] = {}
                    end
                end
            end
        end
    end

    if GetDiffBetweenTimeStamps(char.attributes.riding.time - GetTimeStamp()) < 0 then
        local data = {
            id = '$M' .. char.bio.name,
            name = char.bio.name,
            info = zo_strformat(GetString(LEOALT_MOUNT_FINISHED), char.bio.name),
            time = char.attributes.riding.time
        }
        LeoAltholic.AddToQueue(data)
    end

    for _,craft in pairs(LeoAltholic.craftResearch) do
        for line = 1, GetNumSmithingResearchLines(craft) do
            local lineName, lineIcon = GetSmithingResearchLineInfo(craft, line)
            for trait = 1, LeoAltholic.maxTraits do
                local traitType, _, known = GetSmithingResearchLineTraitInfo(craft, line, trait)
                if char.bio.name ~= LeoAltholic.CharName then
                    local traitData = char.research[craft][line][trait] or false
                    if type(traitData) == 'number' then
                        local data = {
                            id = '$R' .. char.bio.name..craft..line..trait,
                            charName = char.bio.name,
                            info = zo_strformat(
                                    GetString(LEOALT_RESEARCH_FINISHED) .. ': |c00FF00<<C:2>> <<C:3>>|r.',
                                    char.bio.name,
                                    GetString('SI_ITEMTRAITTYPE',traitType),
                                    lineName
                            ),
                            time = traitData
                        }
                        LeoAltholic.AddToQueue(data)
                    end
                end
                if known == false then
                    local _,remaining = GetSmithingResearchLineTraitTimes(craft,line,trait)
                    if remaining and remaining > 0 then
                        char.research[craft][line][trait] = remaining + GetTimeStamp()
                    else
                        char.research[craft][line][trait] = false
                    end
                else
                    char.research[craft][line][trait] = true
                end
            end
        end
    end

    char.champion = {}
    for _, attribute in ipairs({ATTRIBUTE_HEALTH, ATTRIBUTE_MAGICKA, ATTRIBUTE_STAMINA}) do
        char.champion[attribute] = {}
        char.champion[attribute].spent = 0
        char.champion[attribute].unspent = GetNumUnspentChampionPoints(attribute)
        char.champion[attribute].disciplines = {}
    end

    for i = 1, GetNumChampionDisciplines() do
        local attribute = GetChampionDisciplineAttribute(i)
        char.champion[attribute].disciplines[i] = {
            spent = GetNumPointsSpentInChampionDiscipline(i),
            skills = {}
        }
        char.champion[attribute].spent = char.champion[attribute].spent + char.champion[attribute].disciplines[i].spent
        for j = 1, GetNumChampionDisciplineSkills(i) do
            if WillChampionSkillBeUnlocked(i, j) then
                char.champion[attribute].disciplines[i].skills[j] = true
            else
                char.champion[attribute].disciplines[i].skills[j] = GetNumPointsSpentOnChampionSkill(i, j)
            end
        end
    end

    char.inventory = {}
    char.inventory.size = GetBagSize(BAG_BACKPACK)
    char.inventory.used = GetNumBagUsedSlots(BAG_BACKPACK)
    char.inventory.free = GetNumBagFreeSlots(BAG_BACKPACK)

    local _, _, soulGemEmpty = GetSoulGemInfo(SOUL_GEM_TYPE_EMPTY, char.bio.level, true)
    local _, _, soulGemFilled = GetSoulGemInfo(SOUL_GEM_TYPE_FILLED, char.bio.level, true)
    char.inventory.soulGemEmpty = soulGemEmpty
    char.inventory.soulGemFilled = soulGemFilled

    char.inventory.ap = GetAlliancePoints()
    char.inventory.gold = GetCurrentMoney()
    char.inventory.telvar = GetCarriedCurrencyAmount(CURT_TELVAR_STONES)
    char.inventory.writVoucher = GetCarriedCurrencyAmount(CURT_WRIT_VOUCHERS)

    local bag = SHARED_INVENTORY:GenerateFullSlotData(nil,BAG_WORN,BAG_BACKPACK)
    char.inventory[BAG_WORN] = {}
    char.inventory[BAG_BACKPACK] = {}
    for _, data in pairs(bag) do
        char.inventory[data.bagId][data.slotIndex] = {
            link = GetItemLink(data.bagId, data.slotIndex),
            name = data.name,
            count = data.stackCount
        }
    end

    if char.stats == nil then char.stats = {} end

    if char.quests == nil or char.quests.tracked == nil then
        char.quests = {
            actives = {},
            tracked = {}
        }
    else
        char.quests.actives = {}
    end

    local n = 0
    for i = 1, MAX_JOURNAL_QUESTS do
        if IsValidQuestIndex(i) then
            local quest = createQuestEntry(i)
            table.insert(char.quests.actives, quest)
            n = n + 1
        end
    end
    --char.achievements = createCharDataAchievements()

    LeoAltholic.savedVariables.CharList[LeoAltholic.CharName] = char
end

local function fixTrackedQuest()
    for trackedId, trackedQuest in pairs(LeoAltholic.savedVariables.CharList[LeoAltholic.CharName].quests.tracked) do
        if trackedQuest.questType == nil then
            LeoAltholic.savedVariables.CharList[LeoAltholic.CharName].quests.tracked[trackedId] = nil
        end
    end
end

function LeoAltholic.TodayReset()
    local diff = zo_floor(GetDiffBetweenTimeStamps(GetTimeStamp(), 1538200800) / 86400)
    return 1538200800 + (diff * 86400)
end

local function parseAchievementLinkId(link)

    if (link == nil or link == "") then
        return -1, 0, 0
    end

    local linkType, itemText, achId, achData, achTimestamp = link:match("|H(.-):(.-):(.-):(.-):(.-)|h|h")

    if (achId == nil or achData == nil or achTimestamp == nil) then
        return -1, 0, 0
    end

    return achId, tonumber(achData), tonumber(achTimestamp)
end

local function createCharDataAchievements()
    local achievements = {}
    local numTopLevelCategories = GetNumAchievementCategories()
    local countCategory = 1

    for topLevelIndex = 1, numTopLevelCategories do
        local cateName, numCategories, numCateAchievements, earnedPoints, totalPoints, hidesPoints = GetAchievementCategoryInfo(topLevelIndex)

        if earnedPoints > 0 then

            achievements[countCategory] = {
                id = topLevelIndex,
                name = cateName,
                earnedPoints = earnedPoints,
                totalPoints = totalPoints,
                subCategory = {}
            }

            local countSub = 1

            for categoryIndex = 1, numCategories do
                local subcategoryName, numAchievements, earnedSubSubPoints, totalSubSubPoints, hidesSubSubPoints = GetAchievementSubCategoryInfo(topLevelIndex, categoryIndex)

                if earnedSubSubPoints > 0 then
                    achievements[countCategory].subCategory[countSub] = {
                        name = subcategoryName,
                        earnedPoints = earnedSubSubPoints,
                        totalPoints = totalSubSubPoints,
                        achievements = {}
                    }
                    earnedPoints = earnedPoints - earnedSubSubPoints
                    totalPoints = totalPoints - totalSubSubPoints

                    local countAchiev = 1

                    for achievementIndex = 1, numAchievements do
                        local achId = GetAchievementId(topLevelIndex, categoryIndex, achievementIndex)
                        local currentId = GetFirstAchievementInLine(achId)

                        if (currentId == 0) then currentId = achId end

                        while (currentId ~= nil and currentId > 0) do
                            local achLink = GetAchievementLink(currentId)
                            local _, progress, timestamp = parseAchievementLinkId(achLink)

                            if (progress ~= 0 or timestamp ~= 0) then
                                achievements[countCategory].subCategory[countSub].achievements[countAchiev] = {
                                    id = currentId,
                                    name = GetAchievementNameFromLink(achLink),
                                    progress = progress,
                                    timestamp = timestamp,
                                    completed = IsAchievementComplete(currentId),
                                    numCriteria =  GetAchievementNumCriteria(currentId),
                                    numCriteriaDone = 0
                                }
                                local numCriteria = achievements[countCategory].subCategory[countSub].achievements[countAchiev].numCriteria
                                local numCriteriaDone = achievements[countCategory].subCategory[countSub].achievements[countAchiev].numCriteriaDone
                                for critIndex = 1, numCriteria do
                                    local _, numCompleted, numRequired = GetAchievementCriterion(currentId, critIndex)
                                    if numCompleted == numRequired then
                                        numCriteriaDone = numCriteriaDone + 1
                                    end
                                end
                                achievements[countCategory].subCategory[countSub].achievements[countAchiev].numCriteriaDone = numCriteriaDone
                                countAchiev = countAchiev + 1
                            end

                            currentId = GetNextAchievementInLine(currentId)
                        end
                    end
                    countSub = countSub + 1
                end
            end
            countCategory = countCategory + 1
        end
    end

    achievements.earnedPoints = GetEarnedAchievementPoints()
    achievements.totalPoints = GetTotalAchievementPoints()

    return achievements
end

local function copy(obj, seen)
    if type(obj) ~= 'table' then return obj end
    if seen and seen[obj] then return seen[obj] end
    local s = seen or {}
    local res = setmetatable({}, getmetatable(obj))
    s[obj] = res
    for k, v in pairs(obj) do res[copy(k, s)] = copy(v, s) end
    return res
end

function LeoAltholic.GetCharacters()
    if #LeoAltholic.charList == 0 then
        LeoAltholic.charList =  {}
        local i = 1
        for k, v in pairs(LeoAltholic.savedVariables.CharList) do
            if k == nil then return end
            LeoAltholic.charList[i] = copy(v)
            i = i + 1
        end
        table.sort(LeoAltholic.charList, function(a, b)
            return a.bio.name < b.bio.name
        end)
    end
    return LeoAltholic.charList
end

function LeoAltholic.GetItems(char, bagId)
    local itemLines =  {}
    local i = 1
    for k, v in pairs(char.inventory[bagId]) do
        if k == nil then return end
        itemLines[i] = copy(v)
        i = i + 1
    end
    table.sort(itemLines, function(a, b)
        return a.name < b.name
    end)

    return itemLines
end

local function formatMessage(message)
    return LeoAltholic.chatPrefix .. message
end

function LeoAltholic.log(message)
    d(formatMessage(message))
end

function LeoAltholic.GetMyself()
    return LeoAltholic.savedVariables.CharList[LeoAltholic.CharName]
end

local function onResearchCompleted(eventCode, craft, line, trait)
    if LeoAltholic.initialized == false then
        local myself = LeoAltholic.GetMyself()
        myself.research[craft][line][trait] = true
    end

    local lineName = GetSmithingResearchLineInfo(craft, line)
    local traitType = GetSmithingResearchLineTraitInfo(craft, line, trait)

    local name = GetUnitName("player")
    local msg = zo_strformat(
            GetString(LEOALT_RESEARCH_FINISHED) .. ': |c00FF00<<C:2>> <<C:3>>|r.',
            name,
            GetString('SI_ITEMTRAITTYPE',traitType),
            lineName
    )
    if LeoAltholic.savedVariables.settings.completedResearch.screen == true then
        local messageParams = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_CATEGORY_SMALL_TEXT, SOUNDS.SMITHING_FINISH_RESEARCH)
        messageParams:SetText(formatMessage(msg))
        CENTER_SCREEN_ANNOUNCE:AddMessageWithParams(messageParams)
    end
    if LeoAltholic.savedVariables.settings.completedResearch.chat == true then
        LeoAltholic.log(msg)
    end
end

function LeoAltholic.AddToQueue(data)
    for x,queue in pairs(LeoAltholic.timerQueue) do
        if queue.id == data.id then
            return
        end
    end
    table.insert(LeoAltholic.timerQueue, data)
end

local function processQueue()
    for x,data in pairs(LeoAltholic.timerQueue) do
        if GetDiffBetweenTimeStamps(data.time, GetTimeStamp()) <= 0 then
            if data.charName ~= LeoAltholic.CharName and LeoAltholic.savedVariables.settings.completedResearch.chat == true then
                LeoAltholic.log(data.info)
            end
            if data.charName == LeoAltholic.CharName and LeoAltholic.savedVariables.settings.completedResearch.screen == true then
                local messageParams = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_CATEGORY_SMALL_TEXT, SOUNDS.SMITHING_FINISH_RESEARCH)
                messageParams:SetText(formatMessage(data.info))
                CENTER_SCREEN_ANNOUNCE:AddMessageWithParams(messageParams)
            end
            table.remove(LeoAltholic.timerQueue, x)
        end
    end
end

local function createMessageQueue()
    --LeoAltholic.timerQueue = {}
    for _, char in pairs(LeoAltholic.GetCharacters()) do
        if char.bio.name ~= LeoAltholic.CharName then
            if GetDiffBetweenTimeStamps(char.attributes.riding.time - GetTimeStamp()) < 0 then
                local data = {
                    id = '$M' .. char.bio.name,
                    name = char.bio.name,
                    info = zo_strformat(GetString(LEOALT_MOUNT_FINISHED), char.bio.name),
                    time = char.attributes.riding.time
                }
                LeoAltholic.AddToQueue(data)
            end
            for _,craft in pairs(LeoAltholic.craftResearch) do
                for line = 1, GetNumSmithingResearchLines(craft) do
                    local lineName, lineIcon = GetSmithingResearchLineInfo(craft, line)
                    for trait = 1, LeoAltholic.maxTraits do
                        local traitType = GetSmithingResearchLineTraitInfo(craft, line, trait)
                        local traitData = char.research[craft][line][trait] or false
                        if type(traitData) == 'number' then
                            local data = {
                                id = '$R' .. char.bio.name..craft..line..trait,
                                charName = char.bio.name,
                                info = zo_strformat(
                                        GetString(LEOALT_RESEARCH_FINISHED) .. ': |c00FF00<<C:2>> <<C:3>>|r.',
                                        char.bio.name,
                                        GetString('SI_ITEMTRAITTYPE',traitType),
                                        lineName
                                ),
                                time = traitData
                            }
                            LeoAltholic.AddToQueue(data)
                        end
                    end
                end
            end
        end
    end
end

local function onUpdate()

    processQueue()

    if LeoAltholic:isHidden() then
        return
    end

    local control
    for x,char in pairs(LeoAltholic.GetCharacters()) do

        local riding = '|t20:20:esoui/art/mounts/ridingskill_speed.dds|t ' .. char.attributes.riding.speed .. '%' ..
                ' |t20:20:esoui/art/mounts/ridingskill_stamina.dds|t ' .. char.attributes.riding.stamina ..
                ' |t20:20:esoui/art/mounts/ridingskill_capacity.dds|t ' .. char.attributes.riding.capacity ..
                ' |t22:22:esoui/art/miscellaneous/timer_32.dds|t ' .. LeoAltholic.GetTime(char.attributes.riding.time - GetTimeStamp())
        control = WINDOW_MANAGER:GetControlByName('LeoAltholicBioRow'..x.."Riding")
        control:SetText(riding)

        for _,craft in pairs(LeoAltholic.craftResearch) do
            local i = 1
            --local craftName = GetCraftingSkillName(craft)
            for line = 1, GetNumSmithingResearchLines(craft) do
                local lineName, lineIcon = GetSmithingResearchLineInfo(craft, line)
                for trait = 1, LeoAltholic.maxTraits do
                    local traitType = GetSmithingResearchLineTraitInfo(craft, line, trait)
                    --local traitName = GetString('SI_ITEMTRAITTYPE',traitType)
                    local traitData = char.research[craft][line][trait]
                    if type(traitData) == 'number' then
                        control = WINDOW_MANAGER:GetControlByName('LeoAltholicResearchRow'..x.."Craft"..craft.."Timer"..i)
                        control:SetText(LeoAltholic.GetTime(traitData - GetTimeStamp()))
                        i = i + 1
                    end
                end
            end
        end
    end
end

local function trackQuest(questId, automatically)
    local type = GetJournalQuestRepeatType(questId)
    if type ~= QUEST_REPEAT_DAILY then
        if automatically ~= true then
            LeoAltholic.log(GetString(LEOALT_TRACK_ONLY_DAILY))
        end
        return
    end
    local quest = createQuestEntry(questId)
    for _,trackedQuest in pairs(LeoAltholic.savedVariables.CharList[LeoAltholic.CharName].quests.tracked) do
        if (trackedQuest.name == quest.name) then
            if automatically ~= true then
                LeoAltholic.log(zo_strformat(GetString(LEOALT_QUEST_ALREADY_TRACKED), quest.name))
            end
            return
        end
    end
    quest.lastDone = nil
    table.insert(LeoAltholic.savedVariables.CharList[LeoAltholic.CharName].quests.tracked, quest)
    LeoAltholic.log(GetString(LEOALT_TRACKING) .. " " .. quest.name .. "...")
end

local function onQuestAdded(eventCode, journalIndex, questName, objectiveName)
    local quest = createQuestEntry(journalIndex)
    if quest.isDaily == false then return end
    if LeoAltholic.savedVariables.settings.tracked.allDaily == true or (quest.questType == QUEST_TYPE_CRAFTING and LeoAltholic.savedVariables.settings.tracked.dailyWrits == true) then
        trackQuest(journalIndex, true)
    end
end

local function onQuestComplete(eventCode, questName, level, previousExperience, currentExperience, rank, previousPoints, currentPoints)
    for _,trackedQuest in pairs(LeoAltholic.savedVariables.CharList[LeoAltholic.CharName].quests.tracked) do
        if (trackedQuest.name == questName) then
            trackedQuest.lastDone = GetTimeStamp()
            LeoAltholic.log(zo_strformat(GetString(LEOALT_QUEST_DONE_TODAY), questName))
            return
        end
    end
end

local function initializeVars()
    local oldVariables = ZO_SavedVars:NewAccountWide("LeoAltholicSavedVariables", 2)
    LeoAltholic.savedVariables = ZO_SavedVars:NewAccountWide("LeoAltholicSavedVariables", 2, nil, nil, GetWorldName())
    if oldVariables ~= nil and oldVariables.CharList ~= nil then
        local varCopy = copy(oldVariables)
        LeoAltholic.savedVariables.activeTab = varCopy.activeTab
        LeoAltholic.savedVariables.CharList = varCopy.CharList
        LeoAltholic.savedVariables.position = varCopy.position
        LeoAltholicSavedVariables["Default"] = nil
    end
    if not LeoAltholic.savedVariables.settings or LeoAltholic.savedVariables.settings == nil then
        LeoAltholic.savedVariables.settings = {
            tracked = {
                dailyWrits = false,
                allDaily = false
            },
            completedResearch = {
                chat = true,
                screen = true
            }
        }
    end
end

local function initialize()

    local LibFeedback = LibStub:GetLibrary("LibFeedback")
    local showButton, feedbackWindow = LibFeedback:initializeFeedbackWindow(LeoAltholic,
        LeoAltholic.name,LeoAltholicWindow, "@LeandroSilva",
        {TOPRIGHT, LeoAltholicWindow, TOPRIGHT,-50,3},
        {0,1000,10000,"https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Y9KM4PZU2UZ6A"},
        "If you found a bug, have a request or a suggestion, or simply wish to donate, send a mail.")
    LeoAltholic.feedback = feedbackWindow

    LeoAltholic.RestorePosition()

    initCharsList()
    fixTrackedQuest()
    LeoAltholic.InitializeCharacterFrames()
    LeoAltholic.DisplayCharacterFrames()
    createMessageQueue()

    local keybindStripDescriptor = {
        alignment = KEYBIND_STRIP_ALIGN_LEFT,
        {
            name = "Track with " .. LeoAltholic.displayName,
            keybind = "LEOALTHOLIC_TRACK_QUEST",
            enabled = function() return true end,
            visible = function() return true end,
            order = 100,
            callback = function()
                trackQuest(QUEST_JOURNAL_MANAGER:GetFocusedQuestIndex())
            end,
        }
    }

    QUEST_JOURNAL_SCENE:RegisterCallback("StateChange",
            function(oldState, newState)
                if(newState == SCENE_SHOWING) then
                    local type = GetJournalQuestRepeatType(QUEST_JOURNAL_MANAGER:GetFocusedQuestIndex())
                    if type == QUEST_REPEAT_DAILY then
                        KEYBIND_STRIP:AddKeybindButtonGroup(keybindStripDescriptor)
                    else
                        KEYBIND_STRIP:RemoveKeybindButtonGroup(keybindStripDescriptor)
                    end
                elseif(newState == SCENE_HIDDEN) then
                    KEYBIND_STRIP:RemoveKeybindButtonGroup(keybindStripDescriptor)
                end
            end)

    QUEST_JOURNAL_KEYBOARD:RegisterCallback("QuestSelected",
            function(questId)
                local type = GetJournalQuestRepeatType(questId)
                if type == QUEST_REPEAT_DAILY then
                    KEYBIND_STRIP:AddKeybindButtonGroup(keybindStripDescriptor)
                else
                    KEYBIND_STRIP:RemoveKeybindButtonGroup(keybindStripDescriptor)
                end
            end)

    LeoAltholic.settings = LeoAltholic_Settings:New()
    LeoAltholic.settings:CreatePanel()

    LeoAltholic.initialized = true
    CALLBACK_MANAGER:FireCallbacks("LeoAltholicInitialized")
end

local orig_ZO_QuestJournalNavigationEntry_OnMouseUp = ZO_QuestJournalNavigationEntry_OnMouseUp

function ZO_QuestJournalNavigationEntry_OnMouseUp(label, button, upInside)
    orig_ZO_QuestJournalNavigationEntry_OnMouseUp(label, button, upInside)
    if button == MOUSE_BUTTON_INDEX_RIGHT and upInside then
        local questIndex = label.node.data.questIndex
        if questIndex and GetJournalQuestRepeatType(questIndex) == QUEST_REPEAT_DAILY then
            AddMenuItem("Track with " .. LeoAltholic.displayName, function()
                trackQuest(questIndex)
            end)
            ShowMenu(label)
        end
    end
end

local function onNewMovementInUIMode(eventCode)
    if not LeoAltholicWindow:IsHidden() then LeoAltholic:HideUI() end
end

local function onChampionPerksSceneStateChange(oldState,newState)
    if newState == SCENE_SHOWING then
        if not LeoAltholicWindow:IsHidden() then LeoAltholic:HideUI() end
    end
end

local function onPlayerDeactivated(event, addonName)
    EVENT_MANAGER:UnregisterForEvent(LeoAltholic.Name, EVENT_PLAYER_DEACTIVATED)
    initCharsList()
end

local function onAddOnLoaded(event, addonName)
    if addonName ~= LeoAltholic.name then return end

    EVENT_MANAGER:UnregisterForEvent(LeoAltholic.name, EVENT_ADD_ON_LOADED)
    SCENE_MANAGER:RegisterTopLevel(LeoAltholicWindow, false)

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

    initializeVars()

    EVENT_MANAGER:RegisterForEvent(LeoAltholic.name, EVENT_SMITHING_TRAIT_RESEARCH_COMPLETED, onResearchCompleted)

    initialize()

    EVENT_MANAGER:RegisterForEvent(LeoAltholic.name, EVENT_PLAYER_DEACTIVATED, onPlayerDeactivated)
    EVENT_MANAGER:RegisterForEvent(LeoAltholic.name, EVENT_QUEST_COMPLETE, onQuestComplete)
    EVENT_MANAGER:RegisterForEvent(LeoAltholic.name, EVENT_QUEST_ADDED, onQuestAdded)
    EVENT_MANAGER:RegisterForUpdate(LeoAltholic.name, 5000, onUpdate)
    EVENT_MANAGER:RegisterForEvent(LeoAltholic.name, EVENT_NEW_MOVEMENT_IN_UI_MODE, onNewMovementInUIMode)
    CHAMPION_PERKS_SCENE:RegisterCallback('StateChange', onChampionPerksSceneStateChange)

    LeoAltholic.log("started.")
end

EVENT_MANAGER:RegisterForEvent(LeoAltholic.name, EVENT_ADD_ON_LOADED, onAddOnLoaded)