local _, addon = ...
local StatCardController = addon.StatCardController;
local GetCategoryNumAchievements = addon.GetCustomCategoryNumAchievements;
local IsBossCard = addon.IsBossCard;
local GetStatisticInfo = addon.GetStatisticInfo;
local DataProvider = addon.DataProvider;
local PinUtil = addon.PinUtil;

--Constant
local NUM_ACHIEVEMENT_CARDS = 8;
local LEGACY_ID = 15234;
local FEAT_OF_STRENGTH_ID = 81;
--local GUILD_FEAT_OF_STRENGTH_ID = 15093;
--local GUILD_CATEGORY_ID = 15076;

local sin = math.sin;
local cos = math.cos;
local abs = math.abs;
local min = math.min;
local max = math.max;
local sqrt = math.sqrt;
local pow = math.pow;
local pi = math.pi;
local floor = math.floor;
local ceil = math.ceil;
local After = C_Timer.After;
local gsub = string.gsub;
local bband = bit.band;
local format = string.format;
local tremove = table.remove;
local tinsert = table.insert;

local GetAchievementNumCriteria = GetAchievementNumCriteria;
local GetAchievementCriteriaInfo = GetAchievementCriteriaInfo;
local GetRewardItemID = C_AchievementInfo.GetRewardItemID;
local GetPreviousAchievement = GetPreviousAchievement;
local GetNextAchievement = GetNextAchievement;
local SetFocusedAchievement = SetFocusedAchievement;    --Requset guild achievement progress from server, will fire "CRITERIA_UPDATE" after calling GetAchievementCriteriaInfo()
local FadeFrame = NarciFadeUI.Fade;
local GetParentAchievementID = NarciAPI.GetParentAchievementID;
local L = Narci.L;

local function linear(t, b, e, d)
	return (e - b) * t / d + b
end

local function outSine(t, b, e, d)
	return (e - b) * sin(t / d * (pi / 2)) + b
end

local function inOutSine(t, b, e, d)
	return (b - e) / 2 * (cos(pi * t / d) - 1) + b
end

local function outQuart(t, b, e, d)
    t = t / d - 1;
    return (b - e) * (pow(t, 4) - 1) + b
end

addon.outQuart = outQuart;

--FormatShortDate Derivated from FormatShortDate (Util.lua)
local FormatDate;
if LOCALE_enGB then
    function FormatDate(day, month, year, twoRowMode)
        if (year) then
            if twoRowMode then
                return format("%1$d/%2$d\n20%3$02d", day, month, year);
            else
                return format("%1$d/%2$d/%3$02d", day, month, year);
            end
        else
            return format("%1$d/%2$d", day, month);
        end
    end
else
    function FormatDate(day, month, year, twoRowMode)
        if (year) then
            if twoRowMode then
                return format("%2$d/%1$02d\n20%3$02d", day, month, year);
            else
                return format("%2$d/%1$02d/%3$02d", day, month, year);
            end
        else
            return format("%2$d/%1$02d", day, month);
        end
    end
end

local themeID = 0;
local showNotEarnedMark = false;
local isDarkTheme = true;
local isGuildView = false;
local texturePrefix = "Interface\\AddOns\\Narcissus_Achievements\\Art\\DarkWood\\";

local function ReskinButton(button)
    --if true then return end
    button.border:SetTexture(texturePrefix.."AchievementCardBorder");
    button.background:SetTexture(texturePrefix.."AchievementCardBackground");
    button.bottom:SetTexture(texturePrefix.."AchievementCardBackground");
    button.lion:SetTexture(texturePrefix.."Lion");
    button.mask:SetTexture(texturePrefix.."AchievementCardBorderMask");
    local isDarkTheme = isDarkTheme;
    button.RewardFrame.background:SetShown(not isDarkTheme);
    button.RewardFrame.rewardNodeLeft:SetShown(isDarkTheme);
    button.RewardFrame.rewardNodeRight:SetShown(isDarkTheme);
    button.RewardFrame.rewardLineLeft:SetShown(isDarkTheme);
    button.RewardFrame.rewardLineRight:SetShown(isDarkTheme);
    button.RewardFrame.reward:SetPoint("BOTTOM", button.RewardFrame, "BOTTOM", 0, (isDarkTheme and 3) or 1);

    if isDarkTheme then
        button.description:SetFontObject(NarciAchievementText);
    else
        button.description:SetFontObject(NarciAchievementTextBlack);
    end
    button.isDarkTheme = isDarkTheme;

    --Reposition Elements
    button.icon:ClearAllPoints();
    button.lion:ClearAllPoints();
    button.date:ClearAllPoints();
    button.NotEarned:ClearAllPoints();
    if showNotEarnedMark then
        button.NotEarned:SetWidth(20);
    else
        button.NotEarned:SetWidth(0.1);
    end
    if themeID == 3 then
        button.icon:SetPoint("CENTER", button.border, "LEFT", 32, 0);
        button.lion:SetPoint("CENTER", button.border, "RIGHT", -28, -1);
        button.date:SetPoint("RIGHT", button.border, "TOPRIGHT", -54, -25);
        button.NotEarned:SetPoint("TOPLEFT", button.icon, "TOPRIGHT", 6, -2);
    else
        button.icon:SetPoint("CENTER", button.border, "LEFT", 27, 4);
        button.lion:SetPoint("CENTER", button.border, "RIGHT", -22, 3);
        button.date:SetPoint("RIGHT", button.border, "TOPRIGHT", -48, -25);
        button.NotEarned:SetPoint("TOPLEFT", button.icon, "TOPRIGHT", 7, -6.5);
    end

    button.isDark = nil;
end


local MainFrame, InspectionFrame, MountPreview, Tooltip, ReturnButton, SummaryButton, GoToCategoryButton, FilterButton;
local CategoryContainer, AchievementContainer, DIYContainer, EditorContainer, SummaryFrame, AchievementCards, TabButtons;
local UpdateSummaryFrame;

local TabUtil = {};
addon.TabUtil = TabUtil;

local CategoryButtons = {
    player = { parentButtons = {}, buttons = {}, },
    guild = { parentButtons = {}, buttons = {}, },
    stats = { parentButtons = {}, buttons = {}, },
};

local IS_STAT_CATEGORY = {
    [-2] = true,    --Used to show pinned statistics
};

function CategoryButtons:GetActiveParentButtons(tabID)
    tabID = tabID or TabUtil.tabID;
    if tabID == 1 then
        return self.player.parentButtons;
    elseif tabID == 2 then
        return self.guild.parentButtons;
    elseif tabID == 3 then
        return self.stats.parentButtons;
    end
end

local CategoryStructure = {
    player = {},
    guild = {},
    stats = {},
};

local function IsAccountWide(flags)
    --ACHIEVEMENT_FLAGS_ACCOUNT
    if flags then
        return bband(flags, 131072) == 131072
    else
        return false
    end
end

addon.IsAccountWide = IsAccountWide;


local ScrollUtil = {};      --DataProvider for ScrollFrame

--Limit the request frequency
local processor = CreateFrame("Frame");
processor:Hide();
processor:SetScript("OnUpdate", function(self, elapsed)
    local processComplete;
    self.cycle = self.cycle + 1;
    if self.func then
        self.arg2, processComplete = self.func(self.arg1, self.arg2);
        if processComplete then
            self:Hide();
            self.func = nil;
            self.callback();
            if self.cycle == 2 then
                ScrollUtil:UpdateScrollChild(0);
            end
        end
        if self.cycle == 3 then
            ScrollUtil:UpdateScrollChild(0);
        end
    else
        self:Hide();
    end
end)

function processor:Start()
    local processComplete;
    if self.func then
        self.cycle = 1;
        self.arg2, processComplete = self.func(self.arg1, self.arg2);
        if processComplete then
            self:Hide();
            self.func = nil;
            self.callback();

            --Achievement Card Data is being constructed by OnUpdate script
            --The step is 4: Meaning it takes 2 frames to build the visible area
            --Update scrollframe on the 3rd frame
            ScrollUtil:UpdateScrollChild(0);
        else
            self:Show();
        end
    else
        self:Hide();
    end
end

------------------------------------------------------------------------------------------------------
local function HideItemPreview()
    GameTooltip:Hide();
    if MountPreview:IsShown() then
        MountPreview:FadeOut();
    end
    MountPreview:ClearCallback();
end


local animFlyIn = NarciAPI_CreateAnimationFrame(0.45);
local animFlyOut = NarciAPI_CreateAnimationFrame(0.25);

animFlyIn:SetScript("OnUpdate", function(self, elapsed)
    self.total = self.total + elapsed;
    local alpha = outQuart(self.total, self.fromAlpha, 1, self.duration);
    local scale = outQuart(self.total, 0.5, 1, 0.2);

    if self.total >= 0.2 then
        scale = 1;
        local textAlpha = outQuart(self.total - 0.2, 0, 1, 0.2);
        self.header:SetAlpha(textAlpha);
        self.description:SetAlpha(textAlpha);
        self.date:SetAlpha(textAlpha);
        self.reward:SetAlpha(textAlpha);
    end
    
    if self.total >= self.duration then
        alpha = 1;
        --offsetX = 0;
        --offsetY = 0;
        scale = 1;
        self.header:SetAlpha(1);
        self.description:SetAlpha(1);
        self.date:SetAlpha(1);
        self.reward:SetAlpha(1);
        self:Hide();
    end
    self.background:SetAlpha(alpha);
    self.ObjectiveFrame:SetAlpha(alpha);
    self.ChainFrame:SetAlpha(alpha);
    self.Card:SetScale(scale);
end)


function animFlyIn:Play()
    self:Hide();
    self.Card:SetAlpha(1);
    self.header:SetAlpha(0);
    self.description:SetAlpha(0);
    self.date:SetAlpha(0);
    self.reward:SetAlpha(0);
    self.fromAlpha = self.background:GetAlpha();
    animFlyOut:Hide();
    self:Show();
end


animFlyOut:SetScript("OnUpdate", function(self, elapsed)
    self.total = self.total + elapsed;
    local offsetX = inOutSine(self.total, self.fromX, self.toX, self.duration);
    local offsetY = outSine(self.total, self.fromY, self.toY, self.duration);
    local alpha = outQuart(self.total, 1, 0, self.duration);
    if self.total >= self.duration then
        offsetX = self.toX;
        offsetY = self.toY;
        alpha = 0;
        InspectionFrame:Hide();
        if self.button then
            self.button:Show();
        end
        self.noTranslation = nil;
        self:Hide();
    end
    self.background:SetAlpha(alpha);
    self.ObjectiveFrame:SetAlpha(alpha);
    self.ChainFrame:SetAlpha(alpha);
    if self.noTranslation then
        self.Card:SetAlpha(alpha);
    else
        self.Card:SetPoint("BOTTOM", InspectionFrame, "CENTER", offsetX, offsetY);
    end
end)

function animFlyOut:Play()
    animFlyIn:Hide();
    self:Hide();
    if self.button then
        --achievement button in the scrollframe
        self.button:Hide();
    end
    local _;
    _, _, _, self.fromX, self.fromY = self.Card:GetPoint();
    self:Show();
    HideItemPreview();
    InspectionFrame.isTransiting = true;
    InspectionFrame.HotkeyShiftClick:Hide();
    InspectionFrame.HotkeyMouseWheel:Hide();
    InspectionFrame.GetLink:Hide();
    InspectionFrame.GoToCategoryButton:Hide();
    InspectionFrame.Card.ParentAchievmentButton:Hide();
    InspectionFrame.PrevButton:Hide();
    InspectionFrame.NextButton:Hide();
end

------------------------------------------------------------------------------------------------------
local function DisplayProgress(id, flags)
    local cData, iData = {}, {};
    cData.names, iData.names = {}, {};
    cData.icons, iData.icons = {}, {};
    cData.assetIDs, iData.assetIDs = {}, {};
    cData.bars, iData.bars = {}, {};

    local numCompleted, numIncomplete = 0, 0;
    --if ( not ( bit.band(flags, 128) == 128 ) ) then   --ACHIEVEMENT_FLAGS_HAS_PROGRESS_BAR = 128!!
        local numCriteria =  GetAchievementNumCriteria(id);
        if numCriteria == 0 then
            numCompleted = 0;
            numIncomplete = 0;
        else
            local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString;
            for i = 1, numCriteria do
                criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString = GetAchievementCriteriaInfo(id, i);
                --print("criteriaType: "..criteriaType)
                if ( bband(flags, 1) == 1 ) then  --EVALUATION_TREE_FLAG_PROGRESS_BAR = 1
                    if ( completed == false ) then
                        numIncomplete = numIncomplete + 1;
                        tinsert(iData.bars, {quantity, reqQuantity, criteriaString});
                    else
                        numCompleted = numCompleted + 1;
                        tinsert(cData.bars, {quantity, reqQuantity, criteriaString});
                    end
                else
                    if ( completed == false ) then
                        numIncomplete = numIncomplete + 1;
                        criteriaString = "|CFF808080" .. criteriaString .. "|r";
                        tinsert(iData.names, criteriaString);
                        if criteriaType == 8 and assetID then  --CRITERIA_TYPE_ACHIEVEMENT
                            local icon = DataProvider:GetAchievementInfo(assetID, 10);
                            iData.icons[numIncomplete] = icon;
                            iData.assetIDs[numIncomplete] = assetID;
                        end
                    else
                        numCompleted = numCompleted + 1;
                        criteriaString = "|CFF5fbb46" .. criteriaString .. "|r"; --00FF00
                        tinsert(cData.names, criteriaString);
                        if criteriaType == 8 and assetID then  --CRITERIA_TYPE_ACHIEVEMENT
                            local icon = DataProvider:GetAchievementInfo(assetID, 10);
                            cData.icons[numCompleted] = icon;
                            cData.assetIDs[numCompleted] = assetID;
                        end
                    end
                end
            end
        end
    --end
    
    cData.count = numCompleted;
    iData.count = numIncomplete;

    InspectionFrame:DisplayCriteria(cData, iData);
end


local InspectCard;  --function

local function ShutInspection()
    InspectionFrame:Hide();
end

local function ToggleTracking(id)
    if not id then return end;

    if DataProvider:IsTrackedAchievement(id) then
        RemoveTrackedAchievement(id);
    else
        local MAX_TRACKED_ACHIEVEMENTS = 10;
        if ( DataProvider.numTrackedAchievements >= MAX_TRACKED_ACHIEVEMENTS ) then
            UIErrorsFrame:AddMessage(format(ACHIEVEMENT_WATCH_TOO_MANY, MAX_TRACKED_ACHIEVEMENTS), 1.0, 0.1, 0.1, 1.0);
            return;
        end

        local _, _, _, completed, _, _, _, _, _, _, _, isGuild, wasEarnedByMe = DataProvider:GetAchievementInfo(id);
        if ( (completed and isGuild) or wasEarnedByMe ) then
            UIErrorsFrame:AddMessage(ERR_ACHIEVEMENT_WATCH_COMPLETED, 1.0, 0.1, 0.1, 1.0);
            return;
        end
    
        AddTrackedAchievement(id);
        return true
    end
end

local function ProcessModifiedClick(button)
    local achievementID = button.id;
    if not achievementID then return true end

    local isModifiedClick = IsModifiedClick();
	if isModifiedClick then
		local handled = nil;
		if ( IsModifiedClick("CHATLINK") ) then
			local achievementLink = GetAchievementLink(achievementID);
			if ( achievementLink ) then
				handled = ChatEdit_InsertLink(achievementLink);
				if ( not handled and SocialPostFrame and Social_IsShown() ) then
					Social_InsertLink(achievementLink);
					handled = true;
				end
			end
		end
		if ( not handled and IsModifiedClick("QUESTWATCHTOGGLE") ) then
            local isTracking = ToggleTracking(achievementID);
            button.trackIcon:SetShown(isTracking);
        end
    end
    return isModifiedClick
end

local function AchievementCard_OnClick(self)
    if not ProcessModifiedClick(self) then
        InspectCard(self, true);
    end
end

local function FormatRewardText(id, rewardText)
    if isDarkTheme then
        local itemID = GetRewardItemID(id);
        if itemID then
            local itemID, itemType, itemSubType, _, icon, itemClassID, itemSubClassID = GetItemInfoInstant(itemID);
            if itemSubType == "Mount" then
                rewardText = gsub(rewardText, ".+:(.+)", "|cff808080".. "Mount:" .."|r|cff8950c6".."%1".."|r");
            elseif itemSubType == "Companion Pets" then
                rewardText = gsub(rewardText, ".+:(.+)", "|cff808080".. "Pet:" .."|r|cfff2b344".."%1".."|r");
            else
                rewardText = "|cffa3d39c"..rewardText.."|r";
            end
            return rewardText, itemID
        else
            return ("|cffa3d39c"..rewardText.."|r");      --Pastel Yellow Green
        end
    else
        return ("|cffffd200"..rewardText.."|r");
    end
end

local function GetProgressivePoints(achievementID, basePoints)
	local points;
    local _, progressivePoints, completed
    if basePoints then
        progressivePoints = basePoints;
    else
        _, _, progressivePoints, completed = DataProvider:GetAchievementInfo(achievementID);
    end
    achievementID = GetPreviousAchievement(achievementID);
	while achievementID do
		_, _, points, completed = DataProvider:GetAchievementInfo(achievementID);
        progressivePoints = progressivePoints + points;
        achievementID = GetPreviousAchievement(achievementID);
	end

	if ( progressivePoints ) then
		return progressivePoints;
	else
		return 0;
	end
end


local function FormatAchievementCard(button, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe)
    if not button then
        return
    end
    local headerObject, numLines, textHeight;

    button.id = id;
    button.trackIcon:SetShown( DataProvider:IsTrackedAchievement(id) );
    if ( not completed or ( not isGuild and not wasEarnedByMe ) ) and (showNotEarnedMark) then
        button.NotEarned:Show();
    else
        button.NotEarned:Hide();
    end

    --for long text
    button.header:SetText(name);
    if button.header:IsTruncated() then
        headerObject = button.headerLong;
        headerObject:SetText(name);
        button.header:Hide();
    else
        headerObject = button.header;
        button.headerLong:Hide();
    end
    headerObject:Show();

    if IsAccountWide(flags) then
        if completed then
            if isDarkTheme then
                headerObject:SetTextColor(0.427, 0.812, 0.965); --(0.427, 0.812, 0.965)(0.4, 0.755, 0.9)
            else
                headerObject:SetTextColor(1, 1, 1);
            end
        else
            if isDarkTheme then
                headerObject:SetTextColor(0.214, 0.406, 0.484);
            else
                headerObject:SetTextColor(0.5, 0.5, 0.5);
            end
        end
    else
        if completed then
            if isDarkTheme then
                headerObject:SetTextColor(0.9, 0.82, 0.58);  --(1, 0.91, 0.647); --(0.9, 0.82, 0.58) --(0.851, 0.774, 0.55)
            else
                headerObject:SetTextColor(1, 1, 1);
            end
        else
            if isDarkTheme then
                headerObject:SetTextColor(0.5, 0.46, 0.324);
            else
                headerObject:SetTextColor(0.5, 0.5, 0.5);
            end
        end
    end

    points = GetProgressivePoints(id, points);
    if points == 0 then
        button.points:SetText("");
        button.lion:Show();
    else
        if points >= 100 then
            if not button.useSmallPoints then
                button.useSmallPoints = true;
                button.points:SetFontObject(NarciAchievemtPointsSmall);
            end
        else
            if button.useSmallPoints then
                button.useSmallPoints = nil;
                button.points:SetFontObject(NarciAchievemtPoints);
            end
        end
        button.points:SetText(points);
        button.lion:Hide();
    end

    button.icon:SetTexture(icon);

    local rewardHeight;
    local shadowHeight = 0;
    if rewardText and rewardText ~= "" then
        local itemID;
        rewardHeight = 22;
        rewardText, itemID = FormatRewardText(id, rewardText);
        button.RewardFrame.reward:SetText(rewardText);
        button.RewardFrame.itemID = itemID;
        button.itemID = itemID;
        button.RewardFrame:Show();
    else
        if isDarkTheme then
            rewardHeight = 2;
        else
            rewardHeight = 8;
        end
        button.RewardFrame:Hide();
        button.RewardFrame:SetHeight(2);
    end
    button.RewardFrame:SetHeight(rewardHeight);
    button.description:SetHeight(0);
    button.description:SetText(description);
    textHeight = floor( button.background:GetHeight() + 0.5 );
    local descriptionHeight = button.description:GetHeight();
    button.description:SetHeight(descriptionHeight + 2)
    numLines = ceil( descriptionHeight / 14 - 0.1 );
    button:SetHeight(72 + rewardHeight + 14*(numLines - 1) );
    button.shadow:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 12, - 6 - numLines * 6 - shadowHeight);

    if IsAccountWide(flags) then     --ACHIEVEMENT_FLAGS_ACCOUNT
        if button.accountWide ~= true then
            button.accountWide = true;
            button.border:SetTexCoord(0.05078125, 0.94921875, 0.5, 1);
            button.bottom:SetTexCoord(0.05078125, 0.94921875, 0.985, 1);
        end
        if textHeight <= 288 then
            button.background:SetTexCoord(0.05078125, 0.94921875, 0.985 - textHeight/288/2, 0.985);
        else
            button.background:SetTexCoord(0.05078125, 0.94921875,  0.5, 0.985);
        end
    else
        if button.accountWide then
            button.accountWide = nil;
            button.border:SetTexCoord(0.05078125, 0.94921875, 0, 0.5);
            button.bottom:SetTexCoord(0.05078125, 0.94921875, 0.485, 0.5);
        end
        if textHeight <= 288 then
            button.background:SetTexCoord(0.05078125, 0.94921875, 0.485 - textHeight/288/2, 0.485);
        else
            button.background:SetTexCoord(0.05078125, 0.94921875,  0, 0.485);
        end
    end

    if completed then
        button.date:SetText( FormatDate(day, month, year) );
        button.RewardFrame.reward:SetAlpha(1);

        if (button.isDark == nil) or (button.isDark) then
            button.isDark = false;
            button.icon:SetDesaturated(false);
            button.points:SetTextColor(0.8, 0.8, 0.8);
            if isDarkTheme then
                button.description:SetTextColor(0.72, 0.72, 0.72);
            else
                button.description:SetTextColor(0, 0, 0);
            end
            button.icon:SetVertexColor(1, 1, 1);
            button.lion:SetVertexColor(1, 1, 1);
            button.border:SetVertexColor(1, 1, 1);
            button.background:SetVertexColor(1, 1, 1);
            button.bottom:SetVertexColor(1, 1, 1);
            button.border:SetDesaturated(false);
            button.background:SetDesaturated(false);
            button.bottom:SetDesaturated(false);
        end
    else
        button.date:SetText("");
        button.RewardFrame.reward:SetAlpha(0.60);

        if (button.isDark == nil) or (not button.isDark) then
            button.isDark = true;
            button.icon:SetDesaturated(true);
            button.points:SetTextColor(0.6, 0.6, 0.6);
            if isDarkTheme then
                button.description:SetTextColor(0.6, 0.6, 0.6);
            else
                button.description:SetTextColor(0, 0, 0);
            end
            button.icon:SetVertexColor(0.60, 0.60, 0.60);
            button.lion:SetVertexColor(0.60, 0.60, 0.60);
            button.border:SetVertexColor(0.60, 0.60, 0.60);
            button.background:SetVertexColor(0.72, 0.72, 0.72);
            button.bottom:SetVertexColor(0.72, 0.72, 0.72);
            button.border:SetDesaturation(0.6);
            button.background:SetDesaturation(0.6);
            button.bottom:SetDesaturation(0.6);
        end
    end
    
    button:SetShown(id);
end

local function FormatAchievementCardByIndex(buttonIndex, ...)
    local button = AchievementCards[buttonIndex];
    if button then
        FormatAchievementCard(button, ...);
        if buttonIndex > 7 or buttonIndex < 0 then
            button:SetAlpha(1);
        else
            button.toAlpha = 1;     --for flip animation
        end
    end
end

local function InspectAchievement(achievementID)
    if not achievementID then return end;
    
    local id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe = DataProvider:GetAchievementInfo(achievementID);
    local displayCard;
    if DataProvider:IsStatistic(achievementID) then
        displayCard = InspectionFrame.StatCard;
        InspectionFrame.Card:Hide();
        displayCard:SetData(achievementID);
        displayCard:Show();
        displayCard:SetAlpha(1);
        displayCard:ClearAllPoints();
        displayCard:SetPoint("BOTTOM", InspectionFrame, "CENTER", 0, 36);
    else
        displayCard = InspectionFrame.Card;
        InspectionFrame.StatCard:Hide();
        FormatAchievementCardByIndex(-1, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe);
    end
    animFlyOut.Card = displayCard;

    DisplayProgress(id, flags);
    InspectionFrame.currentAchievementID = id;
    InspectionFrame.currentAchievementName = name;
    InspectionFrame:Show();
    InspectionFrame.HotkeyShiftClick:Show();
    InspectionFrame.GetLink:Show();

    local itemID = GetRewardItemID(id);
    if itemID then
        MountPreview:SetItem(itemID);
    else
        HideItemPreview();
    end

    InspectionFrame:UpdateChain(id, completed);
    InspectionFrame:FindParentAchievementID(id);
    GoToCategoryButton:SetAchievement(id, isGuild);

    return completed, displayCard:GetHeight()/2
end

local function FormatStatCard(buttonIndex, id)
    local button = StatCardController:Accquire(buttonIndex);
    if button then
        button:SetData(id);
    end
end

ScrollUtil.heightData = {};
ScrollUtil.achievementID = {};
ScrollUtil.totalHeight = 0;
ScrollUtil.positionToButton = {};
ScrollUtil.formatFunc = FormatAchievementCardByIndex;

function ScrollUtil:ResetHeights()
    self.totalHeight = 18;
    self.position = 1;
    self.lastOffset = 0;
    self.nextOffset = 0;
    wipe(self.heightData);
end

function ScrollUtil:GetScrollRange()
    local range = self.totalHeight - (AchievementContainer:GetHeight() or 0) + 18;
    if range < 0 then
        range = 0;
    end
    return range;
end

function ScrollUtil:SetCardData(cardIndex, achievementID, description, rewardText)
    local rewardHeight;
    if rewardText and rewardText ~= "" then
        rewardHeight = 22;
    else
        if isDarkTheme then
            rewardHeight = 2;
        else
            rewardHeight = 8;
        end
    end
    self.textReference:SetText("");
    self.textReference:SetHeight(0);
    self.textReference:SetText(description);
    local descriptionHeight = self.textReference:GetHeight();
    local numLines = ceil( descriptionHeight / 14 - 0.1 );
    local buttonHeight = 72 + rewardHeight + 14*(numLines - 1) + 4;     --4 is the Gap

    self.heightData[cardIndex] = self.totalHeight;
    self.achievementID[cardIndex] = achievementID;
    self.totalHeight = self.totalHeight + buttonHeight;

    return buttonHeight;
end

function ScrollUtil:SetStatCardData(cardIndex, achievementID)
    local buttonHeight;
    if achievementID < 0 then
        buttonHeight = 48;
    else
        if IsBossCard(achievementID) then
            --boss card
            buttonHeight = 94;
        else
            buttonHeight = 72;
        end
    end

    self.heightData[cardIndex] = self.totalHeight;
    self.achievementID[cardIndex] = achievementID;
    self.totalHeight = self.totalHeight + buttonHeight;

    return buttonHeight;
end

function ScrollUtil:GetTopButtonIndex(scrollOffset)
    if scrollOffset > self.nextOffset then
        local p = self.position + 1;
        if self.heightData[p] then
            self.position = p;
            self.lastOffset = self.heightData[p];
            self.nextOffset = self.heightData[p + 1];
            self:UpdateScrollChild(-1);
        end
    elseif scrollOffset < self.lastOffset then
        local p = self.position - 1;
        if self.heightData[p] then
            self.nextOffset = self.heightData[p + 1];
            self.lastOffset = self.heightData[p];
            if p < 1 then
                p = 1;
            end
            self.position = p;
            self:UpdateScrollChild(1);
        end
    end
end

function ScrollUtil:UpdateScrollChild(direction)
    if direction < 0 then
        local topButton = tremove(self.activeCards, 1);
        tinsert(self.activeCards, topButton);
    elseif direction > 0 then
        local bottomButton = tremove(self.activeCards);
        tinsert(self.activeCards, 1, bottomButton);
    end
    wipe(self.positionToButton);
    local p = self.position;
    local id;
    local positionIndex;
    local realID, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe;
    local card;
    for i = 1, NUM_ACHIEVEMENT_CARDS do
        card = self.activeCards[i];
        if not card then break end;
        positionIndex = p + i - 1;
        card.positionIndex = positionIndex;
        --card.TableIndex:SetText(i);
        if self.heightData[positionIndex] then
            card:SetPoint("TOP", AchievementContainer.ScrollChild, "TOP", 0, -self.heightData[positionIndex]);
            self.positionToButton[positionIndex] = card;
            id = self.achievementID[positionIndex];
            if id then
                if card.id ~= id then
                    realID, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe = DataProvider:GetAchievementInfo(id);
                    self.formatFunc(i, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe);
                    card:Show();
                else
                    card:Show();
                end
            else
                card:Hide();
            end
        else
            card:Hide();
        end
    end
end

function ScrollUtil:GetOffsetByPositionIndex(index)
    return self.heightData[index] or 0
end

function ScrollUtil:GetAchievementIDByPositionIndex(index)
    return self.achievementID[index];
end

function ScrollUtil:GetCardByPositionIndex(index)
    return self.positionToButton[index];
end

function InspectCard(button, playAnimation)    --Private
    local index = button.positionIndex;
    if not index then return end;

    local Card = InspectionFrame.Card;
    local id = button.id;
    Card:ClearAllPoints();
    Card:SetPoint("BOTTOM", InspectionFrame, "CENTER", 0, 36);

    InspectionFrame.pauseScroll = nil;
    InspectionFrame.dataIndex = index;

    local numAchievements = InspectionFrame.numAchievements;

    if index <= 1 then
        index = 1;
        InspectionFrame.PrevButton:Disable();
        InspectionFrame.NextButton:Enable();
        InspectionFrame.NextButton:Show();
    elseif index >= numAchievements then
        index = numAchievements;
        InspectionFrame.PrevButton:Enable();
        InspectionFrame.PrevButton:Show();
        InspectionFrame.NextButton:Disable();
    else
        InspectionFrame.PrevButton:Enable();
        InspectionFrame.NextButton:Enable();
        InspectionFrame.PrevButton:Show();
        InspectionFrame.NextButton:Show();
    end
    if numAchievements > 0 then
        InspectionFrame.HotkeyMouseWheel:Show();
    else
        InspectionFrame.HotkeyMouseWheel:Hide();
    end

    local completed, extraY = InspectAchievement(id);

    --Animation
    local x0, y0 = InspectionFrame:GetCenter();
    local x1, y1 = button:GetCenter();
    local offsetX = x1 - x0;
    local offsetY = y1 - y0 - extraY;

    --animFlyIn.fromX, animFlyIn.fromY = offsetX, offsetY;
    animFlyOut.toX, animFlyOut.toY = offsetX, offsetY;
    animFlyOut.button = button;
    animFlyOut.duration = max(0.2, 0.2*(sqrt(offsetX^2 + (offsetY - 36)^2))/150 );

    if playAnimation then
        InspectionFrame:SyncBlurOffset(index);
        animFlyIn:Play();
        if DataProvider:IsStatistic(id) then
            InspectionFrame.StatCard:SetAlpha(0);
            FadeFrame(InspectionFrame.StatCard, 0.2, 1);
        end
    end
end


local function Slice_UpdateAchievementCards(categoryID, startIndex)
    --from 1st complete achievement to bottom
    local slice = 4;
    local id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe;
    local processComplete = false;
    local numProcessed = 0;
    
    --print("process: "..startIndex);
    for i = startIndex, startIndex + slice do
        id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe = DataProvider:GetAchievementInfoByOrder(categoryID, i);
        if i > 0 and id then
            if i <= NUM_ACHIEVEMENT_CARDS then
                FormatAchievementCardByIndex(i, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe);
            end
            ScrollUtil:SetCardData(i, id, description, rewardText);
            numProcessed = i;
        else
            processComplete = true;
            break;
        end
    end

    return numProcessed + 1, processComplete
end

local function Slice_ReverselyUpdateAchievementCards_Callback(categoryID, startIndex)
    --from 1st complete achievement to 1st incomplete
    local slice = 4;
    local id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe;
    local processComplete = false;
    local numProcessed = 0;
    local numAchievements, numCompleted, numIncomplete = GetCategoryNumAchievements(categoryID, false);
    local index;
    for i = startIndex, startIndex + slice do
        id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe = DataProvider:GetAchievementInfoByOrder(categoryID, i);
        if i <= numCompleted then
            index = i + numIncomplete;
            if i <= NUM_ACHIEVEMENT_CARDS then
                FormatAchievementCardByIndex(index, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe);
            end
            ScrollUtil:SetCardData(index, id, description, rewardText);
            numProcessed = i;
        else
            processComplete = true;
            break;
        end
    end

    return numProcessed + 1, processComplete
end

local function Slice_ReverselyUpdateAchievementCards(categoryID, startIndex)
    --from 1st incomplete achievement to the bottom
    local slice = 4;
    local id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe;
    local processComplete = false;
    local numProcessed = 0;
    local numAchievements, numCompleted = GetCategoryNumAchievements(categoryID, false);

    --print("reverse process: "..startIndex);
    for i = startIndex, startIndex + slice do
        id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe = DataProvider:GetAchievementInfoByOrder(categoryID, numCompleted + i);
        if id then
            --print("id #"..id)
            if not completed then
                if i <= NUM_ACHIEVEMENT_CARDS then
                    FormatAchievementCardByIndex(i, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, wasEarnedByMe);
                end
                ScrollUtil:SetCardData(i, id, description, rewardText);
                numProcessed = i;
            end
        else
            --print("Break, begin forward")
            processor:Hide();
            processor.arg1 = categoryID;
            processor.arg2 = 1;     --startIndex
            processor.func = Slice_ReverselyUpdateAchievementCards_Callback  --Slice_UpdateAchievementCards;
            processor:Show();
            return 1, false
        end
    end

    return numProcessed + 1, processComplete
end

local function Slice_UpdateStatCards(categoryID, startIndex)
    --from 1st complete achievement to bottom
    local slice = 4;
    local id;
    local processComplete = false;
    local numProcessed = 0;

    for i = startIndex, startIndex + slice do
        id = GetStatisticInfo(categoryID, i);
        if i > 0 and id then
            ScrollUtil:SetStatCardData(i, id);
            numProcessed = i;
        else
            processComplete = true;
            break;
        end
    end

    return numProcessed + 1, processComplete
end

local function UpdateAchievementScrollRange()
    local scrollBar = AchievementContainer.scrollBar;
    local range = ScrollUtil:GetScrollRange();
    scrollBar:SetMinMaxValues(0, range);
    AchievementContainer.range = range;
    scrollBar:SetShown(range ~= 0);
end

processor.func = Slice_ReverselyUpdateAchievementCards  --Slice_UpdateAchievementCards;
processor.callback = UpdateAchievementScrollRange;


local animFlip = CreateFrame("Frame");

function animFlip:Play(groupIndex)
    self:Hide();
    self.t = 0;
    local objects;
    if groupIndex == 1 then
        objects = AchievementCards;
    else
        return
    end
    local numObjects = #objects;
    numObjects = min(numObjects, 6);
    for i = 1, numObjects do
        objects[i]:SetAlpha(0);
    end

    local oT, card;
    local scale, offset1, offset2, alpha;
    local fullDuration = numObjects * 0.05 + 0.4;
    self:SetScript("OnUpdate", function(f, elapsed)
        self.t = self.t + elapsed;
        for i = 1, numObjects do
            oT = self.t - 0.05 * (i - 1);
            if oT > 0 then
                if oT < 0.4 then
                    scale = outQuart(oT, 1.25, 1, 0.4);
                    offset1 = outQuart(oT, 24, 0, 0.4);
                    offset2 = outQuart(oT, -72, -48, 0.4);
                    alpha = min(1, linear(oT, 0, 1, 0.25));
                else
                    scale = 1;
                    offset1 = 0;
                    offset2 = -48;
                    alpha = 1;
                end
                card = objects[i];
                card:SetAlpha(alpha);
                card.border:SetPoint("TOP", 0, offset1);
                card.description:SetPoint("TOP", 0, offset2);
                card.points:SetScale(scale);
                card.icon:SetScale(scale);
                card.lion:SetScale(scale);
                card.border:SetScale(scale);
            end
        end
        if self.t > fullDuration then
            self:Hide();
        end
    end);
    self:Show();
end


local SORT_FUNC = Slice_ReverselyUpdateAchievementCards;
local function UpdateAchievementCardsBySlice(categoryID)
    ScrollUtil:ResetHeights();

    processor:Hide();
    AchievementContainer.scrollBar:SetValue(0);
    for i = 1, NUM_ACHIEVEMENT_CARDS do
        AchievementCards[i]:Hide();
    end

    local numAchievements, numCompleted, numIncomplete = GetCategoryNumAchievements(categoryID, false);
    DataProvider.numAchievements = numAchievements;
    processor.arg1 = categoryID;
    processor.arg2 = 1;     --fromIndex
    processor.func = SORT_FUNC;
    processor:Start();

    --animation
    if numAchievements ~= 0 then
        animFlip:Play(1);
    end
end

local function SwitchToSortMethod(index)
    if index == 2 then
        SORT_FUNC = Slice_UpdateAchievementCards;
    else
        SORT_FUNC = Slice_ReverselyUpdateAchievementCards;
    end

    local categoryID = DataProvider.currentCategory;
    if categoryID and categoryID > 0 then
        UpdateAchievementCardsBySlice(categoryID);
    end
end

local function UpdateStatCardsBySlice(categoryID)
    ScrollUtil:ResetHeights();
    processor:Hide();
    AchievementContainer.scrollBar:SetValue(0);
    local card;
    local numAchievements = GetCategoryNumAchievements(categoryID, false);
    local numCards = math.min(numAchievements, NUM_ACHIEVEMENT_CARDS);
    for i = 1, numCards do
        card = StatCardController:Accquire(i);
        card:Show();
        --card:SetStat(categoryID, i);
    end
    StatCardController:HideRest(numCards + 1);

    DataProvider.numAchievements = numAchievements;
    processor.arg1 = categoryID;
    processor.arg2 = 1;     --fromIndex
    processor.func = Slice_UpdateStatCards;
    processor:Start();

    StatCardController:PlayAnimation();
end

---------------------------------------------------------------------------------------------------
local function UpdateCategoryScrollRange()
    local button, buttons;
    local totalHeight = 0;
    local parentButtons = CategoryButtons:GetActiveParentButtons();
    if not parentButtons then return end;

    for i = 1, #parentButtons do
        button = parentButtons[i];
        if button.expanded then
            totalHeight = totalHeight + ( button.expandedHeight or 32);
        else
            totalHeight = totalHeight + 32;
        end

        totalHeight = totalHeight + 4;
    end

    local scrollBar = CategoryContainer.scrollBar;
    local newRange = max(0, totalHeight - CategoryContainer:GetHeight() + 52);
    local _, oldRange = scrollBar:GetMinMaxValues();

    CategoryContainer.positionFunc = nil;
    if (newRange < oldRange) and (scrollBar:GetValue() > newRange) then
        CategoryContainer.positionFunc = function(endValue, delta, scrollBar)
            if scrollBar:GetValue() <= newRange then
                CategoryContainer.positionFunc = nil;
                scrollBar:SetShown(newRange ~= 0);
                scrollBar:SetMinMaxValues(0, newRange);
                CategoryContainer.range = newRange;
            end
        end
    else
        scrollBar:SetShown(newRange ~= 0);
        scrollBar:SetMinMaxValues(0, newRange);
        CategoryContainer.range = newRange;
    end
end


local animExpand = NarciAPI_CreateAnimationFrame(0.25);
animExpand:SetScript("OnUpdate", function(self, elapsed)
    self.total = self.total + elapsed;
    local height = outSine(self.total, self.fromHeight, self.toHeight, self.duration);
    if self.total >= self.duration then
        self:Hide()
        height = self.toHeight;
    end
    animExpand.object:SetHeight(height);
end)

function animExpand:Set(object, toHeight)
    if self:IsShown() then
        if object == self.object then
            self:Hide();
        else
            --Snap to destination
            self.object:SetHeight(self.toHeight);
            self:Hide();
        end
    end
    local fromHeight = object:GetHeight();
    self.object = object;
    self.fromHeight = fromHeight;
    self.toHeight = toHeight;
    local duration = sqrt(abs(fromHeight - toHeight)/32)*0.085;
    if duration > 0.01 then
        self.duration = duration;
        self:Show();
    end
end

function animExpand:CollapseAll()
    self:Hide();
    local button;
    local parentButtons = CategoryButtons:GetActiveParentButtons();

    for i = 1, #parentButtons do
        button = parentButtons[i];
        button.box:SetHeight(32);
        button.drawer:Hide();
        button.drawer:SetAlpha(0);
        button.expanded = nil;
        if not button.isStats then
            button.progress:Hide();
            button.percentSign:Show();
            button.value:Show();
        end
    end
    local lastButton = DataProvider:GetCategoryButtonByID(DataProvider.currentCategory);
    if lastButton then
        lastButton.label:SetTextColor(0.8, 0.8, 0.8);
    end

    CategoryContainer.scrollBar:SetValue(0);
    UpdateCategoryScrollRange();
end


local function SetCategoryButtonProgress(button, numAchievements, numCompleted, isFeatsOfStrength)
    if numAchievements == 0 or numCompleted == 0 then
        button.fill:Hide();
        button.fillEnd:Hide();
        button.progress:SetText(0 .."/".. numAchievements);
    else
        if isFeatsOfStrength then
            button.fill:Hide();
            button.fillEnd:Hide();
            button.progress:SetText(numCompleted);
        else
            local percentage = numCompleted / numAchievements;
            if percentage == 1 then
                button.fill:Hide();
                button.fillEnd:Hide();
                button.progress:SetText(numAchievements);
                button.label:SetPoint("LEFT", 27, 0);
                button.greenCheck:Show();
            else
                button.fill:Show();
                button.fillEnd:Show();
                button.fill:SetWidth(button.fillWidth * percentage);
                button.fill:SetTexCoord(0, percentage *  0.75, 0, 1);
                button.progress:SetText(numCompleted .."/".. numAchievements);
                button.label:SetPoint("LEFT", 10, 0);
                button.greenCheck:Hide();
            end
        end
    end
    button.progress:Show();
    button.percentSign:Hide();
    button.value:Hide();
    button.numAchievements, button.numCompleted = numAchievements, numCompleted;
end

local function UpdateCategoryButtonProgress(button)
    local categoryID = button.id;
    local totalAchievements, totalCompleted = GetCategoryNumAchievements(categoryID, true);   --ACHIEVEMENT_COMPARISON_SUMMARY_ID

    button.numAchievements, button.numCompleted = totalAchievements, totalCompleted;

    if button.isStats then
        button.progress:SetText(totalAchievements);
        button.progress:Show();
        button.percentSign:Hide();
        button.value:Hide();
        if button.subCategories then
            local numAchievements;
            for i = 1, #button.subCategories do
                categoryID = button.subCategories[i];
                numAchievements = GetCategoryNumAchievements(categoryID, true);
                totalAchievements = totalAchievements + numAchievements;
                local childButton = DataProvider:GetCategoryButtonByID(categoryID);
                if childButton then
                    childButton.progress:SetText(numAchievements);
                    childButton.progress:Show();
                    childButton.percentSign:Hide();
                    childButton.value:Hide();
                end
            end
        end
        return
    end

    local noPercent = button.noPercent;
    if noPercent then
        button.progress:SetText(totalCompleted);
    else
        button.progress:SetText(totalCompleted .."/".. totalAchievements);
    end

    if button.expanded then
        button.progress:Show();
        button.percentSign:Hide();
        button.value:Hide();
    else
        button.progress:Hide();
        button.percentSign:Show();
        button.value:Show();
    end


    if button.subCategories then
        local numAchievements, numCompleted;
        for i = 1, #button.subCategories do
            categoryID = button.subCategories[i];
            numAchievements, numCompleted = GetCategoryNumAchievements(categoryID, true);
            totalAchievements = totalAchievements + numAchievements;
            totalCompleted = totalCompleted + numCompleted;
            local childButton = DataProvider:GetCategoryButtonByID(categoryID);
            if childButton then
                SetCategoryButtonProgress(childButton, numAchievements, numCompleted, noPercent);
            end
        end
    end

    button.totalAchievements, button.totalCompleted = totalAchievements, totalCompleted;
    

    if totalAchievements == 0 or totalCompleted == 0 then
        button.fill:Hide();
        button.fillEnd:Hide();
        button.value:SetText("0");
    else
        if noPercent then
            button.fill:Hide();
            button.fillEnd:Hide();
            button.value:SetText(totalCompleted);
        else
            button.fill:Show();
            button.fillEnd:Show();

            local percentage = totalCompleted / totalAchievements;
            button.fill:SetWidth(button.fillWidth * percentage);
            button.fill:SetTexCoord(0, percentage *  0.75, 0, 1);
            if percentage == 1 then
                button.value:SetText("100");
            else
                button.value:SetText( floor(100 * percentage) );
            end
        end
    end
end

local function UpdateCategoryButtonProgressByCategoryID(categoryID)
    local button = DataProvider:GetCategoryButtonByID(categoryID);
    if button then
        UpdateCategoryButtonProgress(button)
    end
end

local function ReleaseAchievementCard()
    for _, card in pairs(AchievementCards) do
        --card:ClearAllPoints();
        card:Hide();
    end
end

local function SelectCategory(categoryID)
    if IS_STAT_CATEGORY[categoryID] then
        --statistics
        AchievementContainer:Show();
        ScrollUtil.formatFunc = FormatStatCard;
        ScrollUtil.activeCards = StatCardController:GetTable();
        UpdateStatCardsBySlice(categoryID);
        ReleaseAchievementCard();
        SummaryFrame:Hide();
        InspectionFrame.numAchievements = GetCategoryNumAchievements(categoryID, false);
    else
        AchievementContainer:Show();
        ScrollUtil.formatFunc = FormatAchievementCardByIndex;
        ScrollUtil.activeCards = AchievementCards;
        StatCardController:ReleaseAll();
        if categoryID == -1 then
            UpdateSummaryFrame();
            SummaryFrame:Show();
        else
            UpdateAchievementCardsBySlice(categoryID);
            SummaryFrame:Hide();
            InspectionFrame.numAchievements = GetCategoryNumAchievements(categoryID, false);
        end
    end
end

local function SubCategoryButton_OnClick(button)
    local categoryID = button.id;
    if categoryID ~= DataProvider.currentCategory then
        --print(categoryID);
        local lastButton = DataProvider:GetCategoryButtonByID(DataProvider.currentCategory);
        DataProvider.currentCategory = categoryID;
        if lastButton then
            lastButton.label:SetTextColor(0.8, 0.8, 0.8);
        end
        button.label:SetTextColor(1, 0.91, 0.647);
        SelectCategory(categoryID);
    else
        --print("old")
    end
end

local function ToggleFeatOfStrenghtText(button)
    if button.isFoS then
        local _, totalCompleted = GetCategoryNumAchievements(button.id, false);
        if totalCompleted == 0 then
            if isGuildView then
                MainFrame.FeatOfStrengthText:SetText(GUILD_FEAT_OF_STRENGTH_DESCRIPTION);
            else
                MainFrame.FeatOfStrengthText:SetText(FEAT_OF_STRENGTH_DESCRIPTION);
            end
            MainFrame.FeatOfStrengthText:Show();
        else
            MainFrame.FeatOfStrengthText:Hide();
        end
    else
        MainFrame.FeatOfStrengthText:Hide();
    end
end

local function CategoryButton_OnClick(button, mouse)
    SummaryButton:Show();

    local expandedHeight = button.expandedHeight;
    local isExpanded = not button.expanded;
    if expandedHeight ~= 32 then
        if (mouse == "RightButton" or DataProvider.currentCategory == button.id) and (not isExpanded) then
            FadeFrame(button.drawer, 0.15, 0);
            animExpand:Set(button.box, 32);
            button.expanded = nil;
        else
            FadeFrame(button.drawer, 0.2, 1);
            animExpand:Set(button.box, expandedHeight);
            button.expanded = true;
            if mouse ~= "RightButton" then
                SubCategoryButton_OnClick(button);
            end
        end
    else
        button.expanded = isExpanded;
        SubCategoryButton_OnClick(button);
    end

    if button.isStats then

    else
        if button.expanded then
            button.progress:Show();
            button.percentSign:Hide();
            button.value:Hide();
        else
            button.progress:Hide();
            button.percentSign:Show();
            button.value:Show();
        end
    end


    UpdateCategoryScrollRange();


    ----
    ToggleFeatOfStrenghtText(button);
end

local function ExpandCategoryButtonNoAnimation(button)
    if not button then return end;

    if not button.expanded then
        button.progress:Show();
        button.percentSign:Hide();
        button.value:Hide();

        local expandedHeight = button.expandedHeight;
        if expandedHeight ~= 32 then
            button.box:SetHeight(expandedHeight);
            button.drawer:Show();
            button.drawer:SetAlpha(1);
        end
        button.expanded = true;

        UpdateCategoryScrollRange();
        ToggleFeatOfStrenghtText(button);
    end
end

local function BuildCategoryStructure(tabID)
    local GUILD_FEAT_OF_STRENGTH_ID = 15093;
    local GUILD_CATEGORY_ID = 15076;


    local categories, structure, feats, legacys;
    if tabID == 2 then
        categories = GetGuildCategoryList();
        structure = CategoryStructure.guild;
        feats = { FEAT_OF_STRENGTH_ID };
        legacys = {};
    elseif tabID == 1 then
        categories = GetCategoryList();
        structure = CategoryStructure.player;
        feats = { GUILD_FEAT_OF_STRENGTH_ID };
        legacys = { LEGACY_ID };
    elseif tabID == 3 then
        categories = GetStatisticsCategoryList();
        structure = CategoryStructure.stats;
        feats = {};
        legacys = {};
        tinsert(categories, 12080000);  --reserved for Narcissus Stat
        for k, id in pairs(categories) do
            IS_STAT_CATEGORY[id] = true;
        end
    end

    local id;
    local name, parentID;
    local id2Order = {};
    local subCategories = {};

    local numParent = 0;
    
    for i = 1, #categories do
        id = categories[i];
        name, parentID = DataProvider:GetCategoryInfo(id);
        --print(name, parentID)
        if (parentID == -1 or parentID == 15076) then
            if not id2Order[id] then 
                numParent = numParent + 1;
                structure[ numParent ] = { ["id"] = id, ["name"] = name, ["children"] = {} };
                id2Order[ id ] = numParent;
            end
        else
            tinsert(subCategories, id);
        end

        if parentID == LEGACY_ID then
            tinsert(legacys, id);
        elseif parentID == FEAT_OF_STRENGTH_ID then
            tinsert(feats, id);
        end
    end

    local order;
    for i = 1, #subCategories do
        id = subCategories[i];
        name, parentID = DataProvider:GetCategoryInfo(id);
        
        order = id2Order[parentID];
        tinsert( structure[ order ].children,  id);
    end

    structure.numCategories = #categories;
end

local function CreateCategoryButtons(tabID)
    local frame;
    local button, parentButton, parentData, id;
    local structure;
    local numButtons = 0;
    local parentButtons, buttons;

    if not CategoryButtons.buttons then
        CategoryButtons.buttons = {};
    end

    local isStats;
    local parentButtons = CategoryButtons:GetActiveParentButtons(tabID);
    if tabID == 1 then
        buttons = CategoryButtons.player.buttons;
        structure = CategoryStructure.player;
        frame = CategoryContainer.ScrollChild.PlayerCategory;
        --CategoryContainer.ScrollChild.GuildCategory:Hide();
    elseif tabID == 2 then
        buttons = CategoryButtons.guild.buttons;
        structure = CategoryStructure.guild;
        frame = CategoryContainer.ScrollChild.GuildCategory;
        --CategoryContainer.ScrollChild.PlayerCategory:Hide();
    elseif tabID == 3 then
        buttons = CategoryButtons.stats.buttons;
        structure = CategoryStructure.stats;
        frame = CategoryContainer.ScrollChild.StatsCategory;
        isStats = true;
        --CategoryContainer.ScrollChild.GuildCategory:Hide();
    end
    --frame:Show();
    
    for i = 1, #structure do
        numButtons = numButtons + 1;
        parentButton = buttons[numButtons];
        parentData = structure[i];
        id = parentData.id;

        if not parentButton then
            parentButton = CreateFrame("Button", nil, frame, "NarciAchievementCategoryButtonTemplate");
            tinsert(buttons, parentButton);
            tinsert(parentButtons, parentButton);
            parentButton.isParentButton = true;
            parentButton:SetScript("OnClick", CategoryButton_OnClick);
        end
        
        DataProvider.id2Button[id] = parentButton;
        parentButton:SetParent(frame);
        parentButton:ClearAllPoints();

        if i == 1 then
            parentButton:SetPoint("TOP", frame, "TOP", 2 , -24);
        else
            parentButton:SetPoint("TOP", parentButtons[i - 1].box, "BOTTOM", 0, -2);
        end

        if id == LEGACY_ID or id == FEAT_OF_STRENGTH_ID or id == 15093 then
            parentButton.noPercent = true;
            parentButton.percentSign:SetText("");
            parentButton.value:SetPoint("RIGHT", parentButton, "RIGHT", -10, 0);
            if id == FEAT_OF_STRENGTH_ID or id == 15093 then
                parentButton.isFoS = true;
            end
        else
            parentButton.noPercent = nil;
            parentButton.percentSign:SetText("%");
            parentButton.value:SetPoint("RIGHT", parentButton, "RIGHT", -16, 0);
        end

        parentButton.id = id;
        parentButton.label:SetText(parentData.name);
        parentButton.subCategories = parentData.children;

        local numChildren = #parentData.children;
        parentButton.expandedHeight = numChildren * 32 + 32;

        if isStats then
            parentButton.isStats = true;
        end

        for j = 1, numChildren do
            button = buttons[numButtons + 1];
            id = parentData.children[j];
            if not button then
                button = CreateFrame("Button", nil, parentButton.drawer, "NarciAchievementSubCategoryButtonTemplate");
                button.label:SetWidth(130);
                tinsert(buttons, button);
                button:SetScript("OnClick", SubCategoryButton_OnClick);
            end
            DataProvider.id2Button[id] = button;
            button:SetParent(parentButton.drawer);

            button:ClearAllPoints();
            if j == 1 then
                button:SetPoint("TOPRIGHT", parentButton.drawer, "BOTTOMRIGHT", 0, 0);
            else
                button:SetPoint("TOPRIGHT", buttons[numButtons], "BOTTOMRIGHT", 0, 0);
            end
            numButtons = numButtons + 1;
            
            button.id = id;
            button.label:SetText( DataProvider:GetCategoryInfo(id, 1) );
            button.noPercent = nil;

            if isStats then
                button.isStats = true;
            end
        end

        UpdateCategoryButtonProgress(parentButton);
    end
end


local function CreateAchievementButtons(frame)
    local button;
    local buttons = {};
    local numButtons = 0;

    for i = 1, NUM_ACHIEVEMENT_CARDS do
        button = CreateFrame("Button", nil, frame, "NarciAchievementLargeCardTemplate");
        button:SetScript("OnClick", AchievementCard_OnClick);
        button.index = i;
        --button.AbsoluteIndex:SetText(i)
        tinsert(buttons, button);
        if i == 1 then
            button:SetPoint("TOP", frame, "TOP", 0, -18);
        else
            button:SetPoint("TOP", buttons[i - 1], "BOTTOM", 0, -4);
        end
        ReskinButton(button);
    end

    frame.buttons = buttons;
    AchievementCards = buttons;

    --Pop-up Card : Achievement details
    local Card = InspectionFrame.Card
    Card:SetScript("OnClick", AchievementCard_OnClick);
    AchievementCards[-1] = Card;
    animFlyIn.Card = Card;
    animFlyOut.Card = Card;
    animFlyIn.header = Card.header;
    animFlyIn.description = Card.description;
    animFlyIn.date = Card.date;
    animFlyIn.reward = Card.RewardFrame;
end

------------------------------------------------
NarciAchievementInspectionFrameMixin = {};

function NarciAchievementInspectionFrameMixin:OnLoad()
    if not self.isLoaded then
        self.isLoaded = true;
    else
        return
    end

    InspectionFrame = self;

    local CompleteFrame = self.CriteriaFrame.LeftInset;
    CompleteFrame.header:SetText("COMPLETED");
    CompleteFrame.header:SetTextColor(0.216, 0.502, 0.2);
    CompleteFrame.count:SetTextColor(0.216, 0.502, 0.2);
    self.numCompleted = CompleteFrame.count;

    local IncompleteFrame = self.CriteriaFrame.RightInset;
    IncompleteFrame.header:SetText("INCOMPLETED");
    IncompleteFrame.header:SetTextColor(0.502, 0.2, 0.2);
    IncompleteFrame.count:SetTextColor(0.502, 0.2, 0.2);
    self.numIncomplete = IncompleteFrame.count;

    animFlyIn.background = self.blur;
    animFlyIn.ObjectiveFrame = self.CriteriaFrame;
    animFlyIn.ChainFrame = self.ChainFrame;
    animFlyOut.background = self.blur;
    animFlyOut.ObjectiveFrame = self.CriteriaFrame;
    animFlyOut.ChainFrame = self.ChainFrame;

    self.dataIndex = 1;


    self.TextContainerLeft = self.CriteriaFrame.LeftInset.TextFrame;
    self.TextContainerRight = self.CriteriaFrame.RightInset.TextFrame;
    self.MetaContainerLeft = self.CriteriaFrame.LeftInset.MetaFrame;
    self.MetaContainerRight = self.CriteriaFrame.RightInset.MetaFrame;
    self.BarContainerLeft = self.CriteriaFrame.LeftInset.BarFrame;
    self.BarContainerRight = self.CriteriaFrame.RightInset.BarFrame;


    --Achievement Container Blur
    local BlurFrame = self.blur;
    local BlurAnchor = BlurFrame.ScrollChild;
    local blur1 = BlurFrame.ScrollChild.blur1;
    local blur2 = BlurFrame.ScrollChild.blur2;

    local deltaRatio = 1;
    local speedRatio = 0.24;
    local blurHeight = 77;
    local range = 20000;
    local RepositionBlur = function(value, delta)
        local index;
        if delta < 0 then
            index = ceil( (value - 200) /940);
        else
            index = ceil( (value - 200) /940) - 1;
        end
        if index < 0 then
            index = 1;
        end
        if index ~= self.blurOffset then
            self.blurOffset = index;
            if index % 2 == 1 then
                blur2:SetPoint("TOPLEFT", BlurAnchor, "TOPLEFT", 0, -index * 940);
                blur2:SetPoint("TOPRIGHT", BlurAnchor, "TOPRIGHT", 0, -index * 940);
            else
                blur1:SetPoint("TOPLEFT", BlurAnchor, "TOPLEFT", 0, -index * 940);
                blur1:SetPoint("TOPRIGHT", BlurAnchor, "TOPRIGHT", 0, -index * 940);
            end
        end
    end


    NarciAPI_ApplySmoothScrollToScrollFrame(BlurFrame, deltaRatio, speedRatio, RepositionBlur, blurHeight, range);
    local Blur_OnMouseWheel = BlurFrame:GetScript("OnMouseWheel");

    function self:ScrollBlur(delta)
        if delta ~= 0 then
            Blur_OnMouseWheel(BlurFrame, delta);
            ReturnButton:Hide();
        end
    end

    function self:SyncBlurOffset(buttonIndex)
        self.blurOffset = -1;
        local value = (buttonIndex - 1) * blurHeight;
        BlurFrame.scrollBar:SetValue(value);

        local index = ceil( (value - 200) /940);
        if index < 0 then
            index = 1;
        end
        if index % 2 == 1 then
            blur1:SetPoint("TOPLEFT", BlurAnchor, "TOPLEFT", 0, (1 - index) * 940);
            blur1:SetPoint("TOPRIGHT", BlurAnchor, "TOPRIGHT", 0, (1 - index) * 940);
            blur2:SetPoint("TOPLEFT", BlurAnchor, "TOPLEFT", 0, -index * 940);
            blur2:SetPoint("TOPRIGHT", BlurAnchor, "TOPRIGHT", 0, -index * 940);
        else
            blur1:SetPoint("TOPLEFT", BlurAnchor, "TOPLEFT", 0, -index * 940);
            blur1:SetPoint("TOPRIGHT", BlurAnchor, "TOPRIGHT", 0, -index * 940);
            blur2:SetPoint("TOPLEFT", BlurAnchor, "TOPLEFT", 0, (1- index) * 940);
            blur2:SetPoint("TOPRIGHT", BlurAnchor, "TOPRIGHT", 0, (1 -index) * 940);
        end
    end

    --ScrollFrames: Criteria: Text, Meta
    local function UpdateScrollFrameDivider(value, delta, scrollBar)
        local minVal, maxVal = scrollBar:GetMinMaxValues();

        if value >= maxVal - 0.1 then
            scrollBar.divLeft:Hide();
            scrollBar.divCenter:Hide();
            scrollBar.divRight:Hide();
        elseif maxVal ~= 0 then
            scrollBar.divLeft:Show();
            scrollBar.divCenter:Show();
            scrollBar.divRight:Show();
        end
    end

    local positionFunc = UpdateScrollFrameDivider;
    local parentScrollFunc = function(delta)
        InspectionFrame:OnMouseWheel(delta);
    end

    local numLines = 30;
    local deltaRatio = 2;
    local speedRatio = 0.24;
    local buttonHeight = 26;
    local range = numLines * buttonHeight - IncompleteFrame:GetHeight();
    
    NarciAPI_ApplySmoothScrollToScrollFrame(IncompleteFrame.TextFrame, deltaRatio, speedRatio, positionFunc, buttonHeight, range, parentScrollFunc);
    NarciAPI_ApplySmoothScrollToScrollFrame(CompleteFrame.TextFrame, deltaRatio, speedRatio, positionFunc, buttonHeight, range, parentScrollFunc);

    local buttonHeight = 36;
    local deltaRatio = 2;
    NarciAPI_ApplySmoothScrollToScrollFrame(IncompleteFrame.MetaFrame, deltaRatio, speedRatio, positionFunc, buttonHeight, range, parentScrollFunc);
    NarciAPI_ApplySmoothScrollToScrollFrame(CompleteFrame.MetaFrame, deltaRatio, speedRatio, positionFunc, buttonHeight, range, parentScrollFunc);

    self:SetScript("OnLoad", nil);
    self.OnLoad = nil;
end

function NarciAchievementInspectionFrameMixin:OnShow()
    self.isTransiting = nil;
end

function NarciAchievementInspectionFrameMixin:OnMouseDown()
    if not self.NextButton:IsMouseOver() and not self.PrevButton:IsMouseOver() then
        animFlyOut:Play();
    end
end

function NarciAchievementInspectionFrameMixin:ScrollToCategoryButton(button)
    if button then
        local topButton =  CategoryButtons.player.buttons[1];
        local offset = max(0, topButton:GetTop() -  button:GetTop() - (CategoryContainer:GetHeight()/2 or 32) +32); --Attempt to position it to the vertical center
        CategoryContainer.scrollBar:SetValue(offset);
    end
end

function NarciAchievementInspectionFrameMixin:ScrollToPosition(positionIndex)
    local offset = ScrollUtil:GetOffsetByPositionIndex(positionIndex) - 18;
    AchievementContainer.scrollBar:SetValue(offset);
end

function NarciAchievementInspectionFrameMixin:OnMouseWheel(delta, stopLooping)
    if self.isTransiting or self.pauseScroll then return end;

    local index = self.dataIndex;
    if delta > 0 then
        index = index - 1;
    else
        index = index + 1;
    end
    if index < 1 or index > self.numAchievements then
        return
    else
        self.dataIndex = index;
    end

    self.dataIndex = index;
    self:ScrollBlur(delta);
    self:ScrollToPosition(index);

    local newButton = ScrollUtil:GetCardByPositionIndex(index);
    if newButton then
        if newButton.isHeader and not stopLooping then
            --skip statistic header
            self:OnMouseWheel(delta, true);
            return
        end
        InspectCard(newButton);
    end
end

local function FormatTextButtons(container, data, count, completed)
    if not container.buttons then
        container.buttons = {};
    end
    local buttons = container.buttons;
    local button, numLines;
    
    for i = 1, count do
        button = buttons[i];
        if not button then
            button = CreateFrame("Button", nil, container.ScrollChild, "NarciAchievementObjectiveTextButton");
            tinsert(buttons, button);
            if i == 1 then
                button:SetPoint("TOPLEFT", container.ScrollChild, "TOPLEFT", 0, 0);
                button:SetPoint("TOPRIGHT", container.ScrollChild, "TOPRIGHT", 0, 0);
            elseif i % 5 == 1 then
                --bigger distance for every 5 entries
                button:SetPoint("TOPLEFT", buttons[i - 1], "BOTTOMLEFT", 0, -14);
                button:SetPoint("TOPRIGHT", buttons[i - 1], "TOPRIGHT", 0, -14);
            else
                button:SetPoint("TOPLEFT", buttons[i - 1], "BOTTOMLEFT", 0, 0);
                button:SetPoint("TOPRIGHT", buttons[i - 1], "TOPRIGHT", 0, 0);
            end

            if not completed then
                button.dash:SetTextColor(0.6, 0.6, 0.6);
                button.icon:SetDesaturated(true);
            else
                button.dash:SetText("|CFF5fbb46- |r");
                button.icon:SetDesaturated(false);
            end
        end
        button.name:SetText(data.names[i]);
        numLines = ceil( button.name:GetHeight() / 12 - 0.1 );
        button.icon:SetTexture(nil);
        button:SetHeight(18 + (numLines - 1)*12 );
        button:Show();
    end

    for i = count + 1, #buttons do
        buttons[i]:Hide();
    end

    --Update Scroll Range
    local scrollBar = container.scrollBar;
    local range;
    if count == 0 then
        range = 0;
    else
        range = max(0, buttons[1]:GetTop() -  buttons[count]:GetBottom() - container:GetHeight() + 4);
        range = floor(range + 0.2);
    end
    scrollBar:SetValue(0);
    scrollBar:SetMinMaxValues(0, range);
    scrollBar:SetShown(range ~= 0);
    container.positionFunc(0, 1, scrollBar);
    container.range = range;
end

local function FormatMetaButtons(container, data, count, completed)
    if not container.buttons then
        container.buttons = {};
    end
    local buttons = container.buttons;
    local button, icon;

    for i = 1, count do
        button = buttons[i];
        if not button then
            button = CreateFrame("Button", nil, container.ScrollChild, "NarciAchievementObjectiveMetaAchievementButton");
            tinsert(buttons, button);
            if i == 1 then
                local buttonWidth = button:GetWidth();
                button:SetPoint("TOP", container.ScrollChild, "TOP", -(buttonWidth + 1) * 3, 0);
            elseif i % 7 == 1 then
                button:SetPoint("TOPLEFT", buttons[i - 7], "BOTTOMLEFT", 0, -1);
            else
                button:SetPoint("TOPLEFT", buttons[i - 1], "TOPRIGHT", 1, 0);
            end
            if not completed then
                button.icon:SetDesaturated(true);
            end
        end
        icon = data.icons[i];
        button.icon:SetTexture(icon);
        local id = data.assetIDs[i];
        button.id = id;
        if id then
            button.textMode = nil;
            button.criteriaString = nil;
        else
            button.textMode = true;
            button.criteriaString = data.names[i];
            if completed then
                button.icon:SetTexture(461267);     --ThumbsUp
            else
                button.icon:SetTexture(456031);     --Thumbsdown
            end
        end
        button.trackIcon:SetShown( DataProvider:IsTrackedAchievement(id) );
        button:Show();
    end
    for i = count + 1, #buttons do
        buttons[i]:Hide();
    end

    local header = container.name;
    local numRow = ceil( count / 7);

    header:ClearAllPoints();
    if numRow < 3 then
        header:SetPoint("TOPLEFT", buttons[1 + (numRow - 1)*7], "BOTTOMLEFT", 3, -4);
    else
        header:SetPoint("TOPLEFT", container, "BOTTOMLEFT", 3, -16);
    end

    if completed then
        header:SetText("");
        container.description:SetText("");
        container.points:SetText("");
        container.shield:Hide();
    else
        buttons[1]:SetAchievement();
    end

    --Update Scroll Range
    local scrollBar = container.scrollBar;
    local range;
    if count == 0 then
        range = 0;
    else
        range = max(0, buttons[1]:GetTop() -  buttons[count]:GetBottom() - container:GetHeight());
    end
    scrollBar:SetValue(0);
    scrollBar:SetMinMaxValues(0, range);
    scrollBar:SetShown(range ~= 0);
    container.positionFunc(0, 1, scrollBar);
    container.range = range;
end

local function FormatStatusBars(container, data, count, completed)
    if not container.bars then
        container.bars = {};
    end
    local bars = container.bars;
    local numBars = #data.bars;
    local bar, barData;

    for i = 1, numBars do
        bar = bars[i];
        if not bar then
            bar = CreateFrame("Button", nil, container, "NarciAchievementObjectiveStatusBar");
            tinsert(bars, bar);
            if i == 1 then
                bar:SetPoint("TOP", container, "TOP", 0, -2);
            else
                bar:SetPoint("TOP", bars[i - 1], "BOTTOM", 0, -16);
            end
            if completed then
                bar.label:SetTextColor(0.8, 0.8, 0.8);
            else
                bar.label:SetTextColor(0.6, 0.6, 0.6);
            end
        end
        barData = data.bars[i];
        bar:SetMinMaxValues(0, barData[2]);
        bar:SetValue(barData[1], true);
        bar.label:SetText(barData[3]);
        bar:SetHeight(bar.label:GetHeight() + 22);
        bar:Show();
    end
    for i = numBars + 1, #bars do
        bars[i]:Hide();
    end
end

function NarciAchievementInspectionFrameMixin:FindParentAchievementID(achievementID)
    local parentAchievementID = GetParentAchievementID(achievementID);
    local button = AchievementCards[-1].ParentAchievmentButton;
    if parentAchievementID then
        local _, name, _, completed, month, day, year, _, _, icon = DataProvider:GetAchievementInfo(parentAchievementID);
        if completed then
            button:SetAlpha(1);
        else
            button:SetAlpha(0.60);
        end
        button.name = name;
        button.id = parentAchievementID;
        button.icon:SetTexture(icon);
        button.icon:SetDesaturated(not completed);
        button:Show();
        if not button.hasInitialized then
            button.hasInitialized = true;
            button.border:SetTexture("Interface\\AddOns\\Narcissus_Achievements\\Art\\Shared\\IconBorderMiniPointRight");
        end
    else
        button:Hide();
    end
end

function NarciAchievementInspectionFrameMixin:UpdateChain(achievementID, currentIsCompleted)
    local ChainFrame = self.ChainFrame;
    if not ChainFrame.buttons then
        ChainFrame.buttons = {};
    end
    local buttons = ChainFrame.buttons;
    local button;
    local achievements = { achievementID };
    local currentAchievementID = achievementID;
    local currentPosition = 1;

    achievementID = GetPreviousAchievement(currentAchievementID);
    while achievementID do
        tinsert(achievements, 1, achievementID);
        achievementID = GetPreviousAchievement(achievementID);
        currentPosition = currentPosition + 1;
    end

    achievementID = GetNextAchievement(currentAchievementID);
    while achievementID do
        tinsert(achievements, achievementID);
        achievementID = GetNextAchievement(achievementID);
    end

    local numAchievements = #achievements;
    
    if numAchievements > 1 then
        local gap = 1;
        local extraHeight = 0;
        local buttonWidth;
        local id, completed, month, day, year, icon, _;
        local numCompleted = 0;
        for i = 1, numAchievements do
            button = buttons[i];
            if not button then
                button = CreateFrame("Button", nil, ChainFrame, "NarciAchievementChainButton");
                button.date.flyIn.hold1:SetDuration( (i - 1)*0.025 );
                button.date.flyIn.hold2:SetDuration( (i - 1)*0.025 );
                tinsert(buttons, button);
            end

            id = achievements[i];
            _, _, _, completed, month, day, year, _, _, icon = DataProvider:GetAchievementInfo(id);
            if completed then
                numCompleted = numCompleted + 1;
            end

            button:ClearAllPoints();
            if i == 1 then
                buttonWidth = button:GetWidth();
                local numButtonsFirstRow = min(15, numAchievements);
                button:SetPoint("CENTER", ChainFrame.reference, "TOP", (buttonWidth + gap)*(1 - numButtonsFirstRow)/2 , -buttonWidth/2);
                buttonWidth = buttonWidth + gap;
            elseif i % 15 == 1 then
                --15 buttons per row
                local numButtonsThisRow = mod(numAchievements, 15);
                extraHeight = extraHeight + buttonWidth;
                local offsetY;
                if ChainFrame.showDates and completed then
                    offsetY = -78;
                else
                    offsetY = -1.5 * buttonWidth;
                end
                button:SetPoint("CENTER", ChainFrame.reference, "TOP", (buttonWidth + gap)*(1 - numButtonsThisRow)/2, offsetY);
            else
                button:SetPoint("CENTER", buttons[i - 1], "CENTER", buttonWidth, 0);
            end

            if i == currentPosition then
                button.border:SetTexture("Interface\\AddOns\\Narcissus_Achievements\\Art\\Shared\\IconBorderPointyMini");
                button:SetAlpha(1);
            else
                button.border:SetTexture("Interface\\AddOns\\Narcissus_Achievements\\Art\\Shared\\IconBorderMini");
                if completed then
                    button:SetAlpha(0.60);
                else
                    button:SetAlpha(0.33);
                end
            end

            if completed then
                button.date:SetText( FormatDate(day, month, year, true) );
            else
                button.date:SetText("");
            end

            button.icon:SetDesaturated(not completed);
            button.id = id;
            button.icon:SetTexture(icon);
            button:Show();
        end

        for i = numAchievements + 1, #buttons do
            buttons[i]:Hide();
        end

        ChainFrame.count:SetText( numCompleted .."/".. numAchievements);

        ChainFrame.reference:SetHeight(34 + extraHeight);   --base height 34
        ChainFrame:Show();

        return true
    else
        ChainFrame:Hide();
        return false
    end
end

function NarciAchievementInspectionFrameMixin:DisplayCriteria(cData, iData)
    local numCompleted = cData.count;
    local numIncomplete = iData.count;
    
    self.numCompleted:SetText(numCompleted);
    self.numIncomplete:SetText(numIncomplete);

    local TextContainer = self.TextContainerLeft;
    local MetaContainer = self.MetaContainerLeft;
    local BarContainer = self.BarContainerLeft;
    local icon = cData.icons[1];
    local numBars = #cData.bars;
    local type = 1;
    if numBars ~= 0 then
        type = 3
    elseif icon then
        type = 2;
    end

    if numCompleted == 0 then
        TextContainer:Hide();
        MetaContainer:Hide();
        BarContainer:Hide();
    else
        if type == 2 then
            TextContainer:Hide();
            MetaContainer:Show();
            BarContainer:Hide();
            FormatMetaButtons(MetaContainer, cData, numCompleted, true);
        elseif type == 3 then
            TextContainer:Hide();
            MetaContainer:Hide();
            BarContainer:Show();
            FormatStatusBars(BarContainer, cData, numCompleted, true);
        else
            TextContainer:Show();
            MetaContainer:Hide();
            BarContainer:Hide();
            FormatTextButtons(TextContainer, cData, numCompleted, true);
        end
    end

    --
    local TextContainer = self.TextContainerRight;
    local MetaContainer = self.MetaContainerRight;
    local BarContainer = self.BarContainerRight;
    local icon = iData.icons[1];
    local numBars = #iData.bars;
    local type = 1;
    if numBars ~= 0 then
        type = 3
    elseif icon then
        type = 2;
    end

    if numIncomplete == 0 then
        TextContainer:Hide();
        MetaContainer:Hide();
        BarContainer:Hide();
    else
        if type == 2 then
            TextContainer:Hide();
            MetaContainer:Show();
            BarContainer:Hide();
            FormatMetaButtons(MetaContainer, iData, numIncomplete);
        elseif type == 3 then
            TextContainer:Hide();
            MetaContainer:Hide();
            BarContainer:Show();
            FormatStatusBars(BarContainer, iData, numIncomplete);
        else
            TextContainer:Show();
            MetaContainer:Hide();
            BarContainer:Hide();
            FormatTextButtons(TextContainer, iData, numIncomplete);
        end
    end
end

function NarciAchievementInspectionFrameMixin:ShowOrHideChainDates()
    local ChainFrame = self.ChainFrame;
    local showDates = ChainFrame.showDates;
    local buttons = ChainFrame.buttons;
    local button;

    local offsetY;
    if showDates then
        offsetY = -78;
    else
        offsetY = -52.5;
    end

    for i = 1, #buttons do
        button = buttons[i];
        button.date:SetShown(showDates);
        if showDates then
            button.date.flyIn:Play();
        else
            button.date.flyIn:Stop();
        end

        if i ~= 1 and i % 15 == 1 then
            local date = button.date:GetText();
            if date and date ~= "" then
                local point, relativeTo, relativePoint, xOfs, yOfs = button:GetPoint();
                button:SetPoint(point, relativeTo, relativePoint, xOfs, offsetY);
            end
        end
    end

    --Update toggle visual
    if showDates then
        ChainFrame.DateToggle:SetLabelText(L["Hide Dates"]);
    else
        ChainFrame.DateToggle:SetLabelText(L["Show Dates"]);
    end
    ChainFrame.count:SetShown(not showDates);
    ChainFrame.header:SetShown(not showDates);
    ChainFrame.divLeft:SetShown(not showDates);
    ChainFrame.divRight:SetShown(not showDates);
end


------------------------------------------------------------------------------
NarciAchievementGoToCategoryButtonMixin = {};

function NarciAchievementGoToCategoryButtonMixin:OnLoad()
    GoToCategoryButton = self;
    self:OnLeave();
end

function NarciAchievementGoToCategoryButtonMixin:OnEnter()
    self.Label:SetTextColor(0.8, 0.8, 0.8);
    self.Icon:SetTexCoord(0.5, 1, 0, 1);
    self.Icon:SetAlpha(0.6);
end

function NarciAchievementGoToCategoryButtonMixin:OnLeave()
    self.Label:SetTextColor(0.6, 0.6, 0.6);
    self.Icon:SetTexCoord(0, 0.5, 0, 1);
    self.Icon:SetAlpha(0.4);
end

function NarciAchievementGoToCategoryButtonMixin:OnClick()
    if self.categoryID then
        local categoryButton = DataProvider:GetCategoryButtonByID(self.categoryID, self.isGuild);
        if categoryButton and (self.categoryID ~= DataProvider.currentCategory) then
            if not categoryButton.isParentButton then
                local parentCategoryButton = DataProvider:GetCategoryButtonByID(self.parentCategoryID, self.isGuild);
                ExpandCategoryButtonNoAnimation(parentCategoryButton);
            end
            categoryButton:Click();
            animFlyOut.noTranslation = true;
            animFlyOut:Play();

            InspectionFrame:ScrollToCategoryButton(categoryButton);

            AchievementContainer:Show();
            SummaryButton:Show();
        else
            animFlyOut:Play();
        end
    end
end

function NarciAchievementGoToCategoryButtonMixin:SetAchievement(achievementID, isGuild)
    local categoryID = DataProvider:GetAchievementCategory(achievementID);
    local name, parentCategoryID = DataProvider:GetCategoryInfo(categoryID);
    if categoryID then
        self.categoryID = categoryID;
        self.parentCategoryID = parentCategoryID;
        self.isGuild = isGuild;
        self.Label:SetText(name);
        self:Show();
        self:SetWidth(max(self.Label:GetWidth() + 60, 96));
    end
end


NarciMetaAchievementButtonMixin = {};

function NarciMetaAchievementButtonMixin:SetAchievement()
    local id = self.id;
    if id then
        local id, name, points, completed, month, day, year, description, flags, icon, rewardText = DataProvider:GetAchievementInfo(id);
        local parent = self:GetParent():GetParent();
        parent.name:SetText(name);
        parent.description:SetText(description);
        parent.points:SetText(points);
        parent.shield:SetShown(points ~= 0);
        if completed then
            if IsAccountWide(flags) then
                parent.name:SetTextColor(0.427, 0.812, 0.965);
            else
                parent.name:SetTextColor(1, 0.91, 0.647);
            end
        else
            parent.name:SetTextColor(0.8, 0.8, 0.8);
        end

        self.name = name;
    end
end

function NarciMetaAchievementButtonMixin:OnEnter()
    if self.textMode then
        local parent = self:GetParent():GetParent();
        parent.name:SetText(self.criteriaString);
        parent.description:SetText(nil);
        parent.points:SetText(nil);
        parent.shield:Hide();
    else
        self:SetAchievement();
    end
    self.icon.animIn:Play();
    self.border.animIn:Play();
end

function NarciMetaAchievementButtonMixin:OnLeave()
    self.icon.animIn:Stop();
    self.border.animIn:Stop();
    self.icon:SetScale(1);
    self.border:SetScale(1);
end

function NarciMetaAchievementButtonMixin:OnClick()
    if ProcessModifiedClick(self) then return end;

    local id = InspectionFrame.currentAchievementID;
    if id and id ~= self.id then
        ReturnButton:AddToQueue(id, InspectionFrame.currentAchievementName);
        InspectAchievement(self.id);
    end
end

NarciAchievementChainButtonMixin = {};

function NarciAchievementChainButtonMixin:OnLoad()
    
end

function NarciAchievementChainButtonMixin:OnEnter()
    self:SetAchievement();
    self.icon.animIn:Play();
    self.border.animIn:Play();
end

function NarciAchievementChainButtonMixin:OnLeave()
    self.icon.animIn:Stop();
    self.border.animIn:Stop();
    self.icon:SetScale(1);
    self.border:SetScale(1);

    Tooltip:FadeOut();
    local ChainFrame = self:GetParent();
    if ChainFrame.DateToggle and not ChainFrame:IsMouseOver() then
        ChainFrame.DateToggle:FadeOut();
    end
end

function NarciAchievementChainButtonMixin:SetAchievement()
    local id = self.id;
    if id then
        Tooltip:ClearAllPoints();
        Tooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 1, -2);
        Tooltip:SetAchievement(id);
    end
end

function NarciAchievementChainButtonMixin:OnMouseDown()
    Tooltip:FadeOut();
end

NarciAchievementTooltipMixin = {};

function NarciAchievementTooltipMixin:OnLoad()
    local animFade = NarciAPI_CreateAnimationFrame(0.25);
    self.animFade = animFade;
    animFade:SetScript("OnUpdate", function(frame, elapsed)
        frame.total = frame.total + elapsed;
        local alpha = outQuart(frame.total, frame.fromAlpha, frame.toAlpha, frame.duration);
        if frame.total >= frame.duration then
            frame:Hide();
            alpha = frame.toAlpha;
            if alpha == 0 then
                self:Hide();
            end
        end
        self:SetAlpha(alpha);
    end)

    function self:FadeIn()
        if InspectionFrame.isTransiting then return end;
        self:Show();
        animFade:Hide();
        animFade.fromAlpha = self:GetAlpha();
        animFade.toAlpha = 1;
        animFade:Show();
    end

    function self:FadeOut()
        animFade.toAlpha = 0;
        if not self:IsShown() then return end;
        animFade:Hide();
        animFade.fromAlpha = self:GetAlpha();
        animFade:Show();
    end

    local showDelay = NarciAPI_CreateAnimationFrame(0.12);
    self.showDelay = showDelay;
    showDelay:SetScript("OnUpdate", function(frame, elapsed)
        frame.total = frame.total + elapsed;
        if frame.total >= frame.duration then
            frame:Hide();
            if animFade.toAlpha == 1 then
                self:FadeIn();
            end
        end
    end)
end

function NarciAchievementTooltipMixin:OnHide()
    self:Hide();
    self:SetAlpha(0);
end

function NarciAchievementTooltipMixin:ResizeAndShow()
    self:SetHeight( self.name:GetHeight() + self.description:GetHeight() + 4 + 24 );
    
    if not self:IsShown() then
        self.animFade.toAlpha = 1;
        self.showDelay:Show();
    elseif self.animFade.toAlpha == 0 then
        self:FadeIn();
    end
end

function NarciAchievementTooltipMixin:SetAchievement(id)
    local _, name, points, completed, month, day, year, description, flags, icon = DataProvider:GetAchievementInfo(id);

    self.name:SetText(name);
    self.description:SetText(description);
    
    if completed then
        if IsAccountWide(flags) then
            self.name:SetTextColor(0.427, 0.812, 0.965);
        else
            self.name:SetTextColor(1, 0.91, 0.647);
        end
        self.date:SetText( FormatDate(day, month, year) );
    else
        self.name:SetTextColor(0.8, 0.8, 0.8);
        self.date:SetText("");
    end

    if points == 0 then
        self.shield:Hide();
        self.points:Hide();
    else
        self.shield:Show();
        self.points:SetText(points);
        self.points:Show();
    end

    self:ResizeAndShow();
end

----------------------------------------------------------------------------------
NarciAchievementReturnButtonMixin = {};

function NarciAchievementReturnButtonMixin:OnLoad()
    ReturnButton = self;
    self.structure = {};
end

function NarciAchievementReturnButtonMixin:PlayFlyIn()
    self.flyIn:Play();
end

function NarciAchievementReturnButtonMixin:OnEnter()
    self.label:SetTextColor(0.88, 0.88, 0.88);
end

function NarciAchievementReturnButtonMixin:OnLeave()
    self.label:SetTextColor(0.6, 0.6, 0.6);
end

function NarciAchievementReturnButtonMixin:OnHide()
    self:Hide();
    self:StopAnimating();
    wipe(self.structure);
end

function NarciAchievementReturnButtonMixin:SetLabelText(text)
    self.label:SetText("");
    self.label:SetWidth(0);
    self.label:SetText(text);

    local textWidth = min(140, self.label:GetWidth());
    self.label:SetWidth(textWidth)
    self:SetWidth(textWidth + 24);
end

function NarciAchievementReturnButtonMixin:AddToQueue(achievementID, achievementName)
    for i = 1, #self.structure + 1 do
        if self.structure[i] == nil then
            self.structure[i] = {id = achievementID, name = achievementName};
            break;
        end
    end
    self:SetLabelText(achievementName);
    self:Show();
    self:PlayFlyIn();
end

function NarciAchievementReturnButtonMixin:OnClick()
    for i = #self.structure, 1, -1 do
        local info = self.structure[i];
        if info and info.id then
            InspectAchievement(info.id);
            if i == 1 then
                self:Hide();
            else
                self:SetLabelText(self.structure[i - 1].name)
            end
            self.structure[i] = nil;
            break;
        end
    end
end


NarciAchievementSearchBoxMixin = {};

local function InspectResult(button)
    local playAnimation = not InspectionFrame:IsShown();

    local id = button.id;
    local Card = InspectionFrame.Card;
    Card:ClearAllPoints();
    Card:SetPoint("BOTTOM", InspectionFrame, "CENTER", 0, 36);

    InspectionFrame.PrevButton:Disable();
    InspectionFrame.NextButton:Disable();
    InspectionFrame.pauseScroll = true;

    InspectAchievement(id);

    animFlyOut.button = nil;
    animFlyOut.fromX, animFlyOut.toX, animFlyOut.fromY, animFlyOut.toY = 0, 0, 0, 0;
    animFlyOut.noTranslation = true;

    if playAnimation then
        animFlyIn:Play();
        if DataProvider:IsStatistic(id) then
            InspectionFrame.StatCard:SetAlpha(0);
            FadeFrame(InspectionFrame.StatCard, 0.25, 1);
        end
    end
end

addon.InspectResult = InspectResult;


--------------------------------------------------------------
local DateUtil = {};
function DateUtil:GetToday()
    self.today = time();
end

function DateUtil:GetDifference(day, month, year)
    year = 2000 + year;
    local past = time( {day = day, month = month, year = year} )
    if not self.today then
        self.today = time();
    end
    return (self.today - past)
end

function DateUtil:GetPastDays(day, month, year)
    local diff = self:GetDifference(day, month, year);
    local d = floor(diff / 86400);
    if d <= 0 then
        return L["Today"]
    elseif d == 1 then
        return L["Yesterday"]
    elseif d < 31 then
        return format(L["Format Days Ago"], d);
    else
        local m = floor(d / 30.5);
        if m <= 1 then
            return L["A Month Ago"]
        elseif m < 12 then
            return format(L["Format Months Ago"], m);
        else
            local y = floor(m / 12 + 0.15);
            if y == 1 then
                return L["A Year Ago"]
            else
                return format(L["Format Years Ago"], y);
            end
        end
    end
end


local UpdateHeaderFrame;

function UpdateSummaryFrame(breakLoop)
    local recentAchievements = { GetLatestCompletedAchievements(isGuildView) };
    local numAchievements = #recentAchievements;
    if (numAchievements < 5) and (not breakLoop) then
        After(0.05, function()
            UpdateSummaryFrame(true);
        end);
        return
    end

    ScrollUtil:ResetHeights();
    processor:Hide();
    AchievementContainer.scrollBar:SetValue(0);
    for i = 1, NUM_ACHIEVEMENT_CARDS do
        AchievementCards[i]:Hide();
    end
    local id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild;
    for i = 1, #recentAchievements do
        id = recentAchievements[i];
        if id then
            id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild = DataProvider:GetAchievementInfo(id);
            if i <= NUM_ACHIEVEMENT_CARDS then
                FormatAchievementCardByIndex(i, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, true);
                AchievementCards[i].date:SetText( DateUtil:GetPastDays(day, month, year) );
            end
            ScrollUtil:SetCardData(i, id, description, rewardText);
        else
            break;
        end
    end
    UpdateAchievementScrollRange();
    ScrollUtil:UpdateScrollChild(0);
    UpdateHeaderFrame(isGuildView);
    if numAchievements ~= 0 then
        animFlip:Play(1);
    end
    InspectionFrame.numAchievements = numAchievements;
end

----------------------------------------------------------------------------------

function TabUtil:SaveOffset()
    --currentTab before switching
    local button = DataProvider:GetCategoryButtonByID(DataProvider.currentCategory);
    local offset = CategoryContainer.scrollBar:GetValue();
    if self.tabID == 1 then
        --achievement
        self.lastPlayerButton = button;
        self.lastPlayerScrollValue = offset;

    elseif self.tabID == 2 then
        --guild
        self.lastGuildButton = button;
        self.lastGuildScrollValue = offset;

    elseif self.tabID == 3 then
        --statistics
        self.lastStatButton = button;
        self.lastStatScrollValue = offset;
    end
    ShutInspection();
end

function TabUtil:ResumeOffset()
    local lastButton, lastOffset;
    if self.tabID == 1 then
        lastButton = self.lastPlayerButton;
        lastOffset = self.lastPlayerScrollValue;
    elseif self.tabID == 2 then
        lastButton = self.lastGuildButton;
        lastOffset = self.lastGuildScrollValue;
    elseif self.tabID == 3 then
        lastButton = self.lastStatButton;
        lastOffset = self.lastStatScrollValue;
    end
    if lastButton then
        AchievementContainer:Show();
        SummaryButton:Show();
        SubCategoryButton_OnClick(lastButton);
        ToggleFeatOfStrenghtText(lastButton);
    else
        if self.tabID == 4 then
            
        else
            SummaryButton:Click();
        end
    end
    AchievementContainer.scrollBar:SetValue(0);
    CategoryContainer.scrollBar:SetValue(lastOffset or 0);
end

function TabUtil:ToggleAchievement(tabID)
    if tabID ~= self.tabID then
        self:SaveOffset();
        self.tabID = tabID;
        local isGuild = (tabID == 2);
        isGuildView = isGuild;
        CategoryContainer.ScrollChild.PlayerCategory:SetShown(not isGuild);
        CategoryContainer.ScrollChild.GuildCategory:SetShown(isGuild);
        CategoryContainer.ScrollChild.StatsCategory:Hide();
        DIYContainer:Hide();
        EditorContainer:Hide();
        CategoryContainer:Show();
        UpdateCategoryScrollRange();
        MainFrame.HeaderFrame.points:SetText( BreakUpLargeNumbers(GetTotalAchievementPoints(isGuild)) );
        FilterButton:Enable();
        SummaryButton:SetMode(1);
        self:ResumeOffset();
        self:EnableSearchBox(true);
    end
end

function TabUtil:ToggleStats()
    if self.tabID == 3 then return end;
    self:SaveOffset();
    self.tabID = 3;
    CategoryContainer.ScrollChild.StatsCategory:Show();
    CategoryContainer.ScrollChild.PlayerCategory:Hide();
    CategoryContainer.ScrollChild.GuildCategory:Hide();
    MainFrame.FeatOfStrengthText:Hide();
    MainFrame:UpdatePinCount();
    DIYContainer:Hide();
    EditorContainer:Hide();
    CategoryContainer:Show();
    UpdateCategoryScrollRange();
    FilterButton:Disable();
    SummaryButton:SetMode(2);
    self:ResumeOffset();
    self:EnableSearchBox(true);
end

function TabUtil:ToggleDIY()
    if self.tabID == 4 then return end;
    self:SaveOffset();
    self.tabID = 4;
    DataProvider.currentCategory = nil;
    DIYContainer:Show();
    if not TabUtil.isDIYLoaded then
        TabUtil.isDIYLoaded = true;
        DIYContainer:Refresh();
        DIYContainer.scrollBar:SetValue(0);
    end
    
    EditorContainer:Show();
    CategoryContainer:Hide();
    AchievementContainer:Hide();
    FilterButton:Disable();
    SummaryButton:SetMode(3);
    MainFrame.FeatOfStrengthText:Hide();
    self:EnableSearchBox(false);
end

function TabUtil:GetTabID()
    return self.tabID or 1;
end
--------------------------------------------------------------
NarciAchievementFilterButtonMixin = {};

function NarciAchievementFilterButtonMixin:OnLoad()
    self:OnLeave();
    FilterButton = self;
    self.OnLoad = nil;
    self:SetScript("OnLoad", nil);
end

function NarciAchievementFilterButtonMixin:OnClick()
    local state = not NarciAchievementOptions.IncompleteFirst;
    NarciAchievementOptions.IncompleteFirst = state;
    self:UpdateFilter();
end

function NarciAchievementFilterButtonMixin:OnEnable()
    self.label:SetTextColor(0.8, 0.8, 0.8);
    self:EnableMouse(true);
end

function NarciAchievementFilterButtonMixin:OnDisable()
    self.label:SetTextColor(0.5, 0.5, 0.5);
    self:EnableMouse(false);
end

function NarciAchievementFilterButtonMixin:UpdateFilter()
    if NarciAchievementOptions.IncompleteFirst then
        self.label:SetText(L["Incomplete First"]);
        SwitchToSortMethod(1);
    else
        self.label:SetText(L["Earned First"]);
        SwitchToSortMethod(2);
    end
end

function NarciAchievementFilterButtonMixin:OnMouseDown()
    self.texture:SetTexCoord(0, 1, 0.5, 1);
end

function NarciAchievementFilterButtonMixin:OnMouseUp()
    self.texture:SetTexCoord(0, 1, 0, 0.5);
end

function NarciAchievementFilterButtonMixin:OnEnter()
    self.texture:SetVertexColor(1, 1, 1);
end

function NarciAchievementFilterButtonMixin:OnLeave()
    self.texture:SetVertexColor(0.66, 0.66, 0.66);
end

------------------------------------------------------------------
NarciAchievementSummaryButtonMixin = {};

function NarciAchievementSummaryButtonMixin:OnClick()
    self:Hide();
    local categoryID;
    if self.modeID == 2 then
        AchievementContainer:Show();
        SelectCategory(-2);
        animExpand:CollapseAll();
        categoryID = - 2;
    else
        categoryID = -1;
        SelectCategory(-1);
    end
    if DataProvider.currentCategory ~= categoryID then
        animExpand:CollapseAll();
        DataProvider.currentCategory = categoryID;
    end
    MainFrame.FeatOfStrengthText:Hide();
end

function NarciAchievementSummaryButtonMixin:OnMouseDown()
    self.texture:SetTexCoord(0, 1, 0.5, 0.8125);
    self.label:SetTextColor(0.6, 0.6, 0.6);
end

function NarciAchievementSummaryButtonMixin:OnMouseUp()
    self.texture:SetTexCoord(0, 1, 0, 0.3125);
    self.label:SetTextColor(0.8, 0.8, 0.8);
end

function NarciAchievementSummaryButtonMixin:SetMode(modeID)
    self.modeID = modeID;
    if modeID == 3 then
        self:Show();
        self:Disable();
        self.label:SetText("DIY");
    else
        if modeID == 1 then
            self.label:SetText(ACHIEVEMENT_SUMMARY_CATEGORY);
        else
            self.label:SetText(L["Pinned Entries"]);
        end
        self:Enable();
    end
end

function NarciAchievementSummaryButtonMixin:OnEnable()
    self:EnableMouse(true);
    self:OnMouseUp();
end

function NarciAchievementSummaryButtonMixin:OnDisable()
    self:EnableMouse();
    self:OnMouseDown();
end
------------------------------------------------------------------


local function CreateTabButtons()
    local tabNames = {ACHIEVEMENTS, ACHIEVEMENTS_GUILD_TAB, STATISTICS, "DIY", L["Settings"]};
    local frame = Narci_AchievementFrame;
    local buttons = {};
    local function DeselectRest(button)
        for i = 1, #buttons do
            if buttons[i] ~= button then
                buttons[i]:Deselect();
            end
        end
    end

    local funcs = {
        function(self)
            TabUtil:ToggleAchievement(1);
            DeselectRest(self);
            self:Select();
        end,

        function(self)
            TabUtil:ToggleAchievement(2);
            DeselectRest(self);
            self:Select();
        end,

        function(self)
            TabUtil:ToggleStats();
            DeselectRest(self);
            self:Select();
        end,

        function(self)
            TabUtil:ToggleDIY();
            DeselectRest(self);
            self:Select();
        end,

        function(self)
            MainFrame.Settings:Toggle();
        end,
    }
    
    local numTabs = #tabNames;
    for i = 1, numTabs do
        local button = CreateFrame("Button", nil, frame, "NarciAchievementTabButtonTemplate");
        tinsert(buttons, button);
        button:SetLabel(tabNames[i]);
        button.id = i;

        if i == 1 then
            button:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 23, 0);
            button:Select();
        elseif i == numTabs then
            --settings
            button:SetPoint("TOPRIGHT", frame, "BOTTOMRIGHT", -23, 0);
            MainFrame.Settings.linkedTabButton = button;
        else
            button:SetPoint("TOPLEFT", buttons[i - 1], "TOPRIGHT", 14, 0);
        end

        button:SetScript("OnClick", funcs[i]);
    end

    TabButtons = buttons;

    TabUtil:ToggleAchievement(1);
end
----------------------------------------------------------------------------------
local function GetInspectedCardLink()
    local achievementID = InspectionFrame.currentAchievementID;
    if not achievementID then return end;

    local URL = NarciLanguageUtil:GetWowheadLink();
    URL = URL .. "achievement=" .. tostring(achievementID);

    return URL
end

NarciAchievementGetLinkButtonMixin = {};

function NarciAchievementGetLinkButtonMixin:OnLoad()
    self.Label:SetText(L["External Link"]);
    local textWidth = max(40, floor( self.Label:GetWidth() + 0.5 ) );
    self:SetWidth(textWidth + 40 + 16);

    local range = textWidth + 24;
    self.Icon:SetPoint("RIGHT", self, "RIGHT", -18, 0);


    local animShow = NarciAPI_CreateAnimationFrame(0.25);
    self.animShow = animShow;
    animShow:SetScript("OnUpdate", function(frame, elapsed)
        frame.total = frame.total + elapsed;
        local offsetX = inOutSine(frame.total, frame.fromX, frame.toX, frame.duration);
        local textOffset = inOutSine(frame.total, frame.fromTextX, frame.toTextX, frame.duration);
        local alpha = outQuart(frame.total, frame.fromAlpha, frame.toAlpha, frame.duration);
        if frame.total >= frame.duration then
            offsetX = frame.toX;
            textOffset = frame.toTextX;
            alpha = frame.toAlpha;
            frame:Hide();

            if alpha == 0 then
                self.Clipboard:Hide();
            end
        end
        self.Icon:SetPoint("RIGHT", self, "RIGHT", offsetX, 0);
        self.Label:SetPoint("RIGHT", self, textOffset, 0);
        self.Label:SetAlpha(alpha);
        self.Clipboard:SetAlpha(alpha);
    end);

    function self:AnimShow()
        animShow:Hide();
        local offsetX, _;
        _, _, _, offsetX = self.Icon:GetPoint();
        _, _, _, animShow.fromTextX = self.Label:GetPoint();
        animShow.duration = ( sqrt(1 - abs(offsetX)/ range) * 0.4);
        animShow.fromX = offsetX;
        animShow.toX = -range;
        --animShow.fromTextX = -28; --keep label still
        animShow.toTextX = -28;
        animShow.fromAlpha = self.Label:GetAlpha();
        animShow.toAlpha = 1;
        animShow:Show();
    end

    function self:AnimHide()
        animShow:Hide();
        local offsetX, _;
        _, _, _, offsetX = self.Icon:GetPoint();
        _, _, _, animShow.fromTextX = self.Label:GetPoint();
        animShow.duration = ( sqrt(abs(offsetX)/ range) * 0.5);
        animShow.fromX = offsetX;
        animShow.toX = -20;
        animShow.toTextX = -10;
        animShow.fromAlpha = self.Label:GetAlpha();
        animShow.toAlpha = 0;
        animShow:Show();
    end

    self.Clipboard:SetScript("OnLeave", function()
        self:OnLeave();
    end);
    self.Clipboard.EditBox:SetScript("OnLeave", function()
        self:OnLeave();
    end);

    self.Clipboard:ReAnchorTooltipToObject(self.Label);
    self.Clipboard.EditBox.onQuitFunc = function()
        self:OnLeave();
    end
end

function NarciAchievementGetLinkButtonMixin:OnClick()
    self.Clipboard:ShowClipboard();
    self.Clipboard:SetText( GetInspectedCardLink() );
    self.Clipboard:SetFocus();
    self.Label:Hide();
end

function NarciAchievementGetLinkButtonMixin:OnEnter()
    --self.Label:SetTextColor(1, 1, 1);
    self.Icon:SetTexCoord(0.5, 1, 0, 1);
    self.Icon:SetAlpha(0.6);
    self:AnimShow();
    self.Label:Show();
end

function NarciAchievementGetLinkButtonMixin:OnLeave()
    if not self:IsMouseOver() and not self.Clipboard:HasFocus() then
        self.Label:SetTextColor(0.8, 0.8, 0.8);
        self.Icon:SetTexCoord(0, 0.5, 0, 1);
        self.Icon:SetAlpha(0.4);
        self:AnimHide();
        self.Clipboard.EditBox:HighlightText(0, 0);
    end
end


--------------------------------------------------------
local function InitializeFrame(frame)
    --Category
    CategoryContainer = frame.CategoryFrame;
    local isGuild = true;
    CreateCategoryButtons(2);
    isGuild = not isGuild;
    CreateCategoryButtons(1);
    CreateCategoryButtons(3);
    local numCategories = CategoryStructure.player.numCategories;
    local deltaRatio = 2;
    local speedRatio = 0.15;
    local buttonHeight = 32;
    local range = numCategories  * (buttonHeight + 4) - CategoryContainer:GetHeight();
    local positionFunc;
    
    NarciAPI_ApplySmoothScrollToScrollFrame(CategoryContainer, deltaRatio, speedRatio, positionFunc, buttonHeight, range);
    UpdateCategoryScrollRange();


    --Sort Method
    FilterButton:UpdateFilter();
    showNotEarnedMark = NarciAchievementOptions.ShowRedMark;

    --Achievement
    AchievementContainer = frame.AchievementCardFrame;
    CreateAchievementButtons(AchievementContainer.ScrollChild);

    local numCards = #AchievementCards;
    local deltaRatio = 1;
    local speedRatio = 0.24;
    local buttonHeight = 64;
    local range = numCards  * (buttonHeight + 4) - AchievementContainer:GetHeight();
    local positionFunc;
    NarciAPI_ApplySmoothScrollToScrollFrame(AchievementContainer, deltaRatio, speedRatio, positionFunc, buttonHeight, range);
    local scrollBar = AchievementContainer.scrollBar;
    scrollBar:SetScript("OnValueChanged", function(bar, value)
        AchievementContainer:SetVerticalScroll(value);
        ScrollUtil:GetTopButtonIndex(value);
    end);
    UpdateAchievementScrollRange();

    --Model Preview
    MountPreview = CreateFrame("ModelScene", nil, frame,"NarciAchievementRewardModelTemplate");
    MountPreview:ClearAllPoints();
    MountPreview:SetPoint("LEFT", frame, "RIGHT", 4, 0);

    Tooltip = frame.Tooltip;

    --Header Filter, Total Points, Search, Close
    local HeaderFrame = frame.HeaderFrame;

    SummaryButton = frame.SummaryButton;
    
    function UpdateHeaderFrame(isGuild) --Private
        local total, completed = GetNumCompletedAchievements(isGuild);
        HeaderFrame.totalAchievements:SetText(completed.."/"..total);
        HeaderFrame.points:SetText( BreakUpLargeNumbers(GetTotalAchievementPoints(isGuild)) );
        if completed > 0 then
            HeaderFrame.fill:Show();
            local percentage = completed / total;
            HeaderFrame.fill:SetWidth(198 * percentage);
            HeaderFrame.fill:SetTexCoord(0, percentage *  0.75, 0, 1);
            if percentage == 1 then
                HeaderFrame.value:SetText("100");
                HeaderFrame.fillEnd:Hide();
            else
                HeaderFrame.value:SetText( floor(100 * percentage) );
                HeaderFrame.fillEnd:Show();
            end
        end
        HeaderFrame.value:Show();
        HeaderFrame.percentSign:Show();
        HeaderFrame.progress:Hide();
    end

    --SummaryFrame
    SummaryFrame = frame.SummaryFrame;

    --DIY Achievements
    DIYContainer = frame.DIYContainer;
    EditorContainer = frame.EditorContainer;

    --Tabs
    CreateTabButtons();
    NarciAchievement_SelectTheme(NarciAchievementOptions.Theme or 1);

    --
    tinsert(UISpecialFrames, frame:GetName());

    frame:Show();
    UpdateSummaryFrame();

    --Reclaim Temp
    wipe(CategoryStructure);
    CategoryStructure = nil;
    CreateCategoryButtons = nil;
    CreateAchievementButtons = nil;
    CreateTabButtons = nil;
    InitializeFrame = nil;
end

-----------------------------------------
local FloatingCard = addon.FloatingCard;


local function RefreshInspection(achievementID)
    --Refresh inspection card
    if InspectionFrame:IsShown() then
        if achievementID then
            if (achievementID == InspectionFrame.Card.id) then
                InspectAchievement(achievementID);
            end
        else
            if InspectionFrame.Card.id then
                InspectAchievement(InspectionFrame.Card.id);
            end
        end
    end
end
--------------------------------------------------------------------
--Public
NarciAchievementFrameMixin = {};

function NarciAchievementFrameMixin:OnLoad()
    MainFrame = self;

    ScrollUtil.textReference = self.TextHeightRetriever;

    self:RegisterForDrag("LeftButton");
    self:SetAttribute("nodeignore", true);  --ConsolePort: Ignore this frame
end

function NarciAchievementFrameMixin:OnShow()
    if self.pendingCategoryID then
        SelectCategory(self.pendingCategoryID);
        self.pendingCategoryID = nil;
    elseif self.pendingUpdate then
        self.pendingUpdate = nil;
        UpdateSummaryFrame();
    end
    self:RegisterDynamicEvent(true);
    RefreshInspection();
    StatCardController:UpdateList();
end

function NarciAchievementFrameMixin:OnHide()
    self:RegisterDynamicEvent(false);
end

function NarciAchievementFrameMixin:RegisterDynamicEvent(state)
    if state then
        self:RegisterEvent("CRITERIA_UPDATE");
        self:RegisterEvent("CRITERIA_COMPLETE");
    else
        self:UnregisterEvent("CRITERIA_UPDATE");
        self:UnregisterEvent("CRITERIA_COMPLETE");
    end
end

function NarciAchievementFrameMixin:OnEvent(event, ...)
    RefreshInspection(...);
    if event == "CRITERIA_UPDATE" then
        StatCardController:UpdateList();
    end
end

function NarciAchievementFrameMixin:ShowRedMark(visible)
    showNotEarnedMark = visible;
    for i = 1, #AchievementCards do
        if visible then
            AchievementCards[i].NotEarned:SetWidth(20);
        else
            AchievementCards[i].NotEarned:SetWidth(0.1);
        end
    end
    if MainFrame:IsShown() then
        local categoryID = DataProvider.currentCategory;
        if categoryID and categoryID ~= 0 then
            SelectCategory(categoryID);
        end
    end
end

function NarciAchievementFrameMixin:LocateAchievement(achievementID, clickAgainToClose)
    local Card = InspectionFrame.Card;

    if  (not achievementID) or ( clickAgainToClose and (Card.id == achievementID) and MainFrame:IsShown() and InspectionFrame:IsShown() ) then
        MainFrame:Hide();
        return
    end
    
    MainFrame:Show();
    local playAnimation = not InspectionFrame:IsShown();
    Card:ClearAllPoints();
    Card:SetPoint("BOTTOM", InspectionFrame, "CENTER", 0, 36);

    InspectionFrame.PrevButton:Disable();
    InspectionFrame.NextButton:Disable();
    InspectionFrame.pauseScroll = true;

    InspectAchievement(achievementID);

    animFlyOut.button = nil;
    animFlyOut.fromX, animFlyOut.toX, animFlyOut.fromY, animFlyOut.toY = 0, 0, 0, 0;
    animFlyOut.noTranslation = true;

    if playAnimation then
        animFlyIn:Play();
    end
end

function NarciAchievementFrameMixin:OnDragStart()
    self:StartMoving();

    --Anchor settings frame to self
    local f = self.Settings;
    f:ClearAllPoints();
    f:SetPoint("TOPLEFT", self, "TOPRIGHT", 8, -4);
end

function NarciAchievementFrameMixin:OnDragStop()
    self:StopMovingOrSizing();

    self.Settings.AnchorToUIParent();
end

function NarciAchievementFrameMixin:UpdatePinCount()
    local HeaderFrame = self.HeaderFrame;
    local total, cap = PinUtil:GetTotal();
    HeaderFrame.totalAchievements:SetText(L["Pinned Entries"]);
    HeaderFrame.progress:SetText(string.format("%d/%d", total, cap));
    HeaderFrame.progress:Show();
    HeaderFrame.value:Hide();
    HeaderFrame.percentSign:Hide();

    if total > 0 then
        HeaderFrame.fill:Show();
        local percentage = total / cap;
        HeaderFrame.fill:SetWidth(198 * percentage);
        HeaderFrame.fill:SetTexCoord(0, percentage *  0.75, 0, 1);
        if percentage == 1 then
            HeaderFrame.fillEnd:Hide();
        else
            HeaderFrame.fillEnd:Show();
        end
    else
        HeaderFrame.fill:Hide();
        HeaderFrame.fillEnd:Hide();
    end
end

--[[
function NarciAchievement_FormatAlertCard(card)
    local achievementID = card.id;
    local uiScale = MainFrame:GetEffectiveScale();
    card.uiScale = uiScale;
    local id, name, points, completed, month, day, year, description, flags, icon, rewardText = DataProvider:GetAchievementInfo(achievementID);
    FormatFloatingCard(card, id, name, points, completed, month, day, year, description, flags, icon, rewardText);
    ReskinButton(card);
end

function NarciAchievement_ReskinButton(card)
    ReskinButton(card);
    card.isDarkTheme = isDarkTheme;
end
--]]

NarciAchievementLargeCardMixin = {};

function NarciAchievementLargeCardMixin:OnDragStart()
    --Create Floating Card
    if not self.id then return end;
    self.AnimPushed:Stop();
    self:Hide();

    local card = FloatingCard:CreateFromCard(self, 1);
end

function NarciAchievementLargeCardMixin:OnLoad()
    self:RegisterForDrag("LeftButton");
    self:SetScript("OnLoad", nil);
    self.OnLoad = nil;
end

function NarciAchievementLargeCardMixin:OnMouseDown()
    self.AnimPushed:Stop();
    self.AnimPushed.hold:SetDuration(20);
    self.AnimPushed:Play();
end

function NarciAchievementLargeCardMixin:OnMouseUp()
    self.AnimPushed.hold:SetDuration(0);
end

function NarciAchievementLargeCardMixin:UpdateTheme()
    ReskinButton(self);
    if self.id then
        local visibility = self:IsShown();
        local id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, earnedByMe  = DataProvider:GetAchievementInfo(self.id);
        FormatAchievementCard(self, id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuild, earnedByMe);
        self:SetShown(visibility);
    end
end

function NarciAchievement_SelectTheme(index)
    if not index or index > 3 or index < 1 then
        index = 1;
    end
    if index == themeID then return end;

    themeID = index;
    NarciAchievementOptions.Theme = index;

    if index == 3 then
        isDarkTheme = true;
        texturePrefix = "Interface\\AddOns\\Narcissus_Achievements\\Art\\Flat\\";
    elseif index == 2 then
        isDarkTheme = false;
        texturePrefix = "Interface\\AddOns\\Narcissus_Achievements\\Art\\Classic\\";
    else
        isDarkTheme = true;
        texturePrefix = "Interface\\AddOns\\Narcissus_Achievements\\Art\\DarkWood\\";
    end

    --Statistics
    StatCardController:SetTheme(index);
    InspectionFrame.StatCard:UpdateTheme();
    for i = 1, #AchievementCards do
        ReskinButton(AchievementCards[i]);
    end

    ReskinButton(AchievementCards[-1]);

    --DIY Cards
    if DIYContainer.cards then
        for i = 1, #DIYContainer.cards do
            ReskinButton(DIYContainer.cards[i]);
            DIYContainer.cards[i].isDarkTheme = isDarkTheme;
        end
    end
    DIYContainer:RefreshTheme();
    DIYContainer.NewEntry.background:SetTexture(texturePrefix.."NewEntry");
    if index == 1 then
        EditorContainer.notes:SetFontObject(NarciAchievementText);
        EditorContainer.notes:SetTextColor(0.68, 0.58, 0.51);
    elseif index == 2 then
        EditorContainer.notes:SetFontObject(NarciAchievementTextBlack);
        EditorContainer.notes:SetTextColor(0, 0, 0);
    else
        EditorContainer.notes:SetFontObject(NarciAchievementText);
        EditorContainer.notes:SetTextColor(0.5, 0.5, 0.5);
    end

    local inspectedAchievementID = AchievementCards[-1].id;
    if inspectedAchievementID then
        local id, name, points, completed, month, day, year, description, flags, icon, rewardText = DataProvider:GetAchievementInfo(inspectedAchievementID);
        FormatAchievementCardByIndex(-1, id, name, points, completed, month, day, year, description, flags, icon, rewardText);
    end
    
    if SummaryFrame:IsVisible() then
        UpdateSummaryFrame();
    else
        if not DIYContainer:IsShown() then
            local categoryID = DataProvider.currentCategory;
            SelectCategory(categoryID);
        end
    end

    --Search Results:
    local ResultFrame = MainFrame.HeaderFrame.SearchBox.ClipFrame.ResultFrame;
    local resultButtonGap;
    if index == 3 then
        resultButtonGap = -1;
    else
        resultButtonGap = -2
    end
    ResultFrame.background:SetTexture(texturePrefix.."SearchResultFrame");
    for i = 1, #ResultFrame.buttons do
        ResultFrame.buttons[i].background:SetTexture(texturePrefix.."ResultButton");
        ResultFrame.buttons[i].mask:SetTexture(texturePrefix.."ResultButtonMask");
        if i ~= 1 then
            ResultFrame.buttons[i]:SetPoint("TOP", ResultFrame.buttons[i - 1], "BOTTOM", 0, resultButtonGap);
        end
    end

    --Border Skin
    local HeaderFrame = MainFrame.HeaderFrame;
    HeaderFrame.background:SetTexture(texturePrefix.."BoxHeaderBorder");
    HeaderFrame.mask:SetTexture(texturePrefix.."BoxHeaderBorderMask");

    MainFrame.background:SetTexture(texturePrefix.."BoxRight");
    MainFrame.categoryBackground:SetTexture(texturePrefix.."BoxLeft");

    AchievementContainer.OverlayFrame.top:SetTexture(texturePrefix.."BoxRight");
    AchievementContainer.OverlayFrame.bottom:SetTexture(texturePrefix.."BoxRight");
    AchievementContainer.scrollBar.Thumb:SetTexture(texturePrefix.."SliderThumb");
    
    CategoryContainer.OverlayFrame.top:SetTexture(texturePrefix.."BoxLeft");
    CategoryContainer.OverlayFrame.bottom:SetTexture(texturePrefix.."BoxLeft");
    CategoryContainer.scrollBar.Thumb:SetTexture(texturePrefix.."SliderThumb");

    DIYContainer.OverlayFrame.top:SetTexture(texturePrefix.."BoxRight");
    DIYContainer.OverlayFrame.bottom:SetTexture(texturePrefix.."BoxRight");
    DIYContainer.scrollBar.Thumb:SetTexture(texturePrefix.."SliderThumb");

    EditorContainer.OverlayFrame.top:SetTexture(texturePrefix.."BoxLeft");
    EditorContainer.OverlayFrame.bottom:SetTexture(texturePrefix.."BoxLeft");
    EditorContainer.scrollBar.Thumb:SetTexture(texturePrefix.."SliderThumb");
    
    --Scroll frame inner Shadow
    local showShadow = index == 1;
    AchievementContainer.OverlayFrame.topShadow:SetShown(showShadow);
    AchievementContainer.OverlayFrame.bottomShadow:SetShown(showShadow);
    DIYContainer.OverlayFrame.topShadow:SetShown(showShadow);
    DIYContainer.OverlayFrame.bottomShadow:SetShown(showShadow);
    CategoryContainer.OverlayFrame.topShadow:SetShown(showShadow);
    CategoryContainer.OverlayFrame.bottomShadow:SetShown(showShadow);
    EditorContainer.OverlayFrame.topShadow:SetShown(showShadow);
    EditorContainer.OverlayFrame.bottomShadow:SetShown(showShadow);

    --Category Buttons
    local cateButtons = {CategoryButtons.player.buttons, CategoryButtons.guild.buttons, CategoryButtons.stats.buttons};
    for _, buttons in pairs(cateButtons) do
        for i = 1, #buttons do
            if buttons[i].isParentButton then
                buttons[i].background:SetTexture(texturePrefix.."CategoryButton");
            else
                buttons[i].background:SetTexture(texturePrefix.."SubCategoryButton");
            end
            buttons[i].fill:SetTexture(texturePrefix.."CategoryButtonBar");
            buttons[i].fillEnd:SetTexture(texturePrefix.."CategoryButtonBar");
        end
    end

    HeaderFrame.fill:SetTexture(texturePrefix.."CategoryButtonBar");
    HeaderFrame.fillEnd:SetTexture(texturePrefix.."CategoryButtonBar");

    --Header Reposition
    local offsetY = 0;
    if index == 2 then
        offsetY = -6;
    end

    local FilterButton = MainFrame.FilterButton;
    FilterButton:ClearAllPoints();
    FilterButton:SetPoint("TOPRIGHT", MainFrame, "TOP", -2, -12 + offsetY);
    FilterButton.texture:SetTexture(texturePrefix.."DropDownButton");

    local CloseButton = MainFrame.CloseButton;
    CloseButton:ClearAllPoints();
    CloseButton:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -11, -11 + offsetY);

    local SearchBox = HeaderFrame.SearchBox
    SearchBox:ClearAllPoints();
    SearchBox:SetPoint("TOPRIGHT", HeaderFrame, "TOPRIGHT", -67, -5 + offsetY);

    local points = HeaderFrame.points;
    points:ClearAllPoints();
    points:SetPoint("TOPRIGHT", HeaderFrame, "TOP", 137, -17 + offsetY);

    local reference = HeaderFrame.reference;  --Summary All Points/Achv
    if index == 2 then
        reference:SetHeight(35);
    else
        reference:SetHeight(32);
    end

    local CloseButton = MainFrame.CloseButton;
    CloseButton.texture:SetTexture(texturePrefix.."CloseButton");
    if index == 2 then
        CloseButton:SetSize(39, 26);
    else
        CloseButton:SetSize(36, 26);
    end

    SummaryButton:ClearAllPoints();
    SummaryButton:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 32, -8 + offsetY);
    SummaryButton.texture:SetTexture(texturePrefix.."SummaryButton");


    --Tab buttons
    for i = 1, #TabButtons do
        TabButtons[i]:SetButtonTexture(texturePrefix.."TabButton");
        if index == 1 then
            TabButtons[i]:SetTextOffset(28);
        else
            TabButtons[i]:SetTextOffset(20);
        end
    end


    --AlertSystem

    --Floating Cards
    FloatingCard:SetTheme(index);
end


--------------------------------------------------------------------
local function UpdateTrackAchievements()
    local changedAchievementID = DataProvider:GetTrackedAchievements();
    if not changedAchievementID then return end

    local currentCategory = DataProvider.currentCategory;
    local categoryID = DataProvider:GetAchievementCategory(changedAchievementID);
    local shouldUpdate;
    if categoryID == currentCategory then
        shouldUpdate = true;
    end
    local isTracked = DataProvider:IsTrackedAchievement(changedAchievementID);

    if shouldUpdate then
        local numAchievements = DataProvider.numAchievements or 0;
        local card;
        for i = 1, numAchievements do
            card = AchievementCards[i];
            if card then
                if card.id == changedAchievementID then
                    card.trackIcon:SetShown(isTracked);
                end
            else
                break
            end
        end
    end

    if (InspectionFrame:IsShown()) and (changedAchievementID == InspectionFrame.Card.id) then
        InspectionFrame.Card.trackIcon:SetShown(isTracked);
    end
end


-----------------------------------------------------------------------------
function NarciAchievementFrameMixin:Init()
    DataProvider:GetTrackedAchievements();
    PinUtil:Load();
    addon.LoadDIY();
    self:RegisterEvent("TRACKED_ACHIEVEMENT_LIST_CHANGED");
    BuildCategoryStructure(2);
    BuildCategoryStructure(1);
    BuildCategoryStructure(3);
    InitializeFrame(Narci_AchievementFrame);
    self:RegisterEvent("ACHIEVEMENT_EARNED");
    self:RegisterEvent("CRITERIA_EARNED");
    self.Init = nil;
end

function NarciAchievementFrameMixin:InspectCard(card, playAnimation)
    InspectCard(card, playAnimation);
end


local function OnAchivementEarned(achievementID)
    DataProvider:UpdateAchievementCache(achievementID);
    RefreshInspection(achievementID);
    
    local categoryID = DataProvider:GetAchievementCategory(achievementID);
    if categoryID then
        if DataProvider.achievementOrderCache[categoryID] then
            wipe(DataProvider.achievementOrderCache[categoryID]);
        end
        UpdateCategoryButtonProgressByCategoryID(categoryID);
        if categoryID == DataProvider.currentCategory then
            if MainFrame:IsShown() then
                SelectCategory(categoryID);
            else
                MainFrame.pendingCategoryID = categoryID;
            end
            return;
        end
    end
    if SummaryFrame:IsShown() then
        MainFrame.pendingUpdate = true;
    end
end

local EventListener = CreateFrame("Frame");
EventListener:RegisterEvent("ACHIEVEMENT_EARNED");
EventListener:RegisterEvent("TRACKED_ACHIEVEMENT_LIST_CHANGED");

EventListener:SetScript("OnEvent", function(self, event, ...)
    if event == "ACHIEVEMENT_EARNED" then
        local achievementID = ...;
        OnAchivementEarned(achievementID);
        if not self.pauseUpdate then
            self.pauseUpdate = true;
            After(0, function()
                UpdateHeaderFrame(isGuildView);
                self.pauseUpdate = nil;
            end);
        end
    elseif event == "TRACKED_ACHIEVEMENT_LIST_CHANGED" then
        UpdateTrackAchievements();
    end
end)



addon.ReskinButton = ReskinButton;
addon.FormatAchievementCard = FormatAchievementCard;

local function IsDarkTheme()
    return isDarkTheme;
end

addon.IsDarkTheme = IsDarkTheme;