local _, addon = ...
local RemixAPI = addon.RemixAPI
if not RemixAPI then return end;


local ACTIVE_TRACK_INDEX = 1;


local L = addon.L;
local API = addon.API;
local CallbackRegistry = addon.CallbackRegistry;
local DataProvider = RemixAPI.DataProvider;
local CommitUtil = RemixAPI.CommitUtil;
local Easing_OutQuart = addon.EasingFunctions.outQuart;


local ipairs = ipairs;
local InCombatLockdown = InCombatLockdown;
local IsGamePadFreelookEnabled = IsGamePadFreelookEnabled;


local TEXTURE_FILE = "Interface/AddOns/Plumber/Art/Timerunning/LegionRemixUI.png";


local MainFrame;
local GAMEPAD_MODE = false;


local Constants = {
    NodeSize = 40,
    NodeGap = 4,
    ThreeArrowsSize = 24,

    CardWidth = 732,
    CardHeight = 108,
    CardBGWidth = 768,
    CardBGHeight = 160,
    CardDivWidth = 32,
    CardDivHeight = 96,
    CardScale = 0.8,

    White = {0.88, 0.88, 0.88},
    FelGreenBright = {220/255, 231/255, 96/255},

    HeaderWidth = 444,
    HeaderHeight = 96,
    HeaderBGWidth = 512,
    HeaderBGHeight = 192,
};


local function SetFontStringColor(fontString, key)
    local color = Constants[key];
    fontString:SetTextColor(color[1], color[2], color[3]);
end

local function AddLine(oldText, newText)
    if oldText then
        return oldText.."\n"..newText
    else
        return newText
    end
end

local function SharedFadeIn_OnUpdate(self, elapsed)
    self.t = self.t + elapsed;
    local alpha = 8 * self.t;
    if alpha > 1 then
        alpha = 1;
        self:SetScript("OnUpdate", nil);
        self.t = 0;
    end
    self:SetAlpha(alpha);
end


local NodeButtonMixin = {};
do
    function NodeButtonMixin:OnLoad()
        self.Border:SetTexture(TEXTURE_FILE);
        self.GreenGlow:SetTexture(TEXTURE_FILE);
        self.Sheen:SetTexture(TEXTURE_FILE);
        self:SetSquare();
        self.Icon:SetTexCoord(4/64, 60/64, 4/64, 60/64);
        self.Border:SetSize(64, 64);
        self.Icon:SetSize(36, 36);
        self.IconMask:SetSize(36, 36);
        self:SetScript("OnEnter", self.OnEnter);
        self:SetScript("OnLeave", self.OnLeave);
        self:SetScript("OnDragStart", self.OnDragStart);


        local function AnimSheen_OnPlay()
            self.SheenMask:Show();
            self.Sheen:Show();
        end

        local function AnimSheen_OnStop()
            self.SheenMask:Hide();
            self.Sheen:Hide();
        end

        self.AnimSheen:SetScript("OnPlay", AnimSheen_OnPlay);
        self.AnimSheen:SetScript("OnFinished", AnimSheen_OnStop);
        self.AnimSheen:SetScript("OnStop", AnimSheen_OnStop);
    end

    function NodeButtonMixin:OnEnter()
        MainFrame:HoverNode(self);
    end

    function NodeButtonMixin:OnLeave()
        MainFrame:HideTooltip();
        MainFrame:HoverNode();

        if self.nodeChoices or self.isFlyoutButton then
            if not MainFrame:IsNodeFlyoutFocused(self) then
                MainFrame:CloseNodeFlyout();
            end
        end
    end

    function NodeButtonMixin:OnClick()
        --Only FlyoutButton is clickable
        if not self.isFlyoutButton then return end;

        MainFrame:SetLastGamePadFocus(self.parentNodeButton);
        MainFrame:CloseNodeFlyout();

        local parentNodeButton = self.parentNodeButton;
        if not parentNodeButton then return end;

        local shouldPlaySheen;

        DataProvider:SaveLastSelectedEntryID(self.nodeID, self.entryID);
        if CommitUtil:TryPurchaseSelectionNode(self.nodeID, self.entryID) then
            shouldPlaySheen = true;
        end
        --parentNodeButton.selectedEntryID = self.entryID;
        --parentNodeButton:SetData(self.nodeID, self.entryID, self.definitionID);    --debug
        parentNodeButton.shouldPlaySheen = shouldPlaySheen;
        parentNodeButton:Refresh();
    end

    function NodeButtonMixin:OnFocused()
        self:ShowTooltip();

        if self.nodeChoices then
            MainFrame:ShowNodeFlyout(self);
        end
    end

    function NodeButtonMixin:SetSquare()
        self.entryType = 1;
        self.IconMask:SetTexture("Interface/AddOns/Plumber/Art/BasicShape/Mask-Chamfer", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");
        self.Icon:Show();
        self.IconMask:Show();
    end

    function NodeButtonMixin:SetCircle()
        self.entryType = 2;
        self.IconMask:SetTexture("Interface/AddOns/Plumber/Art/BasicShape/Mask-Circle", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");
        self.Icon:Show();
        self.IconMask:Show();
    end

    function NodeButtonMixin:SetHex()
        self.entryType = 0;
        self.IconMask:SetTexture("Interface/AddOns/Plumber/Art/BasicShape/Mask-Hexagon", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");
        self.Icon:Show();
        self.IconMask:Show();
        self.GreenGlow:SetTexCoord(768/1024, 928/1024, 416/1024, 576/1024);
        self.Sheen:SetTexCoord(768/1024, 928/1024, 576/1024, 736/1024);
        self.SheenMask:SetTexture("Interface/AddOns/Plumber/Art/Timerunning/Mask-Halo", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");
    end

    function NodeButtonMixin:SetSpell(spellID)
        local iconID, originalIconID = C_Spell.GetSpellTexture(spellID);
        self.Icon:SetTexture(originalIconID or iconID);
        self.spellID = spellID;
    end

    function NodeButtonMixin:SetData(nodeID, entryID)
        self.nodeID = nodeID;
        local nodeInfo = DataProvider:GetNodeInfo(nodeID);
        if nodeInfo.type == 2 then  --Enum.TraitNodeType.Selection
            self:SetHex();
        end
        self:SetEntry(entryID);
    end

    function NodeButtonMixin:SetEntry(entryID)
        self.entryID = entryID;
        local entryInfo = DataProvider:GetEntryInfo(entryID);
        self.definitionID = entryInfo.definitionID;
        self.maxRanks = entryInfo.maxRanks;

        if self.entryType ~= 0 then
            if entryInfo.type == 1 then --SpendSquare
                self:SetSquare();
            elseif entryInfo.type == 2 then --SpendCircle
                self:SetCircle();
            end
        end

        local spellID = C_Traits.GetDefinitionInfo(self.definitionID).spellID;
        self:SetSpell(spellID);
        if self.Title then
            CallbackRegistry:LoadSpell(spellID, function()
                local name = C_Spell.GetSpellName(spellID);
                self.Title:SetText(name);
            end);
        end
    end

    function NodeButtonMixin:SetNodeChoices(nodeID, entryIDs)
        local entryID = entryIDs[1];   --debug
        self:SetData(nodeID, entryID);
        self.entryIDs = entryIDs;
        self.nodeChoices = {};
        for i = 1, #entryIDs do
            self.nodeChoices[i] = {nodeID, entryIDs[i]}
        end
    end

    function NodeButtonMixin:Refresh(playAnimation)
        if self.entryIDs then
            self:Refresh_SelectionNode(playAnimation);
            return
        end

        if self.isFlyoutButton then
            self:Refresh_FlyoutButton();
            return
        end

        local nodeInfo = DataProvider:GetNodeInfo(self.nodeID);
        if not nodeInfo then return end;

        local currentRank = nodeInfo.currentRank or 0;
        local ranksPurchased = nodeInfo.ranksPurchased or 0;

        local isActive = (self.trackIndex == ACTIVE_TRACK_INDEX) and ranksPurchased > 0;
        self.isActive = isActive;
        local isPurchased;
        local visualState;
        local rankText;

        if not (isActive or currentRank > 0) then
            visualState = 0;
        else
            if self.entryType == 0 then
                if self.selectedEntryID then
                    visualState = 1;
                else
                    visualState = 2;
                end
            else
                visualState = 1;
            end
        end

        self:SetVisualState(visualState);

        if isActive or currentRank > 0 then
            if self.entryType == 1 then
                rankText = currentRank;
            elseif self.entryType == 2 then
                rankText = currentRank;
            elseif self.entryType == 0 then
                if self.selectedEntryID then
                    self.GreenGlow:Hide();
                else
                    self.GreenGlow:Show();
                end
            end
            self.RankText:SetTextColor(1, 0.82, 0);
        else

        end
        self.RankText:SetText(rankText);
    end

    function NodeButtonMixin:Refresh_SelectionNode(playAnimation)
        local nodeInfo = DataProvider:GetNodeInfo(self.nodeID);
        if not nodeInfo then return end;

        local currentRank = nodeInfo.currentRank or 0;
        local ranksPurchased = nodeInfo.ranksPurchased or 0;
        local activeEntryID;
        local committedEntryID;
        local rankText;

        local isEntryCommitted = false;
        if nodeInfo.entryIDsWithCommittedRanks then
            for _, id in ipairs(nodeInfo.entryIDsWithCommittedRanks) do
                committedEntryID = id;
                activeEntryID = id;
                isEntryCommitted = true;
                break
            end
        end

        if not isEntryCommitted then
            if nodeInfo.entryIDToRanksIncreased then
                for _entryID, totalIncreased in pairs(nodeInfo.entryIDToRanksIncreased) do
                    if totalIncreased > 0 then
                        activeEntryID = _entryID;
                        currentRank = totalIncreased;
                        break
                    end
                end
            end
        end

        if committedEntryID then
            self.selectedEntryID = committedEntryID;
        else
            local selectedEntryID, saved = DataProvider:GetLastSelectedEntryID(self.nodeID, self.entryIDs);
            if saved then
                self.selectedEntryID = selectedEntryID;
            end
        end

        local entryChanged;
        local visualState;
        if activeEntryID then
            entryChanged = activeEntryID ~= self.entryID;
            self:SetEntry(activeEntryID);
            visualState = 1;
            rankText = currentRank;
            self.RankText:SetTextColor(1, 0.82, 0);
            if self.shouldPlaySheen then
                self.shouldPlaySheen = nil;
                self:PlaySheen();
            end
        elseif self.selectedEntryID then
            entryChanged = activeEntryID ~= self.selectedEntryID;
            self:SetEntry(self.selectedEntryID);
            visualState = 0;
            rankText = "";
        else
            rankText = "";
            if self.trackIndex == ACTIVE_TRACK_INDEX then
                visualState = 2;
            else
                visualState = 0;
            end
        end

        if visualState ~= 0 and entryChanged then
            if self.shouldPlaySheen then
                self.shouldPlaySheen = nil;
                if playAnimation then
                    self:PlaySheen();
                end
            end
        end

        self:SetVisualState(visualState);
        self.RankText:SetText(rankText);
    end

    function NodeButtonMixin:Refresh_FlyoutButton()
        local nodeInfo = DataProvider:GetNodeInfo(self.nodeID);
        if not nodeInfo then return end;

        local currentRank = nodeInfo.currentRank or 0;
        local ranksPurchased = nodeInfo.ranksPurchased or 0;
        local increasedRanks = 0;

        if nodeInfo.entryIDToRanksIncreased then
            for _entryID, totalIncreased in pairs(nodeInfo.entryIDToRanksIncreased) do
                if _entryID == self.entryID then
                    if currentRank == 0 then
                        currentRank = totalIncreased;
                    end
                    increasedRanks = totalIncreased;
                    break
                end
            end
        end

        local committedEntryID;
        if nodeInfo.entryIDsWithCommittedRanks then
            for _, id in ipairs(nodeInfo.entryIDsWithCommittedRanks) do
                committedEntryID = id;
                break
            end
        end

        local rankText;
        local visualState;
        local isActive = self.trackIndex == ACTIVE_TRACK_INDEX;
        if isActive then
            if self.entryID == committedEntryID then
                visualState = 1;
                rankText = currentRank;
            else
                visualState = 2;
                rankText = increasedRanks;
            end
        else
            if currentRank > 0 then
                visualState = 1;
                rankText = currentRank;
            else
                visualState = 0;
                rankText = "";
            end
        end
        self:SetVisualState(visualState);
        self.RankText:SetText(rankText);
        if visualState == 1 then
            self.RankText:SetTextColor(1, 0.82, 0);
        elseif visualState == 2 then
            self.RankText:SetTextColor(0.098, 1.000, 0.098);
        end
    end

    function NodeButtonMixin:SetVisualState(visualState)
        --0:Disabled  1:Yellow  2:Green

        self.visualState = visualState;
        local disabled = visualState == 0;

        if disabled then
            self.Icon:SetDesaturated(true);
            self.Icon:SetVertexColor(0.8, 0.8, 0.8);
            self.GreenGlow:Hide();
        else
            self.Icon:SetDesaturated(false);
            self.Icon:SetVertexColor(1, 1, 1);
        end

        if self.entryType == 1 then
            if disabled then
                self.Border:SetTexCoord(384/1024, 512/1024, 0/1024, 128/1024);
            elseif visualState == 2 then
                self.Border:SetTexCoord(0/1024, 128/1024, 128/1024, 256/1024);
            else
                self.Border:SetTexCoord(0/1024, 128/1024, 0/1024, 128/1024);
            end
        elseif self.entryType == 2 then
            if disabled then
                self.Border:SetTexCoord(512/1024, 640/1024, 0/1024, 128/1024);
            elseif visualState == 2 then
                self.Border:SetTexCoord(128/1024, 256/1024, 128/1024, 256/1024);
            else
                self.Border:SetTexCoord(128/1024, 256/1024, 0/1024, 128/1024);
            end
        elseif self.entryType == 0 then
            if disabled then
                self.Border:SetTexCoord(896/1024, 1024/1024, 128/1024, 256/1024);
            elseif visualState == 2 then
                self.Border:SetTexCoord(256/1024, 384/1024, 128/1024, 256/1024);
            else
                self.Border:SetTexCoord(768/1024, 896/1024, 128/1024, 256/1024);
            end
        end
    end

    function NodeButtonMixin:PlaySheen()
        self.AnimSheen:Stop();
        self.AnimSheen:Play();
        PlaySound(SOUNDKIT.UI_CLASS_TALENT_APPLY_COMPLETE);
    end

    function NodeButtonMixin:DebugGetNodeInfo()
        local nodeInfo = {
            currentRank = 0,
            maxRanks = self.maxRanks or 1,
        }
        return nodeInfo
    end

    function NodeButtonMixin:ShowTooltip()
        if self.nodeChoices then
            self.UpdateTooltip = nil;
            return
        end


        local spellID = self.spellID;
        --local overrideSpellID = C_Spell.GetOverrideSpell(spellID);
        --[[
        local spell = Spell:CreateFromSpellID(spellID);
        if not spell:IsSpellDataCached() then
            self.spellLoadCancel = spell:ContinueWithCancelOnSpellLoad(GenerateClosure(self.ShowTooltip, self));
        end
        --]]

        --[[
        local definitionInfo = C_Traits.GetDefinitionInfo(self.definitionID);
        if definitionInfo.overrideSubtext then
            tooltip:AddLine(definitionInfo.overrideSubtext, 1, 1, 1, true);
        end
        if definitionInfo.overrideDescription then
            tooltip:AddLine(definitionInfo.overrideDescription, 1, 1, 1, true);
        end
        --]]


        local name = C_Spell.GetSpellName(spellID);
        if not name then
            name = RETRIEVING_DATA;
        end

        local nodeInfo = DataProvider:GetNodeInfo(self.nodeID) or self:DebugGetNodeInfo();
        local currentRank = nodeInfo.currentRank or 0;
        local ranksPurchased = nodeInfo.ranksPurchased or 0;


        --Bonus Ranks
        local increasedRanks = nodeInfo.entryIDToRanksIncreased and nodeInfo.entryIDToRanksIncreased[self.entryID] or 0;
        --local increasedRanks = 0;
        if self.isFlyoutButton then
            if nodeInfo.entryIDsWithCommittedRanks then
                for _, _entryID in ipairs(nodeInfo.entryIDsWithCommittedRanks) do
                    if _entryID == self.entryID then
                        increasedRanks = nodeInfo.ranksIncreased or 0;
                    else
                        ranksPurchased = 0;
                    end
                end
            end
        else
            increasedRanks = nodeInfo.ranksIncreased or 0;
        end

        local description;
        description = string.format(TALENT_BUTTON_TOOLTIP_RANK_FORMAT, ranksPurchased, nodeInfo.maxRanks);

        if increasedRanks > 0 then
            description = description.." |cff19ff19+"..increasedRanks.."|r";
            --[[
            local increasedTraitDataList = C_Traits.GetIncreasedTraitData(self.nodeID, self.entryID);
            for	_index, increasedTraitData in ipairs(increasedTraitDataList) do
                local r, g, b = C_Item.GetItemQualityColor(increasedTraitData.itemQualityIncreasing);
                local qualityColor = CreateColor(r, g, b, 1);
                local coloredItemName = qualityColor:WrapTextInColorCode(increasedTraitData.itemNameIncreasing);
                description = AddLine(description, TALENT_FRAME_INCREASED_RANKS_TEXT:format(increasedTraitData.numPointsIncreased, coloredItemName));
            end
            --]]
        end


        local activeEntryID = self.entryID;
        description = AddLine(description, " ");
        description = API.ConvertTooltipInfoToOneString(description, "GetTraitEntry", activeEntryID, currentRank);

        local nextEntryInfo = nodeInfo.nextEntry  --(self.maxRanks and self.maxRanks > 1) and self.entryType ~= 1 and self.entryID; --self.nodeInfo.nextEntry;  --debug
		if nextEntryInfo and currentRank > 0 then
            description = AddLine(description, " ");
            description = AddLine(description, TALENT_BUTTON_TOOLTIP_NEXT_RANK);
            local nextRank = currentRank + 1;
            description = API.ConvertTooltipInfoToOneString(description, "GetTraitEntry", nextEntryInfo.entryID, nextRank);
		end

        if self.isArtifactAbility then
            if self.trackIndex == ACTIVE_TRACK_INDEX and self.visualState == 0 then
                description = AddLine(description, "\n|cffb1be5f"..L["Artifact Ability Auto Unlock Tooltip"].."|r");
            end
        end

        if self.isFlyoutButton and InCombatLockdown() then
            description = AddLine(description, "\n|cffff2020"..L["Error Change Trait In Combat"].."|r");
        end

        local function updateTooltipFunc()
            self.ShowTooltip(self);
        end

        local icon = self.Icon:GetTexture();
        MainFrame:SetTooltip(self.trackIndex, icon, name, description, updateTooltipFunc);
    end

    function NodeButtonMixin:OnDragStart()
        --debug
        if InCombatLockdown() then
            API.DisplayErrorMessage(L["Error Drag Spell In Combat"]);
            return
        end

        local macroID = RemixAPI.AcquireArtifactAbilityMacro();
        if macroID then
            PickupMacro(macroID);
        else
            C_Spell.PickupSpell(self.spellID);
        end

        if GetCursorInfo() ~= nil then
            MainFrame:OnDraggingSpell(true);
        end
    end
end


local ArrowsButtonMixin = {};
do
    function ArrowsButtonMixin:OnLoad()
        self:SetScript("OnEnter", self.OnEnter);
        self:SetScript("OnLeave", self.OnLeave);
        self.Border:SetTexture(TEXTURE_FILE);
        self:SetTotalRanks(0);
        self.entryType = -1;
    end

    function ArrowsButtonMixin:SetData(leftNodeID)
        self.nodeIDs = {};
        self.entryIDs = {};
        local numEntries = 0;
        for i = 1, 3 do
            local nodeInfo = DataProvider:GetRightNodeInfo(leftNodeID);
            if nodeInfo then
                numEntries = numEntries + 1;
                if not self.firstNodeID then
                    self.firstNodeID = nodeInfo.ID;
                    local entryInfo = DataProvider:GetEntryInfo(nodeInfo.entryID);
                    self.spellID = C_Traits.GetDefinitionInfo(entryInfo.definitionID).spellID;
                end
                self.nodeIDs[numEntries] = nodeInfo.ID;
                self.entryIDs[numEntries] = nodeInfo.entryID;
                leftNodeID = nodeInfo.ID;
            else
                self:Hide();
            end
        end
        self.maxRanks = numEntries;
    end

    function ArrowsButtonMixin:Refresh()
        local totalRanks = 0;
        if self.trackIndex == ACTIVE_TRACK_INDEX then
            for _, nodeID in ipairs(self.nodeIDs) do
                local nodeInfo = DataProvider:GetNodeInfo(nodeID);
                if nodeInfo.entryIDsWithCommittedRanks and #nodeInfo.entryIDsWithCommittedRanks > 0 then
                    totalRanks = totalRanks + 1;
                end
            end
        end
        self:SetTotalRanks(totalRanks);
    end

    function ArrowsButtonMixin:SetupTextureByRanks(texture, totalRanks)
        if totalRanks == 0 then
            texture:SetTexCoord(320/1024, 384/1024, 64/1024, 128/1024);
        elseif totalRanks == 1 then
            texture:SetTexCoord(320/1024, 384/1024, 0/1024, 64/1024);
        elseif totalRanks == 2 then
            texture:SetTexCoord(256/1024, 320/1024, 64/1024, 128/1024);
        else
            texture:SetTexCoord(256/1024, 320/1024, 0/1024, 64/1024);
        end
    end

    function ArrowsButtonMixin:SetTotalRanks(totalRanks)
        self.totalRanks = totalRanks;
        self:SetupTextureByRanks(self.Border, totalRanks);
    end

    function ArrowsButtonMixin:OnEnter()
        MainFrame:HoverNode(self);
    end

    function ArrowsButtonMixin:OnLeave()
        NodeButtonMixin.OnLeave(self);
        MainFrame:HoverNode();
    end

    function ArrowsButtonMixin:OnFocused()
        self:ShowTooltip();
    end

    function ArrowsButtonMixin:ShowTooltip()
        if not self.entryIDs then return end;

        local name = L["Stat Bonuses"];
        local currentRank = self.totalRanks or 0;
        local maxRanks = #self.entryIDs;
        local description = string.format(TALENT_BUTTON_TOOLTIP_RANK_FORMAT, currentRank, maxRanks);
        local lineText;

        description = AddLine(description, " ");

        for index, entryID in ipairs(self.entryIDs) do
            --local desc = C_Spell.GetSpellDescription(self.spellID);
            local tooltipInfo = C_TooltipInfo.GetTraitEntry(entryID, 1);
            if tooltipInfo then
                local line = tooltipInfo.lines and tooltipInfo.lines[2];
                if line and line.leftText then
                    if index > currentRank then
                        lineText = "|cff808080"..line.leftText.."|r";
                    else
                        lineText = "|cffffd100"..line.leftText.."|r";
                    end
                    description = AddLine(description, lineText);
                end
            end
        end

        local function updateTooltipFunc()
            self.ShowTooltip(self);
        end

        MainFrame:SetTooltip(self.trackIndex, nil, name, description, updateTooltipFunc);
    end
end


local ActivateButtonMixin = {};
do
    function ActivateButtonMixin:OnLoad()
        self:SetWidth(128);
        self:SetHitRectInsets(0, 0, -8, -4);
        self:SetButtonText(TALENT_SPEC_ACTIVATE);
        self:Hide();
        self.onEnterFunc = self.OnEnter;
        self.onLeaveFunc = self.OnLeave;
        self:SetScript("OnClick", self.OnClick);
        --self:SetTheme("LEGION");  --Use the default red. Green feels too homogenized
    end

    function ActivateButtonMixin:OnEnter()
        --local card = self:GetParent();
        --card.HoverHighlight:Show();
    end

    function ActivateButtonMixin:OnLeave()
        --local card = self:GetParent();
        --card.HoverHighlight:Hide();
    end

    function ActivateButtonMixin:OnClick()
        MainFrame:TryActivateArtifactTrack(self.trackIndex);
        PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
    end

    function ActivateButtonMixin:SetParentCard(card)
        self:ClearAllPoints();
        self.parentCard = card;
        if card then
            self.trackIndex = card.trackIndex;
            self:SetParent(card);
            self:SetPoint("TOP", card, "LEFT", 104, -2);
            self:SetAlpha(0);
            self.t = 0;
            self:SetScript("OnUpdate", SharedFadeIn_OnUpdate);
            self:Update();
            self:Show();
            self:OnMouseUp();
        else
            self.trackIndex = nil;
            self:SetParent(MainFrame);
            self:Hide();
        end
    end

    function ActivateButtonMixin:IsParentCard(card)
        return self:IsShown() and self.parentCard == card
    end

    function ActivateButtonMixin:Update(inCombat)
        local isLoading;

        if CommitUtil:IsCommitingInProcess() then
            self:Disable();
            isLoading = true;
        elseif self.trackIndex == DataProvider:GetActiveArtifactTrackIndex() or self.trackIndex == ACTIVE_TRACK_INDEX then
            self:Hide();
        elseif inCombat or InCombatLockdown() then
            self:Disable();

        --elseif not DataProvider:CanPreActivateArtifactTrack() then
            --You can't purchase any artifact ability at this moment
            --but we'll save the trackIndex and automatically upgrade when eligible
        --    self:Enable();
        else
            self:Enable();
        end
        self:ShowLoadingIndicator(isLoading);
    end
end


local TrackCardMixin = {};
do
    local ANIM_OFFSET_H_BUTTON_HOVER = 12;
    local ANIM_DURATION_BUTTON_HOVER = 0.25;

    local InifiteResearchQuests = { --Slain enemies with Artifact Ability active
        [1] = 90115,
        [2] = 92439,
        [3] = 92441,
        [4] = 92440,
        [5] = 92442,
    };

    local function QuestIcon_OnEnter(self)
        if self.questID and C_QuestLog.IsOnQuest(self.questID) then
            local tooltip = GameTooltip;
            local title = API.GetQuestName(self.questID);
            local progressTexts = API.GetQuestProgressTexts(self.questID);
            self.UpdateTooltip = QuestIcon_OnEnter;
            tooltip:SetOwner(self, "ANCHOR_RIGHT");
            tooltip:SetText(title, 1, 0.82, 0, 1, true);
            if progressTexts then
                for _, text in ipairs(progressTexts) do
                    tooltip:AddLine(text, 1, 1, 1, true);
                end
            end
            tooltip:Show();
        end
    end

    local function QuestIcon_OnLeave(self)
        GameTooltip:Hide();
    end

    local function QuestIcon_Update(self)
        if C_QuestLog.IsOnQuest(self.questID) and not C_QuestLog.ReadyForTurnIn(self.questID) then
            self:Show();
        else
            self:Hide();
        end
    end

    function TrackCardMixin:OnLoad()
        self.titleCenterX = 104;
        self.Div:ClearAllPoints();
        self.Div:SetPoint("CENTER", self, "LEFT", 2 * self.titleCenterX - 2, 0);

        local s = Constants.CardScale;
        self:SetSize(Constants.CardWidth * s, Constants.CardHeight * s);
        self.Background:SetSize(Constants.CardBGWidth * s, Constants.CardBGHeight * s);
        self.Div:SetSize(Constants.CardDivWidth * s, Constants.CardDivHeight * s);
        self.HoverHighlight:SetSize(512 * s, Constants.CardBGHeight * s);

        self.Background:SetTexture(TEXTURE_FILE);
        self.Background:SetTexCoord(0/1024, 768/1024, 256/1024, 416/1024);
        self.Div:SetTexture(TEXTURE_FILE);
        self.Div:SetTexCoord(992/1024, 1024/1024, 256/1024, 352/1024);
        self.HoverHighlight:SetTexture(TEXTURE_FILE);
        self.HoverHighlight:SetTexCoord(0/1024, 512/1024, 288/1024, 448/1024);

        self.ActivateFX:SetTexture(TEXTURE_FILE);
        self.ActivateFX:SetTexCoord(0/1024, 768/1024, 416/1024, 576/1024);
        self.ActivateFXMask:SetTexture("Interface/AddOns/Plumber/Art/Timerunning/Mask-Halo", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");

        self.EdgeMask1:SetTexture("Interface/AddOns/Plumber/Art/Timerunning/TrackCardEdgeMask-Left", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");
        self.EdgeMask2:SetTexture("Interface/AddOns/Plumber/Art/Timerunning/TrackCardEdgeMask-Right", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE");
        self.EdgeGlow1:SetTexture("Interface/AddOns/Plumber/Art/Timerunning/EdgeGlow");
        self.EdgeGlow1:SetBlendMode("ADD");
        self.EdgeGlow1:SetVertexColor(205/255, 237/255, 59/255);
        self.EdgeGlow2:SetTexture("Interface/AddOns/Plumber/Art/Timerunning/EdgeGlow");
        self.EdgeGlow2:SetBlendMode("ADD");
        self.EdgeGlow2:SetVertexColor(205/255, 237/255, 59/255);

        self.QuestIcon.Texture:SetTexture(TEXTURE_FILE);
        self.QuestIcon.Texture:SetTexCoord(640/1024, 688/1024, 672/1024, 720/1024);
        self.QuestIcon.HighlightTexture:SetTexture(TEXTURE_FILE);
        self.QuestIcon.HighlightTexture:SetTexCoord(640/1024, 688/1024, 672/1024, 720/1024);
        self.QuestIcon.HighlightTexture:SetBlendMode("ADD");
        self.QuestIcon:SetScript("OnEnter", QuestIcon_OnEnter);
        self.QuestIcon:SetScript("OnLeave", QuestIcon_OnLeave);
        self.QuestIcon.Update = QuestIcon_Update;
        self.QuestIcon.questID = InifiteResearchQuests[self.trackIndex];

        --self:SetScript("OnEnter", self.OnEnter);
        --self:SetScript("OnLeave", self.OnLeave);
    end

    function TrackCardMixin:SetVisualState(visualState)
        -- 0:Inactive  1:Active
        self.visualState = visualState;
        if visualState == 1 then
            self.Background:SetVertexColor(1, 1, 1);
            self.Background:SetDesaturated(false);
            self.EdgeGlow1:Show();
            self.EdgeGlow2:Show();
            self.AnimEdge:Play();
            self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, ANIM_OFFSET_H_BUTTON_HOVER);
            self.Subtitle:Show();
            self.Subtitle:SetText(SPEC_ACTIVE);
            SetFontStringColor(self.Title, "White");
            SetFontStringColor(self.Subtitle, "FelGreenBright");
        else
            self.Background:SetVertexColor(0.8, 0.8, 0.8);
            self.Background:SetDesaturated(true);
            self.EdgeGlow1:Hide();
            self.EdgeGlow2:Hide();
            self.Title:SetTextColor(0.6, 0.59, 0.49);
            if MainFrame.ActivateButton:IsParentCard(self) then
                self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, ANIM_OFFSET_H_BUTTON_HOVER);
            else
                self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, 0);
            end
            self.Subtitle:Hide();
            self.ActivateFX:Hide();
            self.AnimActivateFX:Stop();
        end
    end

    function TrackCardMixin:ShowActivateButton(state)
        if state and not self.activateButtonShown then
            self.activateButtonShown = true;
            MainFrame.ActivateButton:SetParentCard(self);
            self:ShowHoverVisual();
        elseif (not state) and self.activateButtonShown then
            self.activateButtonShown = nil;
            if MainFrame.ActivateButton.parentCard == self then
                MainFrame.ActivateButton:SetParentCard();
            end
            self:ResetHoverVisual();
        end
    end

    function TrackCardMixin:OnEnter()
        if self.visualState ~= 1 then
            self:ShowActivateButton(true);
        end
    end

    function TrackCardMixin:OnLeave()
        self:ShowActivateButton(false);
    end

    local function Anim_ShiftButtonCentent_OnUpdate(self, elapsed)
        self.t = self.t + elapsed;
        local offset;
        if self.t < ANIM_DURATION_BUTTON_HOVER then
            offset = Easing_OutQuart(self.t, 0, ANIM_OFFSET_H_BUTTON_HOVER, ANIM_DURATION_BUTTON_HOVER);
        else
            offset = ANIM_OFFSET_H_BUTTON_HOVER;
            self:SetScript("OnUpdate", nil);
        end
        self.offset = offset;
        self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, offset);
    end

    local function Anim_ResetButtonCentent_OnUpdate(self, elapsed)
        self.t = self.t + elapsed;
        local offset;
        if self.t < ANIM_DURATION_BUTTON_HOVER then
            offset = Easing_OutQuart(self.t, self.offset, 0, ANIM_DURATION_BUTTON_HOVER);
        else
            offset = 0;
            self:SetScript("OnUpdate", nil);
        end
        self.offset = offset;
        self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, offset);
    end

    function TrackCardMixin:ShowHoverVisual()
        self.t = 0;
        self:SetScript("OnUpdate", Anim_ShiftButtonCentent_OnUpdate);
    end

    function TrackCardMixin:ResetHoverVisual()
        self.t = 0;
        if ACTIVE_TRACK_INDEX == self.trackIndex then
            self:SetScript("OnUpdate", nil);
            self.offset = ANIM_OFFSET_H_BUTTON_HOVER;
            self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, ANIM_OFFSET_H_BUTTON_HOVER);
        else
            if self:IsVisible() then
                self:SetScript("OnUpdate", Anim_ResetButtonCentent_OnUpdate);
            else
                self:SetScript("OnUpdate", nil);
                self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, 0);
            end
        end
    end

    function TrackCardMixin:Refresh(playAnimation)
        local isActive = self.trackIndex == ACTIVE_TRACK_INDEX;
        for _, obj in ipairs(self.TraitNodes) do
            obj:Refresh(playAnimation);
        end

        self:SetVisualState(isActive and 1 or 0);

        if isActive then
            self:SetScript("OnUpdate", nil);
            self.Title:SetPoint("CENTER", self, "LEFT", self.titleCenterX, ANIM_OFFSET_H_BUTTON_HOVER);

            if isActive ~= self.isActive and playAnimation then
                self.ActivateFX:Show();
                self.ActivateFX:SetAlpha(0);
                self.AnimActivateFX:Stop();
                self.AnimActivateFX:Play();
                PlaySound(SOUNDKIT.UI_CLASS_TALENT_LEARN_TALENT)
            end
        end

        self.isActive = isActive;
        self.QuestIcon:Update();
    end
end


local TraitTooltipMixin = {};
do
    local TraitTooltipFrame;

    function TraitTooltipMixin:OnHide()
        self:Hide();
        self.alpha = 0;
        self.t = 0;
        self:SetAlpha(0);
        self:SetScript("OnUpdate", nil);
        self.updateTooltipFunc = nil;
        self.owner = nil;
    end

    function TraitTooltipMixin:SetTooltipSpell(spellID)
        local name = C_Spell.GetSpellName(spellID);
        if not name then
            name = RETRIEVING_DATA;
        end
        --local description = API.ConvertTooltipInfoToOneString(nil, "GetSpellByID", spellID);
        local description = C_Spell.GetSpellDescription(spellID);
        local icon = C_Spell.GetSpellTexture(spellID);
        local function updateTooltipFunc()
            if self:IsVisible() then
                self:SetTooltipSpell(spellID);
            end
        end
        self:SetTooltip(icon, name, description, updateTooltipFunc);
    end

    function TraitTooltipMixin:SetTooltipTrait(entryID, rank)
        rank = rank or 1;
        local spellID = DataProvider:GetTraitSpell(entryID);
        local name = DataProvider:GetTraitName(entryID);
        if not name then
            name = RETRIEVING_DATA;
        end
        local description = API.ConvertTooltipInfoToOneString(nil, "GetTraitEntry", entryID, rank);
        local icon = C_Spell.GetSpellTexture(spellID);
        local function updateTooltipFunc()
            if self:IsVisible() then
                self:SetTooltipTrait(entryID, rank);
            end
        end
        self:SetTooltip(icon, name, description, updateTooltipFunc);
    end

    function Tooltip_Show_OnUpdate(self, elapsed)
        self.t = self.t + elapsed;
        if self.t > 0.5 then
            self.t = 0;
            if self.updateTooltipFunc then
                self.updateTooltipFunc(self);
            else
                self:SetScript("OnUpdate", nil);
            end
        end

        if self.isFading then
            self.alpha = self.alpha + 8 * elapsed;
            if self.alpha > 1 then
                self.alpha = 1;
                self.isFading = nil;
            end
            self:SetAlpha(self.alpha);
        end
    end

    function TraitTooltipMixin:SetTooltip(icon, header, description, updateTooltipFunc)
        local padding = 20;

        self.Icon:SetTexture(icon);
        self.Header:SetText(header);
        self.Desc:SetText(description);
        self.updateTooltipFunc = updateTooltipFunc;
        self.t = 0;

        local width = math.max(self.Header:GetWrappedWidth(), self.Desc:GetWrappedWidth());
        local height = self.Header:GetHeight() + 4 + self.Desc:GetHeight();

        self:SetSize(width + 2*padding, height + 2*padding + 6);
        API.UpdateTextureSliceScale(self.BackgroundFrame.Texture);
        self:SetScript("OnUpdate", Tooltip_Show_OnUpdate);
        self.isFading = true;
        self:SetFrameStrata("TOOLTIP");
    end

    local function Tooltip_FadeOut_OnUpdate(self, elapsed)
        self.t = self.t + elapsed;
        if self.t >= 0 then
            self.t = 0;
            self.alpha = self.alpha - 5 * elapsed;
            if self.alpha < 0 then
                self.alpha = 0;
                self:SetScript("OnUpdate", nil);
            end
            self:SetAlpha(self.alpha);
        end
    end

    function TraitTooltipMixin:HideTooltip(fadeOut)
        self.updateTooltipFunc = nil;
        if fadeOut then
            if self:IsVisible() then
                self.t = 0;
                if self.alpha >= 0.99 then
                    self.t = -0.2;
                else
                    self.t = 0;
                end
                self:SetScript("OnUpdate", Tooltip_FadeOut_OnUpdate);
            end
        else
            self:Hide();
        end
    end

    function TraitTooltipMixin:SetOwner(owner)
        self.owner = owner;
    end

    function RemixAPI.GetTraitTooltipFrame()
        if not TraitTooltipFrame then
            local f = CreateFrame("Frame", nil, UIParent, "PlumberLegionRemixTooltipTemplate");
            TraitTooltipFrame = f;
            API.Mixin(f, TraitTooltipMixin);
            f.alpha = 0;
            f:SetAlpha(0);
            f.Icon:SetDesaturation(0.2);
            f.Icon:SetVertexColor(0.8, 0.8, 0.8);
            f:SetScript("OnHide", f.OnHide);
        end
        return TraitTooltipFrame
    end

    function RemixAPI.HideTraitTooltipFrame(fadeOut)
        if TraitTooltipFrame then
            TraitTooltipFrame:HideTooltip(fadeOut);
        end
    end
end

local MainFrameMixin = {};
do
    local DynamicEvents = {
        "PLAYER_REGEN_ENABLED",
        "PLAYER_REGEN_DISABLED",
        "CURRENCY_DISPLAY_UPDATE",
        "TRAIT_TREE_CHANGED",
        "QUEST_ACCEPTED",
        "QUEST_REMOVED",
    };

    function MainFrameMixin:OnShow()
        API.RegisterFrameForEvents(self, DynamicEvents);
        CallbackRegistry:Register("LegionRemix.CommitFinished", self.OnCommitFinished, self);
        CallbackRegistry:Register("LegionRemix.ConfigUpdated", self.RequestUpdate, self);
        self:Refresh();
        self:SetScript("OnEvent", self.OnEvent);
        PlaySound(SOUNDKIT.UI_EXPANSION_LANDING_PAGE_OPEN);
    end

    function MainFrameMixin:OnHide()
        self:Hide();

        API.UnregisterFrameForEvents(self, DynamicEvents);
        CallbackRegistry:UnregisterCallback("LegionRemix.CommitFinished", self.OnCommitFinished, self);
        CallbackRegistry:UnregisterCallback("LegionRemix.ConfigUpdated", self.RequestUpdate, self);
        self:UpdateCardFocus();
        self:CloseNodeFlyout();
        self:SetScript("OnEvent", nil);
        self:SetScript("OnUpdate", nil);
        self:OnDraggingSpell(false);
        PlaySound(SOUNDKIT.UI_EXPANSION_LANDING_PAGE_CLOSE);

        if GAMEPAD_MODE then
            self:SetLastGamePadFocus(nil);
        end
    end

    function MainFrameMixin:UpdateHeader()
        local HeaderFrame = self.HeaderFrame;
        if HeaderFrame then
            local nextUpgradeInfo = DataProvider:GetNextUpgradeInfoForUI();
            if nextUpgradeInfo then
                HeaderFrame.Text1:SetText(nextUpgradeInfo.line1);
                HeaderFrame.Text1:SetTextColor(0.6, 0.6, 0.6);
                HeaderFrame.Text2:SetText(nextUpgradeInfo.line2);
                HeaderFrame.Text2:SetTextColor(177/255, 190/255, 95/255);
            else
                HeaderFrame.Text1:SetText("");
                HeaderFrame.Text2:SetText("");
            end
        end
    end

    function MainFrameMixin:ShowHeaderTooltip()
        local nextUpgradeInfo = DataProvider:GetNextUpgradeInfoForUI();
        if nextUpgradeInfo then
            local TooltipFrame = RemixAPI.GetTraitTooltipFrame();
            TooltipFrame:SetTooltipTrait(nextUpgradeInfo.nextEntryID, nextUpgradeInfo.nextRank);
            TooltipFrame:SetOwner(self);
            TooltipFrame:SetParent(self);
            TooltipFrame:ClearAllPoints();
            TooltipFrame:SetPoint("TOPLEFT", MainFrame, "TOPRIGHT", 4, 0);
            TooltipFrame:SetFrameStrata("TOOLTIP");
            TooltipFrame:Show();
        end
    end

    function MainFrameMixin:OnEvent(event, ...)
        if event == "PLAYER_REGEN_ENABLED" then
            self.ActivateButton:Update();
        elseif event == "PLAYER_REGEN_DISABLED" then
            self.ActivateButton:Update(true);
        elseif event == "CURRENCY_DISPLAY_UPDATE" then
            local currencyID = ...
            if currencyID == 3268 then
                if not self.isUpdatingHeader then
                    self.isUpdatingHeader = true;
                    C_Timer.After(0.2, function()
                        self.isUpdatingHeader = nil;
                        self:UpdateHeader();
                    end);
                end
            end
        elseif event == "TRAIT_CONFIG_UPDATED" then
            local configID = ...
            self:RequestUpdate();
        elseif event == "CURSOR_CHANGED" then
            if self.isDraggingSpell then
                if not GetCursorInfo() then
                    self:OnDraggingSpell(false);
                end
            end
        elseif event == "QUEST_ACCEPTED" or event == "QUEST_REMOVED" then
            self:UpdateQuests();
        end
    end

    function MainFrameMixin:Refresh(playAnimation)
        ACTIVE_TRACK_INDEX = DataProvider:GetActiveArtifactTrackIndex() or DataProvider:GetLastArtifactTrackIndexForCurrentSpec();
        for _, card in ipairs(self.TrackCards) do
            card:Refresh(playAnimation);
        end
        self.ActivateButton:Update();
        self:UpdateNodeFlyoutFrame();
        self:UpdateHeader();
        --self:DebugSaveAllNodes();
    end

    function MainFrameMixin:UpdateQuests()
        for _, card in ipairs(self.TrackCards) do
            card.QuestIcon:Update();
        end
    end

    function MainFrameMixin:OnUpdate_UpdateAfterDelay(elapsed)
        self.t = self.t + elapsed;
        if self.t > 0.2 then
            self.t = nil;
            self:SetScript("OnUpdate", nil);
            self:Refresh();
        end
    end

    function MainFrameMixin:RequestUpdate()
        self.t = 0;
        self:SetScript("OnUpdate", self.OnUpdate_UpdateAfterDelay);
    end

    function MainFrameMixin:DebugSaveAllNodes()
        local trackNodes = {};
        for i, card in ipairs(self.TrackCards) do
            local nodes = {};
            trackNodes[i] = nodes;
            for _, obj in ipairs(card.TraitNodes) do
                if obj.nodeID then
                    table.insert(nodes, obj.nodeID);
                end
                if obj.nodeIDs then
                    for _, nodeID in ipairs(obj.nodeIDs) do
                        table.insert(nodes, nodeID);
                    end
                end
            end
        end
        PlumberDevData.RemixArtifactTrackNodes = trackNodes;
    end

    function MainFrameMixin:TryActivateArtifactTrack(trackIndex)
        if InCombatLockdown() or CommitUtil:IsCommitingInProcess() then
            --Activate button shouldn't be clickable in this scenario
            return
        end

        self.ActivateButton:Disable();
        DataProvider:SetLastArtifactTrackIndexForCurrentSpec(trackIndex);
        if not CommitUtil:TryPurchaseArtifactTrack(trackIndex) then
            if DataProvider:CanPreActivateArtifactTrack() then
                self:OnCommitFinished(true);
            end
        end
        self.ActivateButton:Update();
    end

    function MainFrameMixin:HoverNode(nodeButton)
        local f = self.SharedNodeHighlight;
        f:Hide();
        f:ClearAllPoints();
        self.NodeFocusSolver:SetFocus(nodeButton);
        if nodeButton then
            f:SetParent(nodeButton);
            f:SetPoint("TOPLEFT", nodeButton.Border, "TOPLEFT", 0, 0);
            f:SetPoint("BOTTOMRIGHT", nodeButton.Border, "BOTTOMRIGHT", 0, 0);
            if nodeButton.entryType == 1 then
                f.Texture:SetTexCoord(640/1024, 768/1024, 0/1024, 128/1024);
            elseif nodeButton.entryType == 2 then
                f.Texture:SetTexCoord(768/1024, 896/1024, 0/1024, 128/1024);
            elseif nodeButton.entryType == 0 then
                f.Texture:SetTexCoord(896/1024, 1024/1024, 0/1024, 128/1024);
            elseif nodeButton.entryType == -1 then
                --f.Texture:SetTexCoord(768/1024, 832/1024, 128/1024, 192/1024);
                nodeButton:SetupTextureByRanks(f.Texture, nodeButton.totalRanks);
            end
            f.t = 0;
            f:SetAlpha(0);
            f:SetScript("OnUpdate", SharedFadeIn_OnUpdate);
            f:Show();
        end
    end

    function MainFrameMixin:UpdateNodeFlyoutFrame()
        if self.NodeFlyoutFrame and self.NodeFlyoutFrame:IsVisible() then
            for _, button in ipairs(self.flyoutButtonPool:GetActiveObjects()) do
                button:Refresh();
            end
        end
    end

    function MainFrameMixin:ShowNodeFlyout(nodeButton)
        if not nodeButton.nodeChoices then return end;

        local f = self.NodeFlyoutFrame;
        if not f then
            f = CreateFrame("Frame", nil, self);
            self.NodeFlyoutFrame = f;
            f:EnableMouse(true);
            f:EnableMouseMotion(true);
            f:SetSize(80, 80);

            f:SetAttribute("nodeignoremime", true)
            f:SetScript("OnLeave", function()
                if not(f:IsMouseOver() or (f.owner and f.owner:IsVisible() and f.owner:IsMouseOver())) then
                    MainFrame:CloseNodeFlyout();
                end
            end);

            local function FlyoutButton_Create()
                local button = CreateFrame("Button", nil, f, "PlumberLegionRemixNodeTemplate");
                API.Mixin(button, NodeButtonMixin);
                button.isFlyoutButton = true;
                button:OnLoad();
                button:SetScript("OnClick", button.OnClick);
                local shadow = button:CreateTexture(nil, "BACKGROUND");
                shadow:SetPoint("CENTER", button, "CENTER", 0, -8);
                shadow:SetSize(128, 128);
                shadow:SetTexture(TEXTURE_FILE);
                shadow:SetTexCoord(768/1024, 896/1024, 256/1024, 384/1024);
                return button
            end
            self.flyoutButtonPool = API.CreateObjectPool(FlyoutButton_Create);
        end

        if f:IsVisible() and f.owner == nodeButton then
            return
        end

        f:ClearAllPoints();
        self.flyoutButtonPool:Release();

        local buttonSize = Constants.NodeSize;
        local gapH = Constants.NodeGap;
        local offsetX = 0;

        for i, v in ipairs(nodeButton.nodeChoices) do
            local button = self.flyoutButtonPool:Acquire();
            button:SetData(v[1], v[2], v[3]);
            button.trackIndex = nodeButton.trackIndex;
            button.parentNodeButton = nodeButton;
            button:SetPoint("TOPLEFT", f, "TOPLEFT", offsetX, 0);
            button:SetCircle();
            button:Refresh();
            offsetX = offsetX + buttonSize + gapH;
        end

        local totalWidth = offsetX - gapH;
        local bottomPadding = 6;
        f:SetSize(totalWidth, buttonSize + bottomPadding);
        if GAMEPAD_MODE then
            f:SetPoint("BOTTOMLEFT", nodeButton, "CENTER", -buttonSize * 0.5, 0);
        else
            f:SetPoint("BOTTOM", nodeButton, "TOP", 0, -10 -bottomPadding);
        end
        f:SetFrameStrata("DIALOG");
        f.owner = nodeButton;

        local duration = 0.2;
        local function FlyoutFrame_OnUpdate(self, elapsed)
            self.t = self.t + elapsed;
            local scale;
            if self.t < duration then
                scale = Easing_OutQuart(self.t, 1.0, 1.6, duration);
            else
                scale = 1.6;
                self:SetScript("OnUpdate", nil);
            end
            self:SetScale(scale);

            local alpha = self.t * 8;
            if alpha > 1 then
                alpha = 1;
            end
            self:SetAlpha(alpha);
        end

        f:Show();

        f:SetScale(1.0);
        f:SetAlpha(0);
        f.t = 0;
        f:SetScript("OnUpdate", FlyoutFrame_OnUpdate);
    end

    function MainFrameMixin:CloseNodeFlyout()
        if self.NodeFlyoutFrame then
            self.NodeFlyoutFrame:Hide();
            --self.NodeFlyoutFrame:ClearAllPoints();    --Remove this so ConsolePort cursor can correctly transite
        end
    end

    function MainFrameMixin:IsNodeFlyoutFocused(nodeButton)
        if self.NodeFlyoutFrame then
            if not self.NodeFlyoutFrame:IsVisible() then return end;
            if self.NodeFlyoutFrame:IsMouseOver() then
                return true
            end

            if self.NodeFlyoutFrame.owner:IsMouseOver() then
                return true
            end

            if nodeButton then
                if not self.NodeFlyoutFrame.owner == nodeButton then
                    return false
                end
            end

            for _, button in ipairs(self.flyoutButtonPool:GetActiveObjects()) do
                if button:IsMouseMotionFocus() then
                    return true
                end
            end

            if GAMEPAD_MODE then
                local object = ConsolePort:GetCursorNode();
                if object then
                    if nodeButton == object then
                        return true
                    end
                    for _, button in ipairs(self.flyoutButtonPool:GetActiveObjects()) do
                        if button == object then
                            return true
                        end
                    end
                end
            end
        end
        return false
    end

    function MainFrameMixin:UpdateCardFocus()
        local focusedCard;

        if self:IsVisible() then
            if self.NodeFlyoutFrame and self.NodeFlyoutFrame:IsVisible() then
                if self.NodeFlyoutFrame:IsMouseOver() then
                    focusedCard = self.NodeFlyoutFrame.owner.parentCard;
                end
            end

            if not focusedCard then
                for _, card in ipairs(self.TrackCards) do
                    if card:IsMouseOver() then
                        focusedCard = card;
                        break
                    end
                end
            end

            if GAMEPAD_MODE and IsGamePadFreelookEnabled() then
                focusedCard = nil;
                local object = ConsolePort:GetCursorNode();
                if object and object.parentCard then
                    focusedCard = object.parentCard;
                end
            end
        end

        for _, card in ipairs(self.TrackCards) do
            if card ~= focusedCard then
                card:OnLeave();
            end
        end

        if focusedCard then
            focusedCard:OnEnter();
        end
    end

    function MainFrameMixin:OnCommitFinished(success)
        local playAnimation;
        if success then
            playAnimation = true;
        else

        end
        self:Refresh(playAnimation);
    end

    function MainFrameMixin:SetLastGamePadFocus(object)
        if GAMEPAD_MODE then
            if self.lastGamePadFocus then
                self.lastGamePadFocus:SetAttribute("nodepriority", nil);
            end
            self.lastGamePadFocus = object;
            if object then
                object:SetAttribute("nodepriority", 1);
            end
        end
    end

    function MainFrameMixin:SetTooltip(row, icon, header, description, updateTooltipFunc)
        local f = self.TooltipFrame;
        f:SetTooltip(icon, header, description, updateTooltipFunc);
        f:SetOwner(self);
        f:ClearAllPoints();
        f:SetPoint("TOPLEFT", MainFrame, "TOPRIGHT", 4, (1 - row) * 94.4);
        f:SetFrameStrata("TOOLTIP");
        f:SetParent(self);
        f:Show();
    end

    function MainFrameMixin:HideTooltip()
        self.TooltipFrame:HideTooltip(true);
    end

    function MainFrameMixin:OnDraggingSpell(isDraggingSpell)
        self.isDraggingSpell = isDraggingSpell;
        if isDraggingSpell then
            self:RegisterEvent("CURSOR_CHANGED");
            self:SetFrameStrata("LOW");
            self:SetToplevel(false);
        else
            self:UnregisterEvent("CURSOR_CHANGED");
            self:SetFrameStrata("MEDIUM");
            self:SetToplevel(true);
        end
    end

    function MainFrameMixin:ResetParent()
        --For Narcissus
        if self:GetParent() ~= UIParent then
            self:SetParent(UIParent);
            local frameName = self:GetName();
            local found;
            for _, name in ipairs(UISpecialFrames) do
                if name == frameName then
                    found = true;
                    break
                end
            end
            if not found then
                table.insert(UISpecialFrames, frameName);
            end
        end
    end
end


local function CreateMainUI()
    local baseFrameLevel = 20;
    local headerHeight = 64;
    local CreateFrame = CreateFrame;

    local frameName = "PlumberRemixArtifactUI";
    local f = CreateFrame("Frame", frameName, UIParent, "PlumberRemixArtifactUITemplate");
    f:Hide();
    f:SetToplevel(true);
    MainFrame = f;
    API.Mixin(f, MainFrameMixin);
    f:SetPoint("CENTER", UIParent, "CENTER", 0, 0);
    table.insert(UISpecialFrames, frameName);


    local TrackCards = {};
    MainFrame.TrackCards = TrackCards;


    local SharedNodeHighlight = CreateFrame("Frame", nil, f);
    f.SharedNodeHighlight = SharedNodeHighlight;
    SharedNodeHighlight:Hide();
    SharedNodeHighlight:SetUsingParentLevel(true);
    SharedNodeHighlight.Texture = SharedNodeHighlight:CreateTexture(nil, "OVERLAY");
    SharedNodeHighlight.Texture:SetAllPoints(true);
    SharedNodeHighlight.Texture:SetTexture(TEXTURE_FILE);
    SharedNodeHighlight.Texture:SetBlendMode("ADD");


    local NodeFocusSolver = API.CreateFocusSolver(f);
    f.NodeFocusSolver = NodeFocusSolver;
    NodeFocusSolver:SetDelay(0.2);


    local CardFocusSolver = CreateFrame("Frame", nil, f);
    f.CardFocusSolver = CardFocusSolver;
    CardFocusSolver.t = 0;
    CardFocusSolver:SetScript("OnUpdate", function(self, elapsed)
        self.t = self.t + elapsed;
        if self.t > 0.1 then
            self.t = 0;
            MainFrame:UpdateCardFocus();
        end
    end);


    --Artifact Abilities
    local buttonSize = Constants.NodeSize;
    local gapH = Constants.NodeGap;

    local offsetX = 0;
    local offsetY = headerHeight;

    local numEntries;
    ACTIVE_TRACK_INDEX = DataProvider:GetActiveArtifactTrackIndex();

    local cardWidth = Constants.CardWidth * Constants.CardScale;
    local cardHeight = Constants.CardHeight * Constants.CardScale;
    local cardGap = 8;

    for index, trackData in ipairs(DataProvider.ArtifactTracks) do
        numEntries = #trackData;
        offsetX = 226;
        offsetY =  (1 - index) * (cardHeight + cardGap);

        local card = CreateFrame("Frame", nil, f, "PlumberLegionRemixCardTemplate");
        TrackCards[index] = card;
        API.Mixin(card, TrackCardMixin);
        card:SetPoint("TOP", f, "TOP", 0, offsetY);
        card:SetFrameLevel(baseFrameLevel - index);
        card.trackIndex = index;
        card.TraitNodes = {};
        card:OnLoad();

        for i, nodeID in ipairs(trackData) do
            local button = CreateFrame("Button", nil, card, "PlumberLegionRemixNodeTemplate");
            API.Mixin(button, NodeButtonMixin);
            button.trackIndex = index;
            button.parentCard = card;
            button:OnLoad();
            table.insert(card.TraitNodes, button);
            button:SetPoint("LEFT", card, "LEFT", offsetX, 0);

            if i == 1 then
                button.Title = card.Title;
                button.isArtifactAbility = true;
                button:RegisterForDrag("LeftButton");
                --local x = card.Div:GetCenter();
                --print(card:GetLeft() - x);
            end

            local entryID = DataProvider:GetNodeEntryID(nodeID);
            if type(entryID) == "table" then
                button:SetNodeChoices(nodeID, entryID);
            else
                button:SetData(nodeID, entryID);
            end

            offsetX = offsetX + buttonSize + gapH;

            if i ~= numEntries then
                local arrow = CreateFrame("Button", nil, card, "PlumberLegionRemixThreeArrowsTemplate");
                API.Mixin(arrow, ArrowsButtonMixin);
                arrow.trackIndex = index;
                table.insert(card.TraitNodes, arrow);
                arrow:OnLoad();
                arrow:SetData(nodeID);
                arrow:SetPoint("LEFT", card, "LEFT", offsetX, 0);
                arrow.parentCard = card;
                offsetX = offsetX + Constants.ThreeArrowsSize + gapH;
            end
        end
    end


    local ActivateButton = addon.LandingPageUtil.CreateRedButton(f);
    ActivateButton:Hide();
    f.ActivateButton = ActivateButton;
    API.Mixin(ActivateButton, ActivateButtonMixin);
    ActivateButton:OnLoad();


    local TooltipFrame = RemixAPI.GetTraitTooltipFrame(f);
    f.TooltipFrame = TooltipFrame;


    local HeaderFrame = CreateFrame("Frame", nil, f);
    f.HeaderFrame = HeaderFrame;
    HeaderFrame:SetSize(Constants.HeaderWidth * Constants.CardScale, Constants.HeaderHeight * Constants.CardScale);
    HeaderFrame:SetPoint("BOTTOM", f, "TOP", 0, -4);

    HeaderFrame.Background = HeaderFrame:CreateTexture(nil, "BACKGROUND");
    HeaderFrame.Background:SetSize(Constants.HeaderBGWidth * Constants.CardScale, Constants.HeaderBGHeight * Constants.CardScale);
    HeaderFrame.Background:SetPoint("CENTER", HeaderFrame, "CENTER", 0, 0);
    HeaderFrame.Background:SetTexture(TEXTURE_FILE);
    HeaderFrame.Background:SetTexCoord(0/1024, 512/1024, 576/1024, 768/1024);
    HeaderFrame.Background:SetAlpha(1);

    HeaderFrame.Text1 = HeaderFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal");
    HeaderFrame.Text1:SetPoint("BOTTOM", HeaderFrame, "CENTER", 0, 4);
    HeaderFrame.Text1:SetJustifyH("CENTER");
    HeaderFrame.Text2 = HeaderFrame:CreateFontString(nil, "OVERLAY", "ObjectiveTrackerHeaderFont");
    HeaderFrame.Text2:SetPoint("TOP", HeaderFrame, "CENTER", 0, 0);
    HeaderFrame.Text2:SetJustifyH("CENTER");

    HeaderFrame.MouseoverFrame = CreateFrame("Frame", nil, HeaderFrame);
    HeaderFrame.MouseoverFrame:SetPoint("CENTER", HeaderFrame, "CENTER", 0, 2);
    HeaderFrame.MouseoverFrame:SetSize(4 * buttonSize, buttonSize);
    HeaderFrame.MouseoverFrame:SetScript("OnEnter", function()
        MainFrame:ShowHeaderTooltip();
    end);
    HeaderFrame.MouseoverFrame:SetScript("OnLeave", function()
        MainFrame:HideTooltip();
    end);


    local function CreateTextBackground(parent, fontString, height, extensionX)
        local bg = parent:CreateTexture(nil, "BACKGROUND", nil, 2);
        bg:SetTexture("Interface/AddOns/Plumber/Art/Frame/NameplateTextShadow");
        bg:SetTextureSliceMargins(40, 24, 40, 24);
        bg:SetTextureSliceMode(0);
        bg:SetHeight(height);
        local offsetY = 0;
        bg:SetPoint("LEFT", fontString, "LEFT", -extensionX, offsetY);
        bg:SetPoint("RIGHT", fontString, "RIGHT", extensionX, offsetY);
        bg:SetAlpha(0.5);
        return bg
    end
    HeaderFrame.Text1.Background = CreateTextBackground(HeaderFrame, HeaderFrame.Text1, 46, 18);
    HeaderFrame.Text2.Background = CreateTextBackground(HeaderFrame, HeaderFrame.Text2, 54, 20);


    local CardBackground = f:CreateTexture(nil, "BACKGROUND", nil, 1);
    CardBackground:SetPoint("CENTER", TrackCards[3], "CENTER", 0, 0);
    CardBackground:SetTexture(TEXTURE_FILE);
    CardBackground:SetTexCoord(704/1024, 1, 768/1024, 1);
    CardBackground:SetAlpha(0.6);
    local s = 2.1;
    CardBackground:SetSize(320 * s, 256 * s);


    local CloseButton = CreateFrame("Button", nil, f);
    local scale = 1.2;
    CloseButton:SetSize(24*scale, 24*scale);
    CloseButton:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, 48);
    CloseButton.Background = CloseButton:CreateTexture(nil, "BACKGROUND");
    CloseButton.Background:SetSize(64*scale, 48*scale);
    CloseButton.Background:SetPoint("CENTER", CloseButton, "CENTER", 0, 0);
    CloseButton.Background:SetTexture(TEXTURE_FILE);
    CloseButton.Background:SetTexCoord(512/1024, 640/1024, 576/1024, 672/1024);

    local NormalTexture = CloseButton:CreateTexture(nil, "OVERLAY");
    CloseButton:SetNormalTexture(NormalTexture);
    NormalTexture:SetTexture(TEXTURE_FILE);
    NormalTexture:SetTexCoord(640/1024, 688/1024, 576/1024, 624/1024);

    local PushedTexture = CloseButton:CreateTexture(nil, "OVERLAY");
    CloseButton:SetPushedTexture(PushedTexture);
    PushedTexture:SetTexture(TEXTURE_FILE);
    PushedTexture:SetTexCoord(640/1024, 688/1024, 624/1024, 672/1024);

    local HighlightTexture = CloseButton:CreateTexture(nil, "OVERLAY");
    CloseButton:SetHighlightTexture(HighlightTexture);
    HighlightTexture:SetTexture(TEXTURE_FILE);
    HighlightTexture:SetTexCoord(688/1024, 736/1024, 576/1024, 624/1024);
    HighlightTexture:SetBlendMode("ADD");

    CloseButton:SetScript("OnClick", function()
        MainFrame:Hide();
    end);


    local height = 5 * (cardHeight + cardGap) - cardGap;
    f:SetSize(cardWidth, height);
    f:SetScript("OnHide", f.OnHide);
    f:SetScript("OnShow", f.OnShow);


    --ConsolePort Compatibility
    if ConsolePort and ConsolePort.AddInterfaceCursorFrame and ConsolePort.GetCursorNode then
        GAMEPAD_MODE = true;
        f.NodeFocusSolver:SetGamePadMode(true);

        --Create hidden buttons for cusor snapping

        local function HiddenButton_OnEnter(self)
            if self.parentCard:IsVisible() then
                self.parentCard:OnEnter();
                if ActivateButton:IsShown() and ActivateButton:IsEnabled() then
                    local forceFocus = true;
                    ActivateButton:UpdateVisual(forceFocus);
                end
            end
        end

        local function HiddenButton_OnClick(self, button)
            if button == "RightButton" then
                MainFrame:Hide();
                return
            end
            if self.parentCard:IsVisible() then
                ActivateButton:Update();
                if ActivateButton:IsShown() and ActivateButton:IsEnabled() and button == "LeftButton" then
                    ActivateButton:OnClick();
                end
            end
        end

        for i, card in ipairs(TrackCards) do
            local HiddenButton = CreateFrame("Button", nil, card);
            card.HiddenButton = HiddenButton;
            HiddenButton.trackIndex = i;
            HiddenButton.parentCard = card;
            HiddenButton:SetUsingParentLevel(true);
            HiddenButton:SetSize(128, 24);
            HiddenButton:SetPoint("TOP", card, "LEFT", 104, -2);
            HiddenButton:SetScript("OnEnter", HiddenButton_OnEnter);
            HiddenButton:SetScript("OnClick", HiddenButton_OnClick);
        end

        local function SetIgnoreOject(object)
            object:SetAttribute("nodeignore", true);
        end

        SetIgnoreOject(HeaderFrame.MouseoverFrame);
        SetIgnoreOject(ActivateButton);

        ConsolePort:AddInterfaceCursorFrame(CloseButton);
    end

    --GW2 UI Compatibility
    if GwDressingRoom then
        f:SetFrameStrata("HIGH");
    end
end

local function ShowArtifactUI()
    if not MainFrame then
        CreateMainUI();
        MainFrame:Hide();
    end
    MainFrame:ResetParent();
    MainFrame:Show();
end
RemixAPI.ShowArtifactUI = ShowArtifactUI;


local function ToggleArtifactUI()
    if not MainFrame then
        CreateMainUI();
        MainFrame:Hide();
    end

    local state = not MainFrame:IsShown();

    if state then
        if Narci_Attribute and Narci_Attribute:IsVisible() then
            MainFrame:SetParent(Narci_Attribute);
        else
            MainFrame:ResetParent();
        end
    else
        MainFrame:ResetParent();
    end

    MainFrame:SetShown(state);
end
RemixAPI.ToggleArtifactUI = ToggleArtifactUI;
_G.Plumber_ToggleArtifactUI = ToggleArtifactUI; --Globals
_G.Plumber_ToggleArtifactUIForNarcissus = ToggleArtifactUI; --For legacy version