local _, addon = ...
local API = addon.API;
local L = addon.L;
local CallbackRegistry = addon.CallbackRegistry;

local floor = math.floor;
local sqrt = math.sqrt;
local tostring = tostring;
local find = string.find;

local LOCALE = GetLocale and GetLocale() or "enUS";


local function AlwaysNil(arg)
end
API.Nop = AlwaysNil;

local function AlwaysFalse(arg)
    --used to replace non-existent API in Classic
    return false
end
API.AlwaysFalse = AlwaysFalse;

local function AlwaysTrue(arg)
    return true
end
API.AlwaysTrue = AlwaysTrue;

local function AlwaysZero(arg)
    return 0
end

local function CopyEnum(name)
    local tbl = {};
    if Enum and Enum[name] then
        for k, v in pairs(Enum[name]) do
            tbl[k] = v;
        end
    end
    return tbl
end

do  -- Math
    local function GetPointsDistance2D(x1, y1, x2, y2)
        return sqrt( (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2))
    end
    API.GetPointsDistance2D = GetPointsDistance2D;

    local function Round(n)
        return floor(n + 0.5);
    end
    API.Round = Round;

    local function Clamp(value, min, max)
        if value > max then
            return max
        elseif value < min then
            return min
        end
        return value
    end
    API.Clamp = Clamp;

    local function Lerp(startValue, endValue, amount)
        return (1 - amount) * startValue + amount * endValue;
    end
    API.Lerp = Lerp;

    local function ClampLerp(startValue, endValue, amount)
        amount = Clamp(amount, 0, 1);
        return Lerp(startValue, endValue, amount);
    end
    API.ClampLerp = ClampLerp;

    local function Saturate(value)
        return Clamp(value, 0.0, 1.0);
    end

    local TARGET_FRAME_PER_SEC = 60.0;

    local function DeltaLerp(startValue, endValue, amount, timeSec)
        return Lerp(startValue, endValue, Saturate(amount * timeSec * TARGET_FRAME_PER_SEC));
    end
    API.DeltaLerp = DeltaLerp;


    do  --Used for currency amount. Simplified from Blizzard's "AbbreviateNumbers" in UIParent.lua
        local ABBREVIATION_K = L["Abbrev Breakpoint 1000"];    --Asian language cut off: 10,000

        if LOCALE == "zhCN" or LOCALE == "zhTW" or LOCALE == "koKR" then
            local ABBREVIATION_W = L["Abbrev Breakpoint 10000"];
            API.AbbreviateNumbers = function(value)
                if value > 100000 then
                    return floor(value / 10000) .. ABBREVIATION_W
                elseif value >= 10000 then
                    return floor(value / 1000)/10 .. ABBREVIATION_W
                --elseif value >= 1000 then
                --    return floor(value / 100)/10 .. ABBREVIATION_K
                else
                    return tostring(value)
                end
            end
        else
            API.AbbreviateNumbers = function(value)
                if value > 10000 then
                    return floor(value / 1000) .. ABBREVIATION_K
                elseif value > 1000 then
                    return floor(value / 100)/10 .. ABBREVIATION_K
                else
                    return tostring(value)
                end
            end
        end
    end
end

do  -- Table
    local function Mixin(object, ...)
        for i = 1, select("#", ...) do
            local mixin = select(i, ...)
            for k, v in pairs(mixin) do
                object[k] = v;
            end
        end
        return object
    end
    API.Mixin = Mixin;

    local function CreateFromMixins(...)
        return Mixin({}, ...)
    end
    API.CreateFromMixins = CreateFromMixins;
end

do  -- Pixel
    local GetPhysicalScreenSize = GetPhysicalScreenSize;
    local SCREEN_WIDTH, SCREEN_HEIGHT = GetPhysicalScreenSize();
    local UI_SCALE_RATIO = 1 / UIParent:GetEffectiveScale();

    local function GetPixelPertectScale()
        return (768/SCREEN_HEIGHT)
    end
    API.GetPixelPertectScale = GetPixelPertectScale;

    local function UpdateTextureSliceScale(textureSlice)
        --Temp Fix for 10.1.7 change
        textureSlice:SetScale(GetPixelPertectScale());
    end
    API.UpdateTextureSliceScale = UpdateTextureSliceScale;

    --if addon.IS_CATA then
        --Era 1.15.3 has this issue too
        --API.UpdateTextureSliceScale = AlwaysNil;  --4.4.1 has this issue too
    --end

    local function GetPixelForScale(scale, pixelSize)
        if pixelSize then
            return pixelSize * (768/SCREEN_HEIGHT)/scale
        else
            return (768/SCREEN_HEIGHT)/scale
        end
    end
    API.GetPixelForScale = GetPixelForScale;

    local function GetPixelForWidget(widget, pixelSize)
        local scale = widget:GetEffectiveScale();
        return GetPixelForScale(scale, pixelSize);
    end
    API.GetPixelForWidget = GetPixelForWidget;

    local function GetSizeInPixel(scale, size)
        return size * scale / (768/SCREEN_HEIGHT)
    end
    API.GetSizeInPixel = GetSizeInPixel;

    local function DisableSharpening(texture)
        texture:SetTexelSnappingBias(0);
        texture:SetSnapToPixelGrid(false);
    end
    API.DisableSharpening = DisableSharpening;

    local function GetBestViewportSize()
        --WorldFrame's size is unaffected by screen resolution
        --Issue caused by occasionally bugged resolution since 11.0? https://github.com/Peterodox/YUI-Dialogue/issues/104
        local viewportWidth, viewportHeight = WorldFrame:GetSize();
        viewportWidth = math.min(viewportWidth, viewportHeight * 16/9);
        return viewportWidth, viewportHeight
    end
    API.GetBestViewportSize = GetBestViewportSize;

    local PixelUtil = CreateFrame("Frame");
    addon.PixelUtil = PixelUtil;

    PixelUtil.objects = {};

    function PixelUtil:AddPixelPerfectObject(object)
        table.insert(self.objects, object);
    end

    function PixelUtil:MarkScaleDirty()
        self.scaleDirty = true;
        self.t = 0;
        self:SetScript("OnUpdate", self.OnUpdate);
    end
    PixelUtil:MarkScaleDirty();

    function PixelUtil:RequireUpdate()
        if self.scaleDirty then
            self.scaleDirty = nil;
            local scale;

            for _, object in ipairs(self.objects) do
                scale = object:GetEffectiveScale();
                object:UpdatePixel(scale);
            end
        end
    end

    function PixelUtil:OnUpdate(elpased)
        self.t = self.t + elpased;
        if self.t > 0.1 then
            self.t = 0;
            self:SetScript("OnUpdate", nil);
            self:RequireUpdate();
        end
    end

    PixelUtil:RegisterEvent("UI_SCALE_CHANGED");
    PixelUtil:RegisterEvent("DISPLAY_SIZE_CHANGED");

    PixelUtil:SetScript("OnEvent", function(self, event, ...)
        SCREEN_WIDTH, SCREEN_HEIGHT = GetPhysicalScreenSize();
        UI_SCALE_RATIO = 1 / UIParent:GetEffectiveScale();
        self:MarkScaleDirty();
    end);


    local GetCursorPosition = GetCursorPosition;

    local function GetScaledCursorPosition()
        local x, y = GetCursorPosition();
        return x*UI_SCALE_RATIO, y*UI_SCALE_RATIO
    end
    API.GetScaledCursorPosition = GetScaledCursorPosition;
end

do  -- Object Pool (Pool needs to Release all objects before reusing) / DynamicPoolMixin (can reuse any inactive object without Releasing All objects)
    local ObjectPoolMixin = {};
    local ipairs = ipairs;
    local tinsert = table.insert;
    local tremove = table.remove;

    function ObjectPoolMixin:Release()
        for i, object in ipairs(self.objects) do
            if i <= self.numActive then
                self.Remove(object);
            else
                break
            end
        end
        self.numActive = 0;
    end

    function ObjectPoolMixin:Acquire()
        local n = self.numActive + 1;
        self.numActive = n;

        if not self.objects[n] then
            self.objects[n] = self.Create();
        end

        if self.OnAcquired then
            self.OnAcquired(self.objects[n]);
        end

        self.objects[n]:Show();

        return self.objects[n]
    end

    function ObjectPoolMixin:OnLoad()
        self.numActive = 0;
        self.objects = {};
    end

    function ObjectPoolMixin:CallActive(method)
        for i = 1, self.numActive do
            self.objects[i][method](self.objects[i]);
        end
    end

    function ObjectPoolMixin:CallAllObjects(method, ...)
        for i, obj in ipairs(self.objects) do
            obj[method](obj, ...);
        end
    end

    function ObjectPoolMixin:ProcessActiveObjects(func)
        for i = 1, self.numActive do
            func(self.objects[i]);
        end
    end

    function ObjectPoolMixin:ProcessAllObjects(func)
        for i, obj in ipairs(self.objects) do
            func(obj);
        end
    end

    function ObjectPoolMixin:GetObjectsByPredicate(pred)
        local tbl = {};
        for i, obj in ipairs(self.objects) do
            if pred(obj) then
                tinsert(tbl, obj);
            end
        end
        return tbl
    end

    function ObjectPoolMixin:GetActiveObjects()
        local tbl = {};
        for i = 1, self.numActive do
            tinsert(tbl, self.objects[i]);
        end
        return tbl
    end

    function ObjectPoolMixin:EnumerateActive()
        local activeObjects = self:GetActiveObjects();
        return ipairs(activeObjects)
    end

    local function RemoveObject(object)
        object:Hide();
        object:ClearAllPoints();
    end

    local function CreateObjectPool(createFunc, removeFunc, onAcquiredFunc)
        local pool = API.CreateFromMixins(ObjectPoolMixin);
        pool:OnLoad();
        pool.Create = createFunc;
        pool.Remove = removeFunc or RemoveObject;
        pool.OnAcquired = onAcquiredFunc;
        return pool
    end
    API.CreateObjectPool = CreateObjectPool;




    local DynamicPoolMixin = {};

    function DynamicPoolMixin:ReleaseAll()
        local removeFunc = self.Remove or RemoveObject;
        for obj, active in pairs(self.activeObjects) do
            if active then
                removeFunc(obj);
            end
        end
        self.activeObjects = {};
        self.bins = {};
        for i, obj in ipairs(self.allObjects) do
            self.bins[i] = obj;
        end
    end

    function DynamicPoolMixin:RecycleObject(obj)
        if self.activeObjects[obj] then
            self.activeObjects[obj] = nil;
            if self.Remove then
                self.Remove(obj);
            else
                RemoveObject(obj);
            end
        end
        tinsert(self.bins, obj);
    end

    function DynamicPoolMixin:Acquire()
        local obj = tremove(self.bins);
        if not obj then
            obj = self.Create();
            obj.Release = self.ReleaseObject;
            tinsert(self.allObjects, obj);
        end
        if self.OnAcquired then
            self.OnAcquired(obj);
        end
        self.activeObjects[obj] = true;
        return obj
    end

    function DynamicPoolMixin:CallActive(method, arg1, arg2, arg3, arg4)
        for obj, active in pairs(self.activeObjects) do
            obj[method](obj, arg1, arg2, arg3, arg4);
        end
    end

    function DynamicPoolMixin:EnumerateActive()
        return pairs(self.activeObjects);
    end

    function DynamicPoolMixin:DebugGetCount()
        local numTotal = #self.allObjects;
        local numInactive = #self.bins;
        local numActive = 0;
        for obj, active in pairs(self.activeObjects) do
            numActive = numActive + 1;
        end
        print(numTotal, numActive, numInactive);
    end

    local function CreateDynamicObjectPool(createFunc, removeFunc, onAcquiredFunc)
        local pool = API.CreateFromMixins(DynamicPoolMixin);

        pool.allObjects = {};
        pool.bins = {};
        pool.activeObjects = {};

        pool.Create = createFunc;
        pool.Remove = removeFunc or RemoveObject;
        pool.OnAcquired = onAcquiredFunc;

        pool.ReleaseObject = function(obj)
            pool:RecycleObject(obj);
        end

        return pool
    end
    API.CreateDynamicObjectPool = CreateDynamicObjectPool;
end

do  -- String
    local match = string.match;
    local gmatch = string.gmatch;
    local gsub = string.gsub;
    local tinsert = table.insert;

    local function SplitParagraph(text)
        local tbl = {};

        if text then
            local n = 0;
            for v in gmatch(text, "[%C]+") do
                if v ~= " " then
                    n = n + 1;
                    tbl[n] = v;
                end
            end
        end

        return tbl
    end
    API.SplitParagraph = SplitParagraph;


    local READING_CPS = 15; --Vary depends on Language
    local strlenutf8 = strlenutf8;

    local function GetTextReadingTime(text)
        local numWords = strlenutf8(text);
        return API.Clamp(numWords / READING_CPS, 2.75, 8);
    end
    API.GetTextReadingTime = GetTextReadingTime;


    local function ReplaceRegularExpression(formatString)
        return gsub(formatString, "%%d", "%%s")
    end
    API.ReplaceRegularExpression = ReplaceRegularExpression;

    do
        local function UpdateFormat(k)
            if L[k] then
                L[k] = ReplaceRegularExpression(L[k]);
            else
                print("DialogueUI Missing String:", k);
            end
        end

        UpdateFormat("Format Player XP");
        UpdateFormat("Format Gold Amount")
        UpdateFormat("Format Silver Amount")
        UpdateFormat("Format Copper Amount")
    end

    local function GetItemIDFromHyperlink(link)
        local id = match(link, "[Ii]tem:(%d*)");
        if id then
            return tonumber(id)
        end
    end
    API.GetItemIDFromHyperlink = GetItemIDFromHyperlink;

    local function GetGlobalObject(objNameKey)
        --Get object via string "FrameName.Key1.Key2"
        local obj = _G;

        for k in string.gmatch(objNameKey, "[%w_]+") do
            obj = obj[k];
            if not obj then
                return
            end
        end

        return obj
    end
    API.GetGlobalObject = GetGlobalObject;

    local function DoesGlobalObjectExist(objNameKey)
        return GetGlobalObject(objNameKey) ~= nil
    end
    API.DoesGlobalObjectExist = DoesGlobalObjectExist;
end

do  -- NPC Interaction
    local SetUnitCursorTexture = SetUnitCursorTexture;
    local UnitExists = UnitExists;
    local UnitName = UnitName;
    local UnitGUID = UnitGUID;

    local f = CreateFrame("Frame");
    f.texture = f:CreateTexture();
    f.texture:SetSize(1, 1);

    local CursorTextureTypes = {
        ["Cursor Talk"] = "gossip",     --Most interactable NPC that doesn't provide quests
        [4675624] = "direction",        --Guard, asking for direction
    };

    local TexturePrefix = "Interface/AddOns/DialogueUI/Art/Icons/NPCType-";

    local CustomTypeTexture = {
        direction = "Direction",
    };

    local function GetInteractType(unit)
        if UnitExists(unit) then
            --Returns cursor texture (RepairNPC, Transmog, Taxi...)
            --Quest NPC (with question mark) returns nil (there is no type icon on the nameplate)
            SetUnitCursorTexture(f.texture, unit);
            local file = f.texture:GetTexture();
            return file, file and CursorTextureTypes[file]
        end
    end
    API.GetInteractType = GetInteractType;

    local function GetInteractTexture(unit)
        local _, type = GetInteractType(unit);
        if type and CustomTypeTexture[type] then
            return TexturePrefix..CustomTypeTexture[type]
        end
    end
    API.GetInteractTexture = GetInteractTexture;


    local IsInteractingWithNpcOfType = C_PlayerInteractionManager.IsInteractingWithNpcOfType;
    local TYPE_GOSSIP = Enum.PlayerInteractionType and Enum.PlayerInteractionType.Gossip or 3;
    local TYPE_QUEST_GIVER = Enum.PlayerInteractionType and Enum.PlayerInteractionType.QuestGiver or 4;

    local function IsInteractingWithGossip()
        return IsInteractingWithNpcOfType(TYPE_GOSSIP)
    end
    API.IsInteractingWithGossip = IsInteractingWithGossip;

    local function IsInteractingWithQuestGiver()
        return IsInteractingWithNpcOfType(TYPE_QUEST_GIVER)
    end
    API.IsInteractingWithQuestGiver = IsInteractingWithQuestGiver;

    local function IsInteractingWithDialogNPC()
        return (IsInteractingWithNpcOfType(TYPE_GOSSIP) or IsInteractingWithNpcOfType(TYPE_QUEST_GIVER))
    end
    API.IsInteractingWithDialogNPC = IsInteractingWithDialogNPC;

    --A helper to close gossip interaction
    --CloseGossip twice in a row cause issue: UI like MerchantFrame won't close itself, no frame portrait

    local CloseGossip = C_GossipInfo.CloseGossip;

    local function ResetCloseStatus(self, elapsed)
        self.isClosing = false;
        self:SetScript("OnUpdate", nil);
        if f.closeInteraction then
            CloseGossip();
        end
    end

    local function CloseGossipInteraction()
        f.closeInteraction = true;
        if not f.isClosing then
            f.isClosing = true;
            f:SetScript("OnUpdate", ResetCloseStatus);
        end
    end
    API.CloseGossipInteraction = CloseGossipInteraction;

    local function CancelClosingGossipInteraction()
        if f.isClosing then
            f.closeInteraction = false;
        end
    end
    API.CancelClosingGossipInteraction = CancelClosingGossipInteraction;


    local CGListener = CreateFrame("Frame");
    do
        local function IsPlayingCutscene()
            if (not CGListener.CinematicFrame) and CinematicFrame then
                CGListener.CinematicFrame = CinematicFrame;
            end

            if (not CGListener.MovieFrame) and MovieFrame then
                CGListener.MovieFrame = MovieFrame;
            end

            if (CGListener.CinematicFrame and CGListener.CinematicFrame:IsShown()) or (CGListener.MovieFrame and CGListener.MovieFrame:IsShown()) then
                --Cinematic played upon loading screen finished doesn't trigger events due to loading order? 11.1.0 Undermine
                return true
            else
                return false
            end
        end
        API.IsPlayingCutscene = IsPlayingCutscene;


        CGListener:RegisterEvent("CINEMATIC_START");
        CGListener:RegisterEvent("CINEMATIC_STOP");
        CGListener:RegisterEvent("PLAY_MOVIE");
        CGListener:RegisterEvent("STOP_MOVIE");
        --CGListener:RegisterEvent("LOADING_SCREEN_DISABLED");

        function CGListener:CheckStatus()
            self:SetScript("OnUpdate", self.OnUpdate_CheckStatus);
        end

        function CGListener:OnUpdate_CheckStatus(elapsed)
            self:SetScript("OnUpdate", nil);
            self.isPlayingCutscene = IsPlayingCutscene();
            if self.isPlayingCutscene then
                CallbackRegistry:Trigger("PlayCutscene");
            end
        end

        function CGListener:PauseUpdate()
            self.paused = true;
            self:SetScript("OnUpdate", self.OnUpdate_PauseUpdate);
        end

        function CGListener:OnUpdate_PauseUpdate(elapsed)
            self:SetScript("OnUpdate", nil);
            self.paused = nil;
        end

        CGListener:SetScript("OnEvent", function(self, event, ...)
            --self:CheckStatus();
            if event == "CINEMATIC_START" or event == "PLAY_MOVIE" then
                if not self.paused then
                    self:PauseUpdate();
                    CallbackRegistry:Trigger("PlayCutscene");
                end
            end
        end);
    end


    local function SetPlayCutsceneCallback(callback)
        CallbackRegistry:Register("PlayCutscene", callback);
    end
    API.SetPlayCutsceneCallback = SetPlayCutsceneCallback;


    --Model Size Evaluation
    local ModelScene, NPCActor, MountActor, CameraController;
    --local IsUnitModelReadyForUI = IsUnitModelReadyForUI;


    DUIUtilityActorMixin = {};

    function DUIUtilityActorMixin:EvaluateNPCHeight()
        local bottomX, bottomY, bottomZ, topX, topY, topZ = self:GetActiveBoundingBox(); -- Could be nil for invisible models
        if bottomX and bottomY and bottomZ and topX and topY and topZ then
            local width = topX - bottomX;
            local depth = topY - bottomY;
            local height = topZ - bottomZ;

            --local widthScale = width / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.width;
            --local depthScale = depth / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.depth;
            --local heightScale = height / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.height;
            --print(width, depth, height);

            if CameraController and height then
                CameraController:OnModelEvaluationComplete(height);
            end
        end
        self:ClearModel();
    end

    function DUIUtilityActorMixin:GetMountScale()
        --local calcMountScale = MountActor:CalculateMountScale(PlayerActor);
        --local inverseScale = 1 / calcMountScale;
        --the results are always 1.0 after certain patch
        --print(self.mountName, calcMountScale, inverseScale);
        local bottomX, bottomY, bottomZ, topX, topY, topZ = self:GetActiveBoundingBox(); -- Could be nil for invisible models
        if bottomX and bottomY and bottomZ and topX and topY and topZ then
            local width = topX - bottomX;
            local depth = topY - bottomY;
            local height = topZ - bottomZ;

            local widthScale = width / 2.1;
            local depthScale = depth / 1.1;
            local heightScale = height / 1;
            print(width, depth, height);

            local scale = widthScale * depthScale * heightScale;
            print(scale);

            local volumn = width * height * depth;
            print(volumn)
        end
        self:ClearModel();
    end

    function DUIUtilityActorMixin:OnModelLoaded()
        if self.onModelLoadedCallback then
            self.onModelLoadedCallback(self);
        end
    end

    --0.8, 1.0, 1.6: Goblin
    --0.9, 1.1, 2.1: Human
    --1.1, 1.3, 1.7: Dwarf
    --0.9, 1.2, 2.5: NElf M
    --0.8, 0.8, 2.1: VElf F
    --0.5, 0.8, 1.1: Gnome
    --3.2, 2.0, 2.9: Malicia
    --3.8, 3.5, 5.2: Draknoid
    --2.9, 5.1, 7.7: Watcher Koranos
    --30, 20, 14:    Dragon Aspect

    local function CreateModelScene()
        if not ModelScene then
            ModelScene = CreateFrame("ModelScene");
            ModelScene:SetSize(1, 1);
        end
    end

    local function EvaluateUnitSize(unit)
        CreateModelScene();

        if not NPCActor then
            NPCActor = ModelScene:CreateActor(nil, "DUIUtilityActorTemplate");
            NPCActor.onModelLoadedCallback = NPCActor.EvaluateNPCHeight;
        end

        local success = NPCActor:SetModelByUnit(unit);
        return success
    end

    local function EvaluateNPCSize()
        return EvaluateUnitSize("npc");
    end
    API.EvaluateNPCSize = EvaluateNPCSize;


    local function EvaluateMountScale(mountID)
        CreateModelScene();

        if not MountActor then
            MountActor = ModelScene:CreateActor(nil, "DUIUtilityActorTemplate");
            MountActor.onModelLoadedCallback = MountActor.GetMountScale;
        end

        if not PlayerActor then
            PlayerActor = ModelScene:CreateActor(nil, "DUIUtilityActorTemplate");
            PlayerActor.onModelLoadedCallback = function() print("LOADED") end;
        end

        local hasAlternateForm, inAlternateForm = C_PlayerInfo.GetAlternateFormInfo();
        local sheatheWeapon = true;
        local autodress = false;
		local hideWeapon = true;
        local useNativeForm = not inAlternateForm;
        PlayerActor:SetScale(1);
        
        local result = PlayerActor:SetModelByUnit("player", sheatheWeapon, autodress, hideWeapon, useNativeForm);
        if result then
            local creatureDisplayID, _, _, isSelfMount, _, modelSceneID, animID, spellVisualKitID, disablePlayerMountPreview = C_MountJournal.GetMountInfoExtraByID(mountID);
            MountActor.mountName = C_MountJournal.GetMountInfoByID(mountID);
            local showCustomization = true;
            MountActor:ClearModel();
            MountActor:SetModelByCreatureDisplayID(creatureDisplayID, showCustomization);
        end
    end
    API.EvaluateMountScale = EvaluateMountScale;

    local function SetCameraController(controller)
        CameraController = controller;
    end
    addon.SetCameraController = SetCameraController;


    local match = string.match;

    local function GetCreatureIDFromGUID(guid)
        --Including Creature, Vehicle, GameObject
        local id = guid and match(guid, "^%a+%-0%-%d*%-%d*%-%d*%-(%d*)");
        if id then
            return tonumber(id)
        end
    end
    API.GetCreatureIDFromGUID = GetCreatureIDFromGUID;

    local function GetCurrentNPCInfo()
        local name = UnitName("npc");
        local creatureID = GetCreatureIDFromGUID(UnitGUID("npc"));
        if creatureID then
            name = name or "";
            return name, creatureID
        end
    end
    API.GetCurrentNPCInfo = GetCurrentNPCInfo;

    local function GetUnitTypeAndID(unit)
        unit = unit or "npc";
        local guid = UnitGUID("npc");
        if guid then
            local unitType, id = match(guid, "^(%a+)%-0%-%d*%-%d*%-%d*%-(%d*)");
            if unitType and id then
                return unitType, tonumber(id)
            end
        end
    end
    API.GetUnitTypeAndID = GetUnitTypeAndID;

    local SkippedNPC = {
        [94398] = true,     --Fleet Command Table
        [94399] = true,     --Fleet Command Table
        [138704] = true,    --Mission Command Table
        [138706] = true,    --Mission Command Table
        [147244] = true,    --Mission Command Table
        [215758] = true,    --Mission Command Table
    };
    local function IsInteractingWithGameObject()
        local unitType, id = GetUnitTypeAndID("npc");
        if id then
            if unitType == "GameObject" or unitType == "Vehicle" then
                return true
            elseif unitType == "Creature" and id then
                id = tonumber(id) or 0;
                return SkippedNPC[id]
            end
        end
    end
    API.IsInteractingWithGameObject = IsInteractingWithGameObject;

    local SCOUTING_MAP = ADVENTURE_MAP_TITLE or "Scouting Map";
    local UnitClass = UnitClass;

    local function IsTargetAdventureMap()
        local className = UnitClass("npc");
        return className == SCOUTING_MAP
    end
    API.IsTargetAdventureMap = IsTargetAdventureMap;
end

do  -- Easing
    local EasingFunctions = {};
    addon.EasingFunctions = EasingFunctions;


    local sin = math.sin;
    local cos = math.cos;
    local pow = math.pow;
    local pi = math.pi;

    --t: total time elapsed
    --b: beginning position
    --e: ending position
    --d: animation duration

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

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

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

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

    function EasingFunctions.outQuint(t, b, e, d)
        t = t / d
        return (b - e)* (pow(1 - t, 5) - 1) + b
    end

    function EasingFunctions.inQuad(t, b, e, d)
        t = t / d
        return (e - b) * pow(t, 2) + b
    end

    function EasingFunctions.none(t, b, e, d)
        return e
    end

    function EasingFunctions.noChange(t, b, e, d)
        return b
    end
end

do  -- Quest
    local ICON_PATH = "Interface/AddOns/DialogueUI/Art/Icons/";
    local Enum_QuestClassification = CopyEnum("QuestClassification");

    local QuestGetAutoAccept = QuestGetAutoAccept or AlwaysFalse;
    local C_QuestLog = C_QuestLog;
    local IsOnQuest = C_QuestLog.IsOnQuest;
    local GetQuestReward = GetQuestReward;
    local ReadyForTurnIn = C_QuestLog.ReadyForTurnIn or IsQuestComplete or AlwaysFalse;
    local QuestIsFromAreaTrigger = QuestIsFromAreaTrigger or AlwaysFalse;
    local GetSuggestedGroupSize = GetSuggestedGroupSize or AlwaysZero;
    local IsQuestTrivial = C_QuestLog.IsQuestTrivial or AlwaysFalse;
    local IsCampaignQuest = (C_CampaignInfo and C_CampaignInfo.IsCampaignQuest) or AlwaysFalse;
    local IsQuestTask = C_QuestLog.IsQuestTask or AlwaysFalse;
    local IsWorldQuest = C_QuestLog.IsWorldQuest or AlwaysFalse;
    local GetRewardSkillPoints = GetRewardSkillPoints or AlwaysFalse;
    local GetRewardArtifactXP = GetRewardArtifactXP or AlwaysZero;
    local QuestCanHaveWarModeBonus = C_QuestLog.QuestCanHaveWarModeBonus or AlwaysFalse;
    local QuestHasQuestSessionBonus = C_QuestLog.QuestHasQuestSessionBonus or AlwaysFalse;
    local GetQuestItemInfoLootType = GetQuestItemInfoLootType or AlwaysZero;
    local GetTitleForQuestID = C_QuestLog.GetTitleForQuestID or C_QuestLog.GetQuestInfo or AlwaysFalse;
    local GetQuestObjectives = C_QuestLog.GetQuestObjectives;
    local GetQuestTimeLeftSeconds = C_TaskQuest and C_TaskQuest.GetQuestTimeLeftSeconds or AlwaysNil;
    local IsQuestFlaggedCompletedOnAccount = C_QuestLog.IsQuestFlaggedCompletedOnAccount or AlwaysFalse;
    local GetLogIndexForQuestID = C_QuestLog.GetLogIndexForQuestID or GetQuestLogIndexByID or AlwaysNil;
    local GetNumQuestLeaderBoards = GetNumQuestLeaderBoards;
    local GetQuestLogLeaderBoard = GetQuestLogLeaderBoard;
    local GetQuestClassification = C_QuestInfoSystem.GetQuestClassification or AlwaysNil;
    local IsAccountQuest = C_QuestLog.IsAccountQuest or AlwaysFalse;

    API.IsQuestFlaggedCompletedOnAccount = IsQuestFlaggedCompletedOnAccount;

    local function IsPlayerOnQuest(questID)
        if questID then
            return IsOnQuest(questID)
        end
    end
    API.IsPlayerOnQuest = IsPlayerOnQuest;

    if AcknowledgeAutoAcceptQuest then
        API.AcknowledgeAutoAcceptQuest = AcknowledgeAutoAcceptQuest;
    else    --Classic
        API.AcknowledgeAutoAcceptQuest = AcceptQuest;
    end

    --TWW
    local GetQuestCurrency;

    if C_QuestOffer and C_QuestOffer.GetQuestRewardCurrencyInfo then
        local GetQuestRequiredCurrencyInfo = C_QuestOffer.GetQuestRequiredCurrencyInfo;
        local GetQuestRewardCurrencyInfo = C_QuestOffer.GetQuestRewardCurrencyInfo;

        function GetQuestCurrency(questInfoType, index)
            --Unifiy two APIs and their payload structures:
            --"duiDisplayedAmount" for displaying the ItemCount

            local tbl;

            if questInfoType == "reward" or questInfoType == "choice" then
                tbl = GetQuestRewardCurrencyInfo(questInfoType, index);
                tbl.duiDisplayedAmount = tbl.totalRewardAmount;
            elseif questInfoType == "required" then
                tbl = GetQuestRequiredCurrencyInfo(index);
                tbl.duiDisplayedAmount = tbl.requiredAmount;
            end

            return tbl
        end
    else
        local GetQuestCurrencyInfo = GetQuestCurrencyInfo;
        local GetQuestCurrencyID = GetQuestCurrencyID;

        function GetQuestCurrency(questInfoType, index)
            local name, texture, amount, quality = GetQuestCurrencyInfo(questInfoType, index)
            local currencyID = GetQuestCurrencyID(questInfoType, index);

            local tbl = {
                texture = texture,
                name = name,
                currencyID = currencyID,
                quality = quality or 0,
                baseRewardAmount = amount,
                bonusRewardAmount = 0,
                totalRewardAmount = amount,
                questRewardContextFlags = nil,
                requiredAmount = 0,
                duiDisplayedAmount = amount;
            };

            return tbl
        end
    end
    API.GetQuestCurrency = GetQuestCurrency;

    local function GetQuestLogProgress(questID)
        local questLogIndex = GetLogIndexForQuestID(questID);
        if questLogIndex then
            local numObjectives = GetNumQuestLeaderBoards(questLogIndex);
            if numObjectives > 0 then
                local str;
                local text, objectiveType, finished;
                local n = 0;
                for i = 1, numObjectives do
                    text, objectiveType, finished = GetQuestLogLeaderBoard(i, questLogIndex);
                    if text then
                        if str then
                            str = str.."\n".."- "..text;
                        else
                            str = "- "..text;
                        end
                    end
                end
                return str
            end
        end
    end
    API.GetQuestLogProgress = GetQuestLogProgress;

    --Classic
    API.QuestGetAutoAccept = QuestGetAutoAccept;
    API.QuestIsFromAreaTrigger = QuestIsFromAreaTrigger;
    API.GetSuggestedGroupSize = GetSuggestedGroupSize;
    API.GetRewardSkillPoints = GetRewardSkillPoints;
    API.GetRewardArtifactXP = GetRewardArtifactXP;
    API.QuestCanHaveWarModeBonus = QuestCanHaveWarModeBonus;
    API.QuestHasQuestSessionBonus = QuestHasQuestSessionBonus;
    API.GetQuestItemInfoLootType = GetQuestItemInfoLootType;
    API.GetTitleForQuestID = GetTitleForQuestID;
    API.IsAccountQuest = IsAccountQuest;

    if GetAvailableQuestInfo then
        API.GetAvailableQuestInfo = GetAvailableQuestInfo;
    else
        API.GetAvailableQuestInfo = function()
            return false, 0, false, false, 0
        end
    end

    if GetActiveQuestID then
        API.GetActiveQuestID = GetActiveQuestID;
    else
        API.GetActiveQuestID = function()
            return 0
        end
    end

    local function CompleteCurrentQuest(rewardChoiceID, isAutoComplete)
        rewardChoiceID = rewardChoiceID or 0;
        GetQuestReward(rewardChoiceID);
        CallbackRegistry:Trigger("TriggerQuestFinished", isAutoComplete);   --In some cases game doesn't fire QUEST_FINISHED after completing a quest?
    end
    API.CompleteCurrentQuest = CompleteCurrentQuest;

    local QuestMixin = {};
    do
        function QuestMixin:Refresh()
            self.classification = GetQuestClassification(self.questID) or -1;
            self.isTrivial = IsQuestTrivial(self.questID);
        end
    end

    local function BuildQuestInfo(questInfo)
        questInfo.Refresh = QuestMixin.Refresh;

        local class = GetQuestClassification(questInfo.questID) or -1;
        questInfo.classification = class;

        if questInfo.isOnQuest == nil then
            questInfo.isOnQuest = IsOnQuest(questInfo.questID);
        end

        if not questInfo.isComplete then     --Classic Shenanigans
            questInfo.isComplete = ReadyForTurnIn(questInfo.questID);
        end

        if questInfo.isCampaign == nil then
            questInfo.isCampaign = IsCampaignQuest(questInfo.questID);  --QuestMixin uses C_CampaignInfo.GetCampaignID() ~= 0. Wonder what's the difference here;
        end

        if questInfo.isLegendary == nil then
            questInfo.isLegendary = class == Enum_QuestClassification.Legendary;
        end

        if questInfo.isImportant == nil then
            questInfo.isImportant = class == Enum_QuestClassification.Important;
        end

        if questInfo.isTrivial == nil then
            questInfo.isTrivial  = IsQuestTrivial(questInfo.questID);   --May not get the correct value during the first call
        end

        if questInfo.frequency == nil then
            --frequency may be inaccurate?
            questInfo.frequency = 0;
        end

        if not questInfo.isMeta then
            questInfo.isMeta = class == Enum_QuestClassification.Meta;
        end

        if questInfo.frequency == 2 then
            questInfo.isWeekly = true;
        end

        if questInfo.frequency == 1 then
            questInfo.isDaily = true;
        end

        if questInfo.isAccountQuest == nil then
            questInfo.isAccountQuest = IsAccountQuest(questInfo.questID);
        end

        return questInfo
    end
    API.BuildQuestInfo = BuildQuestInfo;

    local function GetQuestIcon(questInfo)
        --QuestMapLogTitleButton_OnEnter

        if not questInfo then
            return ICON_PATH.."IncompleteQuest.png";
        end

        local file;

        if questInfo.isOnQuest then
            if questInfo.isComplete then
                if questInfo.isCampaign then
                    file = "CompleteCampaignQuest.png";
                elseif questInfo.isLegendary then
                    file = "CompleteLegendaryQuest.png";
                elseif questInfo.isImportant then
                    file = "CompleteImportantQuest.png";
                else
                    file = "CompleteQuest.png";
                end
            else
                if questInfo.isCampaign then
                    file = "IncompleteCampaignQuest.png";
                elseif questInfo.isLegendary then
                    file = "IncompleteLegendaryQuest.png";
                elseif questInfo.isImportant then
                    file = "IncompleteImportantQuest.png";
                elseif questInfo.isMeta then
                    file = "IncompleteMetaQuest.png";
                else
                    file = "IncompleteQuest.png";
                end
            end

        else
            if questInfo.frequency == 1 then    --Enum.QuestFrequency.Daily
                file = "DailyQuest.png";
            elseif questInfo.frequency == 2 then    --Enum.QuestFrequency.Weekly
                file = "WeeklyQuest.png";
            elseif questInfo.frequency == 3 and not questInfo.isMeta then   ----Enum.QuestFrequency.ResetByScheduler
                file = "RepeatableScheduler.png";    --TWW
            elseif  questInfo.repeatable then
                file = "RepeatableQuest.png";
            else
                if questInfo.isCampaign then
                    file = "AvailableCampaignQuest.png";
                elseif questInfo.isLegendary then
                    file = "AvailableLegendaryQuest.png";
                elseif questInfo.isImportant then
                    file = "AvailableImportantQuest.png";
                elseif questInfo.isMeta then
                    file = "AvailableMetaQuest.png";
                else
                    file = "AvailableQuest.png";
                end
            end
        end

        return ICON_PATH..file
    end
    API.GetQuestIcon = GetQuestIcon;

    local function IsQuestAutoAccepted()
        return QuestGetAutoAccept()
    end
    API.IsQuestAutoAccepted = IsQuestAutoAccepted;

    local function ShouldShowQuestAcceptedAlert(questID)
        return not (IsWorldQuest(questID) and IsQuestTask(questID));
    end
    API.ShouldShowQuestAcceptedAlert = ShouldShowQuestAcceptedAlert;

    local GetDetailText = GetQuestText;
    local GetProgressText = GetProgressText;
    local GetRewardText = GetRewardText;
    local GetGreetingText = GetGreetingText;
    local GetGossipText = C_GossipInfo.GetText;

    local QuestTextMethod = {
        Detail = GetDetailText,
        Progress = GetProgressText,
        Complete = GetRewardText,
        Greeting = GetGreetingText,
    };

    API.GetGossipText = GetGossipText;

    local function GetQuestText(method)
        local text = QuestTextMethod[method]();
        if text and text ~= "" then
            return text
        end
    end
    API.GetQuestText = GetQuestText;

    if C_QuestInfoSystem.GetQuestRewardCurrencies then
        local GetQuestRewardCurrencies = C_QuestInfoSystem.GetQuestRewardCurrencies;

        local function GetNumRewardCurrencies_TWW(questID)
            local currencyRewards = GetQuestRewardCurrencies(questID) or {};
            return #currencyRewards
        end
        API.GetNumRewardCurrencies = GetNumRewardCurrencies_TWW;
    else
        local GetNumRewardCurrencies_Deprecated = GetNumRewardCurrencies;
        API.GetNumRewardCurrencies = GetNumRewardCurrencies_Deprecated;
    end


    --QuestTheme
    local GetQuestDetailsTheme = C_QuestLog.GetQuestDetailsTheme or AlwaysFalse;
    local DECOR_PATH = "Interface/AddOns/DialogueUI/Art/ParchmentDecor/";
    local BackgroundDecors = {
        ["QuestBG-Dragonflight"] = "Dragonflight.png",
        ["QuestBG-Azurespan"] = "Dragonflight.png",
        ["QuestBG-EmeraldDream"] = "Dragonflight-Green.png",
        ["QuestBG-Ohnplains"] = "Dragonflight-Green.png",
        ["QuestBG-Thaldraszus"] = "Dragonflight-Bronze.png",
        ["QuestBG-Walkingshore"] = "Dragonflight-Red.png",
        ["QuestBG-ZaralekCavern"] = "Dragonflight.png",
        ["QuestBG-ExilesReach"] = "Dragonflight.png",

        ["QuestBG-Alliance"] = "Alliance.png",
        ["QuestBG-Horde"] = "Horde.png",

        ["QuestBG-Flame"] = "TWW-Flame.png",
        ["QuestBG-Candle"] = "TWW-Candle.png",
        ["QuestBG-Storm"] = "TWW-Storm.png",
        ["QuestBG-Web"] = "TWW-Web.png",
        ["QuestBG-1027"] = "TWW-Azeroth.png",
        ["QuestBG-Rocket"] = "TWW-Rocket.png",
        ["QuestBG-Fist"] = "TWW-Fist.png",
    };

    local function GetQuestBackgroundDecor(questID)
        local theme = GetQuestDetailsTheme(questID);
        --print(theme.background)
        --theme = {background = "QuestBG-Web"};    --debug
        if theme and theme.background and BackgroundDecors[theme.background] then
            return DECOR_PATH..BackgroundDecors[theme.background]
        end
    end
    API.GetQuestBackgroundDecor = GetQuestBackgroundDecor;


    local MAX_QUESTS;
    local GetNumQuestLogEntries = C_QuestLog.GetNumQuestLogEntries;
    local GetQuestIDForLogIndex = C_QuestLog.GetQuestIDForLogIndex;
    local GetQuestInfo = C_QuestLog.GetInfo;

    local function GetNumQuestCanAccept()
        --*Unreliable
        --numQuests include all types of quests.
        --(Account/Daily) quests don't count towards MaxQuest(35)
        if not MAX_QUESTS then
            MAX_QUESTS = C_QuestLog.GetMaxNumQuestsCanAccept();
        end

        local numShownEntries, numAllQuests = GetNumQuestLogEntries();
        local numQuests = 0;
        local questID;
        local n = 0;
        print("numShownEntries", numShownEntries);

        for i = 1, numShownEntries do
            questID = GetQuestIDForLogIndex(i);
            if questID ~= 0 then
                local info = GetQuestInfo(i);

                if info and not (info.isHidden or info.isHeader) then
                    if not (IsAccountQuest(questID)) then
                        numQuests = numQuests + 1;

                        print(numQuests, questID, info.title)
                    end
                end
            end
        end

        print("Num Quests:", numQuests);
        return MAX_QUESTS - numQuests, MAX_QUESTS
    end

    local GetItemInfoInstant = C_Item.GetItemInfoInstant;
    local select = select;

    local function IsQuestLoreItem(item)
        --Display lore text (QuestItemDisplay)
        if not item then return end;
        local classID, subclassID = select(6, GetItemInfoInstant(item));
        --print(item, classID, subclassID)
        return (classID == 12) or (classID == 0 and subclassID == 8) or (classID == 15 and subclassID == 4)     ---Re-ignore Junk?
    end
    API.IsQuestLoreItem = IsQuestLoreItem;

    local function IsQuestRequiredItem(item)
        --Don't show item count in bag if ItemType == 12
        if not item then return end;
        local classID, subclassID = select(6, GetItemInfoInstant(item));
        return classID == 12
    end
    API.IsQuestRequiredItem = IsQuestRequiredItem;

    local function GetQuestName(questID)
        local questName = C_TaskQuest.GetQuestInfoByQuestID(questID);
        if not questName then
            --Retail
            if C_QuestLog.GetTitleForQuestID then
                questName = C_QuestLog.GetTitleForQuestID(questID);
                if questName and questName ~= "" then
                    return questName
                else
                    C_QuestLog.RequestLoadQuestByID(questID);
                end
            end

            --Classic
            if GetQuestLogIndexByID then
                local questIndex = GetQuestLogIndexByID(questID);
                if questIndex and questIndex > 0 then
                    questName = GetQuestLogTitle(questIndex);
                else
                    questName = C_QuestLog.GetQuestInfo(questID);
                end
            end
        end
        return questName
    end
    API.GetQuestName = GetQuestName;

    local HoldableItems = {
        INVTYPE_WEAPON = true,
        INVTYPE_2HWEAPON = true,
        INVTYPE_SHIELD = true,
        INVTYPE_HOLDABLE = true,
        INVTYPE_RANGED = true,
        INVTYPE_RANGEDRIGHT = true,
        INVTYPE_WEAPONMAINHAND = true,
        INVTYPE_WEAPONOFFHAND = true,
    };

    local NoUseTransmogSkin = {
        INVTYPE_HEAD = true,
        INVTYPE_HAND = true,
        --INVTYPE_FEET = true,
    };

    local TransmogSetupGear = {
        INVTYPE_HEAD = {78420},
        INVTYPE_HAND = {78420, 78425},
    };

    local function IsHoldableItem(item)
        if item then
            local _, _, _, itemEquipLoc = GetItemInfoInstant(item);
            return HoldableItems[itemEquipLoc];
        end
    end
    API.IsHoldableItem = IsHoldableItem;

    local function GetTransmogSetup(item)
        local _, _, _, itemEquipLoc = GetItemInfoInstant(item);
        local useTransmogSkin = not (NoUseTransmogSkin[itemEquipLoc] or false);
        local setupGear = TransmogSetupGear[itemEquipLoc];
        return useTransmogSkin, setupGear
    end
    API.GetTransmogSetup = GetTransmogSetup;


    --QuestTag
    local GetQuestTagInfo = C_QuestLog.GetQuestTagInfo or AlwaysFalse;
    local QUEST_TAG_NAME = {
        --Also: Enum.QuestTagType
        [Enum.QuestTag.Dungeon] = {L["Quest Type Dungeon"], "QuestTag-Dungeon.png"},
        [Enum.QuestTag.Raid] = {L["Quest Type Raid"], "QuestTag-Raid.png"},
        [Enum.QuestTag.Raid10] = {L["Quest Type Raid"], "QuestTag-Raid.png"},
        [Enum.QuestTag.Raid25] = {L["Quest Type Raid"], "QuestTag-Raid.png"},

        [271] = {L["Quest Type Covenant Calling"]},
    };

    local function GetQuestTag(questID)
        local info = GetQuestTagInfo(questID);
        if info and info.tagID then
            return info.tagID
        end
    end
    API.GetQuestTag = GetQuestTag;

    local function GetQuestTagNameIcon(tagID)
        if QUEST_TAG_NAME[tagID] then
            local icon = QUEST_TAG_NAME[tagID][2];
            if icon then
                icon = ICON_PATH..icon;
            end
            return QUEST_TAG_NAME[tagID][1], icon
        end
    end
    API.GetQuestTagNameIcon = GetQuestTagNameIcon;


    local PLAYER_HONOR_ICON;

    local function GetHonorIcon()
        if PLAYER_HONOR_ICON == nil then
            if UnitFactionGroup and UnitFactionGroup("player") == "Horde" then
                PLAYER_HONOR_ICON = "Interface/Icons/PVPCurrency-Honor-Horde";
            else
                PLAYER_HONOR_ICON = "Interface/Icons/PVPCurrency-Honor-Alliance";
            end
        end

        return PLAYER_HONOR_ICON
    end
    API.GetHonorIcon = GetHonorIcon;

    local function GetQuestTimeLeft(questID, formatedToText)
        local seconds = GetQuestTimeLeftSeconds(questID);
        if seconds then
            if formatedToText then
                return API.SecondsToTime(seconds, true, true)
            else
                return seconds
            end
        end
    end
    API.GetQuestTimeLeft = GetQuestTimeLeft;


    local function GetRecurringQuestTimeLeft(questID, formatedToText)
        if GetQuestClassification(questID) then
            local seconds = GetQuestTimeLeft(questID, formatedToText);
            return true, seconds
        else
            return false
        end
    end
    API.GetRecurringQuestTimeLeft = GetRecurringQuestTimeLeft;

    local function ShouldMuteQuestDetail(questID)
        --Temp Blizzard bug fix for weekly quest appearing repeatedly issue
        local class = GetQuestClassification and GetQuestClassification(questID);
        if (class == 4 or class == 5 or class == nil) and IsOnQuest(questID) then
            --Nil case is for Classic where QuestClassification doesn't exist
            return true
        else
            return false
        end
    end
    API.ShouldMuteQuestDetail = ShouldMuteQuestDetail;

    do
        --Replace player name with RP name:
        --Handled by addon when installed: Total RP 3: RP Name in Quest Text
        --Otherwise use our own modifier
        --See Code\SupportedAddOns\Roleplay
        local function TextModifier_None(text)
            return text
        end

        local TextModifier = TextModifier_None;

        local function GetModifiedQuestText(method)
            return TextModifier(GetQuestText(method))
        end
        API.GetModifiedQuestText = GetModifiedQuestText;

        local function GetModifiedGossipText()
            return TextModifier(GetGossipText());
        end
        API.GetModifiedGossipText = GetModifiedGossipText;

        local function SetDialogueTextModifier(modifierFunc)
            TextModifier = modifierFunc or TextModifier_None;
        end
        addon.SetDialogueTextModifier = SetDialogueTextModifier;
    end


    --QuestLine
    if C_QuestLog.GetZoneStoryInfo and C_QuestLine and C_QuestLine.GetQuestLineInfo then
        local GetBestMapForUnit = C_Map.GetBestMapForUnit;
        function API.GetQuestLineInfo(questID)
            local uiMapID = GetBestMapForUnit("player");
            local hasQuestLineOnMap, questLineName, questLineID, achievementID;
            if uiMapID then
                achievementID = C_QuestLog.GetZoneStoryInfo(uiMapID);
                if achievementID then
                    hasQuestLineOnMap = true;
                    local questLineInfo = C_QuestLine.GetQuestLineInfo(questID, uiMapID);
                    if questLineInfo then
                        questLineName = questLineInfo.questLineName;
                        questLineID = questLineInfo.questLineID;
                    end
                end
            end
            return hasQuestLineOnMap, questLineName, questLineID, uiMapID, achievementID
        end
    else
        function API.GetQuestLineInfo(questID)

        end
    end
end

do  -- Color
    -- Make Rare and Epic brighter (use the color in Narcissus)
    local CreateColor = CreateColor;
    local ITEM_QUALITY_COLORS = ITEM_QUALITY_COLORS;
    local QualityColors = {};

    QualityColors[0] = CreateColor(0.9, 0.9, 0.9, 1);
    QualityColors[1] = QualityColors[0];
    QualityColors[3] = CreateColor(105/255, 158/255, 255/255, 1);
    QualityColors[4] = CreateColor(185/255, 83/255, 255/255, 1);

    local function GetItemQualityColor(quality)
        if QualityColors[quality] then
            return QualityColors[quality]
        else
            return ITEM_QUALITY_COLORS[quality].color
        end
    end
    API.GetItemQualityColor = GetItemQualityColor;


    local TextPalette = {
        [0] = CreateColor(1, 1, 1, 1),              --Fallback
        [1] = CreateColor(0.87, 0.86, 0.75, 1),     --Ivory: Used by big buttons and low-priority alerts like criteria complete
        [2] = CreateColor(0.19, 0.17, 0.13, 1),     --Dark Brown: Used as paragraph text color
        [3] = CreateColor(0.42, 0.75, 0.48, 1),     --Green: Quest Complete
        [4] = CreateColor(1.000, 0.125, 0.125, 1),  --ERROR_COLOR
    };

    local function GetTextColorByIndex(colorIndex)
        if not (colorIndex and TextPalette[colorIndex]) then
            colorIndex = 0;
        end
        return TextPalette[colorIndex]
    end
    API.GetTextColorByIndex = GetTextColorByIndex;

    local function SetTextColorByIndex(fontString, colorIndex)
        local color = GetTextColorByIndex(colorIndex);
        if color then
            local r, g, b = color:GetRGB();
            fontString:SetTextColor(r, g, b);
        end
    end
    API.SetTextColorByIndex = SetTextColorByIndex;

    local function SetTextColorByGlobal(fontString, colorMixin)
        local r, g, b;
        if colorMixin then
            r, g, b = colorMixin:GetRGB();
        else
            r, g, b = 1, 1, 1;
        end
        fontString:SetTextColor(r, g, b);
    end
    API.SetTextColorByGlobal = SetTextColorByGlobal;
end

do  -- Currency
    local GetCurrencyContainerInfoDefault = C_CurrencyInfo.GetCurrencyContainerInfo;
    local GetCurrencyInfo = C_CurrencyInfo.GetCurrencyInfo;
    local format = string.format;
    local FormatLargeNumber = FormatLargeNumber;

    local function GetCurrencyContainerInfo(currencyID, numItems, name, texture, quality)
        local entry = GetCurrencyContainerInfoDefault(currencyID, numItems);
        if entry then
            return entry.name, entry.icon, entry.displayAmount, entry.quality
        end
        return name, texture, numItems, quality
    end
    API.GetCurrencyContainerInfo = GetCurrencyContainerInfo;


    local function GenerateMoneyText(rawCopper, colorized, noAbbreviation) --coins
        local text;
        local gold = floor(rawCopper / 10000);
        local silver = floor((rawCopper - gold * 10000) / 100);
        local copper = floor(rawCopper - gold * 10000 - silver * 100);

        local goldText, silverText, copperText;

        if copper > 0 then
            if noAbbreviation then
                copperText = format(L["Format Copper Amount"], copper);
            else
                copperText = copper..L["Symbol Copper"];
            end

            if colorized then
                copperText = "|cffe3b277"..copperText.."|r";
            end
        end

        if gold ~= 0 or silver ~= 0 then
            if noAbbreviation then
                silverText = format(L["Format Silver Amount"], silver);
            else
                silverText = silver..L["Symbol Silver"];
            end

            if colorized then
                silverText = "|cffc5d2e8"..silverText.."|r";
            end

            if gold > 0 then
                if noAbbreviation then
                    goldText = format(L["Format Gold Amount"], FormatLargeNumber(gold));
                else
                    goldText = gold..L["Symbol Gold"];
                end

                if colorized then
                    goldText = "|cffffbb18"..goldText.."|r";
                end

                if copperText then
                    text = goldText.." "..silverText.." "..copperText;
                elseif silver == 0 then
                    text = goldText;
                else
                    text = goldText.." "..silverText;
                end
            else
                if copperText then
                    text = silverText.." "..copperText;
                else
                    text = silverText;
                end
            end
        else
            text = copperText;
        end

        return text
    end
    API.GenerateMoneyText = GenerateMoneyText;


    local IGNORED_OVERFLOW_ID = {
        [3068] = true,      --Delver's Journey
        [3143] = true,      --Delver's Journey
    };

    local function WillCurrencyRewardOverflow(currencyID, rewardQuantity)
        if IGNORED_OVERFLOW_ID[currencyID] then return false end;

        local currencyInfo = GetCurrencyInfo(currencyID);
        local quantity = currencyInfo and (currencyInfo.useTotalEarnedForMaxQty and currencyInfo.totalEarned or currencyInfo.quantity);
        return quantity and currencyInfo.maxQuantity > 0 and rewardQuantity + quantity > currencyInfo.maxQuantity;
    end
    API.WillCurrencyRewardOverflow = WillCurrencyRewardOverflow;

    local function GetColorizedAmountForCurrency(currencyID, rewardQuantity, useIcon)
        if WillCurrencyRewardOverflow(currencyID, rewardQuantity) then
            if useIcon then --For Small Button
                return "|TInterface/AddOns/DialogueUI/Art/Icons/CurrencyOverflow.png:0:0|t"..rewardQuantity.."|r"
            else
                return "|cffff2020"..rewardQuantity.."|r"
            end
        else
            return rewardQuantity
        end
    end
    API.GetColorizedAmountForCurrency = GetColorizedAmountForCurrency;

    local function GetOwnedCurrencyQuantity(currencyID)
        local currencyInfo = GetCurrencyInfo(currencyID);
        return currencyInfo and currencyInfo.quantity or 0
    end
    API.GetOwnedCurrencyQuantity = GetOwnedCurrencyQuantity;

    local UnitXP = UnitXP;
    local UnitXPMax = UnitXPMax;
    local UnitLevel = UnitLevel;

    local function GetXPPercentage(xp)
        local current = UnitXP("player");
        local max = UnitXPMax("player");
        if current and max and max ~= 0 and xp > 0 then
            local ratio = xp/max;
            if ratio > 1 then
                
            end
            return API.Round(ratio*100);
        else
            return 0
        end
    end
    API.GetXPPercentage = GetXPPercentage;

    local function GetPlayerLevelXP()
        local level = UnitLevel("player");
        local currentXP = UnitXP("player");
        local maxXP = UnitXPMax("player");
        return level, currentXP, maxXP
    end
    API.GetPlayerLevelXP = GetPlayerLevelXP;

    local function IsPlayerAtMaxLevel()
        local maxLevel;

        if GetMaxLevelForPlayerExpansion then
            maxLevel = GetMaxLevelForPlayerExpansion();
        elseif GetMaxPlayerLevel then
            maxLevel = GetMaxPlayerLevel();
        else
            maxLevel = 999
        end

        return UnitLevel("player") >= maxLevel
    end
    API.IsPlayerAtMaxLevel = IsPlayerAtMaxLevel;
end

do  -- Grid Layout
    local ipairs = ipairs;
    local tinsert = table.insert;

    local GridMixin = {};

    function GridMixin:OnLoad()
        self:SetGrid(1, 1);
        self:SetSpacing(0);
    end

    function GridMixin:SetGrid(x, y)
        self.x = x;
        self.y = y;
        self:ResetGrid();
    end

    function GridMixin:SetSpacing(spacing)
        self.spacing = spacing;
    end

    function GridMixin:ResetGrid()
        self.grid = {};
        self.fromRow = 1;
        self.maxOccupiedX = 0;
        self.maxOccupiedY = 0;
    end

    function GridMixin:SetGridSize(gridWidth, gridHeight)
        self.gridWidth = gridWidth;
        self.gridHeight = gridHeight;
    end

    function GridMixin:CreateNewRows(n)
        n = n or 1;

        for i = 1, n do
            local tbl = {};
            for col = 1, self.x do
                tinsert(tbl, false);
            end
            tinsert(self.grid, tbl);
        end
    end

    function GridMixin:FindGridForSize(objectSizeX, objectSizeY)
        local found = false;
        local maxRow = #self.grid;
        local topleftGridX, topleftGridY;

        for row = self.fromRow, maxRow do
            local rowStatus = self.grid[row];
            local maxCol = #rowStatus;
            local rowFull = true;

            for col, occupied in ipairs(rowStatus) do
                if not occupied then
                    rowFull = false;
                    if (col + objectSizeX - 1 <= maxCol) and (row + objectSizeY - 1 <= maxRow) then
                        found = true;
                        topleftGridX = col;
                        topleftGridY = row;

                        for _row = row, row + objectSizeY - 1 do
                            for _col = col, col + objectSizeX - 1 do
                                self.grid[_row][_col] = true;
                            end
                        end

                        if topleftGridX > self.maxOccupiedX then
                            self.maxOccupiedX = topleftGridX;
                        end

                        if topleftGridY > self.maxOccupiedY then
                            self.maxOccupiedY = topleftGridY;
                            if objectSizeY > 1 then
                                self.maxOccupiedY = self.maxOccupiedY + objectSizeY - 1;
                            end
                        end

                        break
                    end
                end
            end

            if found then
                break
            end

            if rowFull then
                self.fromRow = self.fromRow + 1;
            end
        end

        if found then
            return topleftGridX, topleftGridY
        else
            self:CreateNewRows(self.y);
            return self:FindGridForSize(objectSizeX, objectSizeY)
        end
    end

    function GridMixin:FlagPreviousRowFull()
        local maxRow = #self.grid;

        for row = self.fromRow, maxRow do
            local rowStatus = self.grid[row];
            local anyContentThisRow;
            for col, occupied in ipairs(rowStatus) do
                if col == 1 then
                    if occupied then
                        anyContentThisRow = true;
                    end
                else
                    if anyContentThisRow then
                        self.grid[row][col] = true;
                    end
                end
            end
        end
    end

    function GridMixin:GetOffsetForGridPosition(topleftGridX, topleftGridY)
        local offsetX = (topleftGridX - 1) * (self.gridWidth + self.spacing);
        local offsetY = (topleftGridY - 1) * (self.gridHeight + self.spacing);
        return offsetX, -offsetY
    end

    function GridMixin:PlaceObject(object, objectSizeX, objectSizeY, anchorTo, fromOffsetX, fromOffsetY)
        local topleftGridX, topleftGridY = self:FindGridForSize(objectSizeX, objectSizeY);
        local offsetX, offsetY = self:GetOffsetForGridPosition(topleftGridX, topleftGridY);
        object:SetPoint("TOPLEFT", anchorTo, "TOPLEFT", fromOffsetX + offsetX, fromOffsetY + offsetY);
    end

    function GridMixin:GetWrappedSize()
        local width = (self.maxOccupiedX > 0 and self.maxOccupiedX * (self.gridWidth + self.spacing) - self.spacing) or 0;
        local height = (self.maxOccupiedY > 0 and self.maxOccupiedY * (self.gridHeight + self.spacing) - self.spacing) or 0;

        return width, height
    end

    local function CreateGridLayout()
        local grid = API.CreateFromMixins(GridMixin);
        grid:OnLoad();
        return grid
    end
    API.CreateGridLayout = CreateGridLayout;
end

do  -- Fade Frame
    local abs = math.abs;
    local tinsert = table.insert;
    local wipe = wipe;

    local fadeInfo = {};
    local fadingFrames = {};

    local f = CreateFrame("Frame");

    local function OnUpdate(self, elpased)
        local i = 1;
        local frame, info, timer, alpha;
        local isComplete = true;
        while fadingFrames[i] do
            frame = fadingFrames[i];
            info = fadeInfo[frame];
            if info then
                timer = info.timer + elpased;
                if timer >= info.duration then
                    alpha = info.toAlpha;
                    fadeInfo[frame] = nil;
                    if info.alterShownState and alpha <= 0 then
                        frame:Hide();
                    end
                else
                    alpha = info.fromAlpha + (info.toAlpha - info.fromAlpha) * timer/info.duration;
                    info.timer = timer;
                end
                frame:SetAlpha(alpha);
                isComplete = false;
            end
            i = i + 1;
        end

        if isComplete then
            f:Clear();
        end
    end

    function f:Clear()
        self:SetScript("OnUpdate", nil);
        wipe(fadingFrames);
        wipe(fadeInfo);
    end

    function f:Add(frame, fullDuration, fromAlpha, toAlpha, alterShownState, useConstantDuration)
        local alpha = frame:GetAlpha();
        if alterShownState then
            if toAlpha > 0 then
                frame:Show();
            end
            if toAlpha == 0 then
                if not frame:IsShown() then
                    frame:SetAlpha(0);
                    alpha = 0;
                end
                if alpha == 0 then
                    frame:Hide();
                end
            end
        end
        if fromAlpha == toAlpha or alpha == toAlpha then
            if fadeInfo[frame] then
                fadeInfo[frame] = nil;
            end
            return;
        end
        local duration;
        if useConstantDuration then
            duration = fullDuration;
        else
            if fromAlpha then
                duration = fullDuration * (alpha - toAlpha)/(fromAlpha - toAlpha);
            else
                duration = fullDuration * abs(alpha - toAlpha);
            end
        end
        if duration <= 0 then
            frame:SetAlpha(toAlpha);
            if toAlpha == 0 then
                frame:Hide();
            end
            return;
        end
        fadeInfo[frame] = {
            fromAlpha = alpha,
            toAlpha = toAlpha,
            duration = duration,
            timer = 0,
            alterShownState = alterShownState,
        };
        for i = 1, #fadingFrames do
            if fadingFrames[i] == frame then
                return;
            end
        end
        tinsert(fadingFrames, frame);
        self:SetScript("OnUpdate", OnUpdate);
    end

    function f:SimpleFade(frame, toAlpha, alterShownState, speedMultiplier)
        --Use a constant fading speed: 1.0 in 0.25s
        --alterShownState: if true, run Frame:Hide() when alpha reaches zero / run Frame:Show() at the beginning
        speedMultiplier = speedMultiplier or 1;
        local alpha = frame:GetAlpha();
        local duration = abs(alpha - toAlpha) * 0.25 * speedMultiplier;
        if duration <= 0 then
            return;
        end

        self:Add(frame, duration, alpha, toAlpha, alterShownState, true);
    end

    function f:Snap()
        local i = 1;
        local frame, info;
        while fadingFrames[i] do
            frame = fadingFrames[i];
            info = fadeInfo[frame];
            if info then
                frame:SetAlpha(info.toAlpha);
            end
            i = i + 1;
        end
        self:Clear();
    end

    local function UIFrameFade(frame, duration, toAlpha, initialAlpha)
        if initialAlpha then
            frame:SetAlpha(initialAlpha);
            f:Add(frame, duration, initialAlpha, toAlpha, true, false);
        else
            f:Add(frame, duration, nil, toAlpha, true, false);
        end
    end

    local function UIFrameFadeIn(frame, duration)
        frame:SetAlpha(0);
        f:Add(frame, duration, 0, 1, true, false);
    end


    API.UIFrameFade = UIFrameFade;       --from current alpha
    API.UIFrameFadeIn = UIFrameFadeIn;   --from 0 to 1
end

do  -- Model
    local UnitRace = UnitRace;
    local WantsAlteredForm = C_UnitAuras and C_UnitAuras.WantsAlteredForm or AlwaysFalse;

    local function SetModelByUnit(model, unit)
        local _, raceFileName = UnitRace(unit);
        if raceFileName == "Dracthyr" or raceFileName == "Worgen" then
            local arg = WantsAlteredForm(unit);
            model:SetUnit(unit, false, arg);    --blend = false
        else
            model:SetUnit(unit, false);
        end
        model.unit = unit;
    end
    API.SetModelByUnit = SetModelByUnit;


    local function SetModelLight(model, enabled, omni, dirX, dirY, dirZ, ambIntensity, ambR, ambG, ambB, dirIntensity, dirR, dirG, dirB)
        local lightValues = {
            omnidirectional = omni or false;
            point = CreateVector3D(dirX or 0, dirY or 0, dirZ or 0),
            ambientIntensity = ambIntensity or 1,
            ambientColor = CreateColor(ambR or 1, ambG or 1, ambB or 1),
            diffuseIntensity = dirIntensity or 1,
            diffuseColor = CreateColor(dirR or 1, dirG or 1, dirB or 1),
        };

        model:SetLight(enabled, lightValues);
    end
    API.SetModelLight = SetModelLight;


    do  --UnitPortraitSetter
        local UnitPortraitSetter = CreateFrame("Frame");
        UnitPortraitSetter.queue = {};

        function UnitPortraitSetter:SetPortraitTexture(textureObject, unit)
            if IsUnitModelReadyForUI(unit) then
                self.queue[textureObject] = nil;
                SetPortraitTexture(textureObject, unit);
            else
                self.queue[textureObject] = unit;
                self.t = 0;
                self:SetScript("OnUpdate", self.OnUpdate);
            end
        end

        function UnitPortraitSetter:OnUpdate(elapsed)
            self.t = self.t + elapsed;
            if self.t >= 1.0 then
                self.t = 0;
                self:SetScript("OnUpdate", nil);
                for textureObject, unit in pairs(self.queue) do
                    if textureObject:IsVisible() then
                        SetPortraitTexture(textureObject, unit);
                    end
                end
                self.queue = {};
            end
        end

        API.SetPortraitTexture = function(textureObject, unit)
            UnitPortraitSetter:SetPortraitTexture(textureObject, unit);
        end
    end
end

do  -- Faction -- Reputation
    local C_GossipInfo = C_GossipInfo;
    local C_MajorFactions = C_MajorFactions;
    local C_Reputation = C_Reputation;
    local GetFactionInfoByID = GetFactionInfoByID or C_Reputation.GetFactionDataByID;   --TWW
    local GetFactionGrantedByCurrency = C_CurrencyInfo.GetFactionGrantedByCurrency or AlwaysFalse;
    local IsAccountWideReputation = C_Reputation and C_Reputation.IsAccountWideReputation or AlwaysFalse;

    local function GetFactionStatusText(factionID)
        --Derived from Blizzard ReputationFrame_InitReputationRow in ReputationFrame.lua
        if not factionID then return end;
        local factionName;
        local p1, description, standingID, barMin, barMax, barValue = GetFactionInfoByID(factionID);

        if type(p1) == "table" then     --Return table after TWW
            standingID = p1.reaction;
            barMin = p1.currentReactionThreshold;
            barMax = p1.nextReactionThreshold;
            barValue = p1.currentStanding;
            factionName = p1.name;
        else
            factionName = p1;
        end

        local isParagon = C_Reputation.IsFactionParagon and C_Reputation.IsFactionParagon(factionID);
        local isMajorFaction = C_Reputation.IsMajorFaction and C_Reputation.IsMajorFaction(factionID);
        local repInfo = C_GossipInfo.GetFriendshipReputation(factionID);

        local isCapped;
        local factionStandingtext;  --Revered/Junior/Renown 1

        if repInfo and repInfo.friendshipFactionID > 0 then --Friendship
            factionStandingtext = repInfo.reaction;

            if repInfo.nextThreshold then
                barMin, barMax, barValue = repInfo.reactionThreshold, repInfo.nextThreshold, repInfo.standing;
                barValue = barValue - barMin;
                barMax = barMax - barMin;
            else
                barMin, barMax, barValue = 0, 1, 1;
                isCapped = true;
            end

            local rankInfo = C_GossipInfo.GetFriendshipReputationRanks(repInfo.friendshipFactionID);
            if rankInfo then
                factionStandingtext = factionStandingtext .. string.format(" (Lv. %s/%s)", rankInfo.currentLevel, rankInfo.maxLevel);
            end

        elseif isMajorFaction then
            local majorFactionData = C_MajorFactions.GetMajorFactionData(factionID);
            if majorFactionData then
                barMin, barMax = 0, majorFactionData.renownLevelThreshold;
                isCapped = C_MajorFactions.HasMaximumRenown(factionID);
                barValue = isCapped and majorFactionData.renownLevelThreshold or majorFactionData.renownReputationEarned or 0;
                factionStandingtext = L["Renown Level Label"] .. majorFactionData.renownLevel;
                if isParagon then
                    local totalEarned, threshold = C_Reputation.GetFactionParagonInfo(factionID);
                    if totalEarned and threshold and threshold ~= 0 then
                        local paragonLevel = floor(totalEarned / threshold);
                        local currentValue = totalEarned - paragonLevel * threshold;
                        factionStandingtext = ("|cff00ccff"..L["Paragon Reputation"].."|r %d/%d"):format(currentValue, threshold);
                    end
                else
                    if isCapped then
                        factionStandingtext = factionStandingtext.." "..L["Level Maxed"];
                    end
                end
            end
        elseif (standingID and standingID > 0) then
            isCapped = standingID == 8;  --MAX_REPUTATION_REACTION
            local gender = UnitSex("player");
		    factionStandingtext = GetText("FACTION_STANDING_LABEL"..standingID, gender);    --GetText: Game API that returns localized texts
        end

        local rolloverText; --(0/24000)
        if barValue and barMax and (not isCapped) then
            rolloverText = string.format("(%s/%s)", barValue, barMax);
        end

        local text;

        if factionStandingtext then
            if not text then text = L["Current Colon"] end;
            factionStandingtext = " |cffffffff"..factionStandingtext.."|r";
            text = text .. factionStandingtext;
        end

        if rolloverText then
            if not text then text = L["Current Colon"] end;
            rolloverText = "  |cffffffff"..rolloverText.."|r";
            text = text .. rolloverText;
        end

        return text, factionName
    end
    API.GetFactionStatusText = GetFactionStatusText;

    local function GetFactionStatusTextByCurrencyID(currencyID)
        local factionID =  GetFactionGrantedByCurrency(currencyID);
        if factionID then
            return GetFactionStatusText(factionID);
        end
    end
    API.GetFactionStatusTextByCurrencyID = GetFactionStatusTextByCurrencyID;

    local function IsReputationAccountWide(factionID)
        return factionID and IsAccountWideReputation(factionID);
    end
    API.IsAccountWideReputation = IsAccountWideReputation;
end

do  -- Chat Message
    local ADDON_ICON = "|TInterface\\AddOns\\DialogueUI\\Art\\Icons\\Logo:0:0|t";
    local function PrintMessage(header, msg)
        if not msg then
            msg = "";
        end
        if StripHyperlinks then
            msg = StripHyperlinks(msg);
        end
        print(ADDON_ICON.."|cffffd100"..header.."  |cffffffff"..msg.."|r");
    end
    API.PrintMessage = PrintMessage;

    function API.PrintQuestCompletionText(msg)
        if msg == "" then return end;
        if StripHyperlinks then
            msg = StripHyperlinks(msg);
        end
        print(ADDON_ICON.." |cffffd100"..msg.."|r");
    end
end

do  -- Tooltip
    local GetInventoryItemLink = GetInventoryItemLink;
    local GetInventoryItemID = GetInventoryItemID;
    local GetItemInfoInstant = C_Item.GetItemInfoInstant;
    local GetQuestItemLink = GetQuestItemLink;

    local EQUIPLOC_SLOTID = {
        INVTYPE_HEAD = 1,
        INVTYPE_NECK = 2,
        INVTYPE_SHOULDER = 3,
        INVTYPE_BODY = 4,
        INVTYPE_CHEST = 5,
        INVTYPE_ROBE = 5,
        INVTYPE_WAIST = 6,
        INVTYPE_LEGS = 7,
        INVTYPE_FEET = 8,
        INVTYPE_WRIST = 9,
        INVTYPE_HAND = 10,
        INVTYPE_FINGER = 11,    --12
        INVTYPE_TRINKET = 13,
        INVTYPE_WEAPON = 16,
        INVTYPE_SHIELD = 17,
        INVTYPE_CLOAK = 15,
        INVTYPE_2HWEAPON = 16,
        INVTYPE_WEAPONMAINHAND = 16,
        INVTYPE_WEAPONOFFHAND = 17,
        INVTYPE_HOLDABLE = 17,
        INVTYPE_RANGED = 18,    --Classic
        INVTYPE_RANGEDRIGHT = 18,
    };

    local FORMAT_POSITIVE_VALUE = "|cff19ff19+%s|r %s"; --Green
    local FORMAT_NEGATIVE_VALUE = "|cffff2020%s|r %s";  --Red

    local function FormatValueDiff(value, name)
        if value > 0 then
            return FORMAT_POSITIVE_VALUE:format(value, name);
        else
            return FORMAT_NEGATIVE_VALUE:format(value, name);
        end
    end

    local function GetEquippedSlotID(item)
        local _, _, _, itemEquipLoc = GetItemInfoInstant(item);
        local slotID = itemEquipLoc and EQUIPLOC_SLOTID[itemEquipLoc];
        return slotID
    end

    local function GetEquippedItemLink(comparisonItem)
        local slotID = GetEquippedSlotID(comparisonItem);

        if slotID then
            local link1 = GetInventoryItemLink("player", slotID);
            local link2;
            if slotID == 11 then
                link2 = GetInventoryItemLink("player", 12);
            elseif slotID == 13 then
                link2 = GetInventoryItemLink("player", 14);
            elseif slotID == 16 then
                link2 = GetInventoryItemLink("player", 17);
                if link2 then
                    local slotID2 = GetEquippedSlotID(link2);
                    if not (slotID2 and slotID2 == slotID) then
                        link2 = nil;
                    end
                end
            end

            if link2 and not link1 then
                link1 = link2;
                link2 = nil;
            end

            return link1, link2
        end
    end
    API.GetEquippedItemLink = GetEquippedItemLink;


    local function GetItemLevelDelta(newItem, oldItem, formatedToText)
        local newItemLevel = API.GetItemLevel(newItem) or 0;
        local oldItemLevel = API.GetItemLevel(oldItem) or 0;
        local diff = newItemLevel - oldItemLevel;

        if formatedToText then
            if diff ~= 0 then
                return FormatValueDiff(diff, L["Item Level"]);
            end
            return
        end

        return diff
    end
    API.GetItemLevelDelta = GetItemLevelDelta;


    local function GetEquippedItemLevelDelta(newLink)
        --Compare a reward item to the equipped one (check 2 slots for ring, trinket, weapon)
        --Return the maximum delta

        if not (newLink and API.IsEquippableItem(newLink)) then return end;

        local newItemLevel = API.GetItemLevel(newLink) or 0;
        local slotID = GetEquippedSlotID(newLink);
        local unit = "player";

        if slotID then
            local link1, link2, secondarySlotID;
            local itemID1 = GetInventoryItemID(unit, slotID);
            local itemID2;

            if itemID1 then
                link1 = GetInventoryItemLink(unit, slotID);
            end
            if slotID == 11 then
                secondarySlotID = 12;
            elseif slotID == 13 then
                secondarySlotID = 14;
            elseif slotID == 16 then
                --Case: Two-hand vs One-hand, Offhand vs Shield
                secondarySlotID = 17;
                itemID2 = GetInventoryItemID(unit, secondarySlotID);
                if itemID2 then
                    local equippedSlotID = GetEquippedSlotID(itemID2);
                    if not (equippedSlotID and equippedSlotID == slotID) then
                        itemID2 = nil;
                        secondarySlotID = nil;
                    end
                else
                    secondarySlotID = nil;
                end
            end

            local n = 0;
            local tbl = {};
            local itemLevel;

            if itemID1 then
                n = n + 1;
                itemLevel = link1 and API.GetItemLevel(link1) or 0;
                tbl[n] = {
                    isReady = itemLevel > 0 and newItemLevel > 0,
                    delta = newItemLevel - itemLevel,
                };
            else
                n = n + 1;
                tbl[n] = {
                    isReady = newItemLevel > 0,
                    delta = newItemLevel,
                };
            end

            if secondarySlotID then
                itemID2 = GetInventoryItemID(unit, secondarySlotID);
                link2 = GetInventoryItemLink(unit, secondarySlotID);
                n = n + 1;
                if itemID2 then
                    itemLevel = link1 and API.GetItemLevel(link2) or 0;
                    tbl[n] = {
                        isReady = itemLevel > 0 and newItemLevel > 0,
                        delta = newItemLevel - itemLevel,
                    };
                else
                    tbl[n] = {
                        isReady = newItemLevel > 0,
                        delta = newItemLevel,
                    };
                end
            end

            return tbl
        end
    end

    local function GetMaxEquippedItemLevelDelta(newLink)
        local itemLevelDeltaInfo = GetEquippedItemLevelDelta(newLink);
        if itemLevelDeltaInfo then
            local isReady = true;
            local maxDelta;
            for _, info in ipairs(itemLevelDeltaInfo) do
                if info.isReady then
                    if not maxDelta then
                        maxDelta = info.delta;
                    elseif info.delta > maxDelta then
                        maxDelta = info.delta;
                    end
                else
                    isReady = false;
                end
            end
            return maxDelta, isReady
        else
            return nil, true
        end
    end
    API.GetMaxEquippedItemLevelDelta = GetMaxEquippedItemLevelDelta;


    local function GetRewardItemLevelDelta(questInfoType, index)
        local newLink = GetQuestItemLink(questInfoType, index);
        return GetMaxEquippedItemLevelDelta(newLink);
    end
    API.GetRewardItemLevelDelta = GetRewardItemLevelDelta;

    local function IsItemAnUpgrade(newLink)
        local delta, isReady = GetMaxEquippedItemLevelDelta(newLink)
        return (delta and delta > 0), isReady
    end
    API.IsItemAnUpgrade = IsItemAnUpgrade;
    API.IsItemAnUpgrade_External = IsItemAnUpgrade;     --Override our API if Pawn is installed (see SupportedAddOns/Pawn.lua)

    local function IsRewardItemUpgrade(questInfoType, index)
        local newLink = GetQuestItemLink(questInfoType, index);
        return API.IsItemAnUpgrade_External(newLink)
    end
    API.IsRewardItemUpgrade = IsRewardItemUpgrade;


    if C_TooltipInfo then
        addon.TooltipAPI = C_TooltipInfo;

    else
        --For Classic where C_TooltipInfo doesn't exist:

        local TooltipAPI = {};
        local CreateColor = CreateColor;
        local TOOLTIP_NAME = "DialogueUIVirtualTooltip";
        local TP = CreateFrame("GameTooltip", TOOLTIP_NAME, nil, "GameTooltipTemplate");
        local UIParent = UIParent;

        TP:SetOwner(UIParent, 'ANCHOR_NONE');
        TP:SetClampedToScreen(false);
        TP:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", 0, -128);
        TP:Show();
        TP:SetScript("OnUpdate", nil);
        TP.shouldManuallyAddItemLevel = not addon.IsToCVersionEqualOrNewerThan(50500);


        local UpdateFrame = CreateFrame("Frame");

        local function UpdateTooltipInfo_OnUpdate(self, elapsed)
            self.t = self.t + elapsed;
            if self.t > 0.2 then
                self.t = 0;
                self:SetScript("OnUpdate", nil);
                CallbackRegistry:Trigger("SharedTooltip.TOOLTIP_DATA_UPDATE", 0);
            end
        end

        function UpdateFrame:OnItemChanged(numLines)
            self.t = 0;
            self.numLines = numLines;
            self:SetScript("OnUpdate", UpdateTooltipInfo_OnUpdate);
        end

        local function GetTooltipHyperlink()
            local name, link = TP:GetItem();
            if link then
                return link
            end

            name, link = TP:GetSpell();
            if link then
                return "spell:"..link
            end
        end

        local function GetTooltipTexts()
            local numLines = TP:NumLines();
            if numLines == 0 then return end;

            local tooltipData = {};
            tooltipData.dataInstanceID = 0;

            local shouldAddItemLevel = TP.shouldManuallyAddItemLevel;
            local itemLevel;
            local itemLink = GetTooltipHyperlink();

            if itemLink then
                if itemLink ~= TP.hyperlink then
                    UpdateFrame:OnItemChanged(numLines);
                end

                if shouldAddItemLevel and API.IsEquippableItem(itemLink) then
                    itemLevel = API.GetItemLevel(itemLink);
                end
            end

            TP.hyperlink = itemLink;
            tooltipData.hyperlink = itemLink;

            local lines = {};
            local n = 0;

            local fs, text;
            for i = 1, numLines do
                if i == 2 and shouldAddItemLevel and itemLevel then
                    --Default tooltip shows item level after Mists
                    n = n + 1;
                    lines[n] = {
                        leftText = L["Format Item Level"]:format(itemLevel);
                        leftColor = CreateColor(1, 0.82, 0),
                    };
                end

                fs = _G[TOOLTIP_NAME.."TextLeft"..i];
                if fs then
                    n = n + 1;

                    local r, g, b = fs:GetTextColor();
                    text = fs:GetText();
                    local lineData = {
                        leftText = text,
                        leftColor = CreateColor(r, g, b),
                        rightText = nil,
                        wrapText = true,
                        leftOffset = 0,
                    };

                    fs = _G[TOOLTIP_NAME.."TextRight"..i];
                    if fs then
                        text = fs:GetText();
                        if text and text ~= "" then
                            r, g, b = fs:GetTextColor();
                            lineData.rightText = text;
                            lineData.rightColor = CreateColor(r, g, b);
                        end
                    end

                    lines[n] = lineData;
                end
            end

            local sellPrice = API.GetItemSellPrice(itemLink);
            if sellPrice then
                n = n + 1;
                lines[n] = {
                    leftText = "",  --this will be ignored by our tooltip
                    price = sellPrice,
                };
            end

            tooltipData.lines = lines;
            return tooltipData
        end

        do
            local accessors = {
                SetItemByID = "GetItemByID",
                SetCurrencyByID = "GetCurrencyByID",
                SetQuestItem = "GetQuestItem",
                SetQuestCurrency = "GetQuestCurrency",
                SetSpellByID = "GetSpellByID",
                SetItemByGUID = "GetItemByGUID",
                SetHyperlink = "GetHyperlink",
            };

            for accessor, getterName in pairs(accessors) do
                if TP[accessor] then
                    local function GetterFunc(...)
                        TP:ClearLines();
                        TP:SetOwner(UIParent, "ANCHOR_PRESERVE");
                        TP[accessor](TP, ...);
                        return GetTooltipTexts();
                    end

                    TooltipAPI[getterName] = GetterFunc;
                end
            end
        end

        addon.TooltipAPI = TooltipAPI;


        local tinsert = table.insert;
        local match = string.match;
        local gsub = string.gsub;

        local function RemoveBrackets(text)
            return gsub(text, "[()（）]", "")
        end

        local function Pattern_RemoveControl(text)
            return gsub(text, "%%c", "");
        end

        local function Pattern_WrapSpace(text)
            return gsub(Pattern_RemoveControl(text), "%%s", "%(%.%+%)");
        end

        local function RemoveThousandSeparator(numberText)
            if numberText then
                numberText = gsub(numberText, "[,%.%-]", "");   --Include %- as a temp fix for French locale bug
                return numberText
            end
        end

        local STATS_PATTERN;
        local STATS_ORDER = {
            "dps", "armor", "stamina", "strength", "agility", "intellect", "spirit",
        };

        local STATS_NAME = {
            dps = STAT_DPS_SHORT or "DPS",      --ITEM_MOD_DAMAGE_PER_SECOND_SHORT
            armor = RESISTANCE0_NAME or "Armor",
            stamina = SPELL_STAT3_NAME or "Stamina",
            strength = SPELL_STAT1_NAME or "Strengh",
            agility = SPELL_STAT2_NAME or "Agility",
            intellect = SPELL_STAT4_NAME or "Intellect",
            spirit = SPELL_STAT5_NAME or "Spirit",
        };

        local function BuildStatsPattern()
            local PATTERN_DPS = L["Match Stat DPS"]             --Pattern_WrapSpace(RemoveBrackets(DPS_TEMPLATE or "(%s damage per second)"));
            local PATTERN_ARMOR = L["Match Stat Armor"]         --Pattern_WrapSpace(ARMOR_TEMPLATE or "%s Armor");
            local PATTERN_STAMINA = L["Match Stat Stamina"]     --Pattern_WrapSpace(ITEM_MOD_STAMINA or "%c%s Stamina");
            local PATTERN_STRENGTH = L["Match Stat Strength"]   --Pattern_WrapSpace(ITEM_MOD_STRENGTH or "%c%s Strength");
            local PATTERN_AGILITY = L["Match Stat Agility"]     --Pattern_WrapSpace(ITEM_MOD_AGILITY or "%c%s Agility");
            local PATTERN_INTELLECT = L["Match Stat Intellect"] --Pattern_WrapSpace(ITEM_MOD_INTELLECT or "%c%s Intellect");
            local PATTERN_SPIRIT = L["Match Stat Spirit"]       --Pattern_WrapSpace(ITEM_MOD_SPIRIT or "%c%s Spirit");

            STATS_PATTERN = {
                dps = PATTERN_DPS,
                armor = PATTERN_ARMOR,
                stamina = PATTERN_STAMINA,
                strength = PATTERN_STRENGTH,
                agility = PATTERN_AGILITY,
                intellect = PATTERN_INTELLECT,
                spirit = PATTERN_SPIRIT,
            };
        end

        local function GetItemStatsFromTooltip()
            if not STATS_PATTERN then
                BuildStatsPattern();
            end

            local numLines = TP:NumLines();
            if numLines == 0 then return end;

            local stats = {};
            local n = 0;

            local fs, text, value;
            for i = 3, numLines do
                fs = _G[TOOLTIP_NAME.."TextLeft"..i];
                if fs then
                    n = n + 1;
                    text = fs:GetText();
                    if text and text ~= " " then
                        for key, pattern in pairs(STATS_PATTERN) do
                            if not stats[key] then
                                value = match(text, pattern);
                                if value then
                                    value = RemoveThousandSeparator(value);
                                    value = tonumber(value) or 0;
                                    if key == "dps" then
                                        value = value * 0.1;
                                    end
                                    stats[key] = value;
                                end
                            end
                        end
                    end
                end
            end

            return stats
        end

        local function AreItemsSameType(item1, item2)
            local classID1, subclassID1 = select(6, GetItemInfoInstant(item1));
            local classID2, subclassID2 = select(6, GetItemInfoInstant(item2));
            return classID1 == classID2 and subclassID1 == subclassID2;
        end

        local function BuildItemComparison(newItemStats, newItem, slotID)
            local equippedItemLink = GetInventoryItemLink("player", slotID);
            if equippedItemLink then
                TP:ClearLines();
                TP:SetOwner(UIParent, "ANCHOR_PRESERVE");
                TP:SetHyperlink(equippedItemLink);
                local equippedItemStats = GetItemStatsFromTooltip();

                if newItemStats and equippedItemStats then
                    local v1;   --new item
                    local v2;   --equipped item
                    local deltaStats;

                    --Show item level diffs
                    v1 = API.GetItemLevel(newItem);
                    v2 = API.GetItemLevel(equippedItemLink);

                    if v1 and v2 and v1 ~= v2 then
                        if not deltaStats then
                            deltaStats = {};
                        end
                        tinsert(deltaStats, FormatValueDiff(v1 - v2, L["Item Level"]));
                    end

                    for _, k in ipairs(STATS_ORDER) do
                        v1 = newItemStats[k] or 0;
                        v2 = equippedItemStats[k] or 0;
                        if v1 ~= v2 then
                            if not deltaStats then
                                deltaStats = {};
                            end
                            tinsert(deltaStats, FormatValueDiff(v1 - v2, STATS_NAME[k]));
                        end
                    end

                    local info = {
                        deltaStats = deltaStats or {},
                        equippedItemLink = equippedItemLink,
                    };

                    return info, AreItemsSameType(newItem, equippedItemLink)
                end
            end
        end

        local function GetItemComparisonInfo(item)
            --Classic
            local _, _, _, itemEquipLoc = GetItemInfoInstant(item);
            local slotID = itemEquipLoc and EQUIPLOC_SLOTID[itemEquipLoc];

            if slotID then
                TP:ClearLines();
                TP:SetOwner(UIParent, "ANCHOR_PRESERVE");
                if type(item) == "number" then
                    TP:SetItemByID(item);
                else
                    TP:SetHyperlink(item);
                end
                local newItemStats = GetItemStatsFromTooltip();
                local item1Info, areItemsSameType = BuildItemComparison(newItemStats, item, slotID);

                local compairsonInfo;
                if item1Info then
                    if not compairsonInfo then
                        compairsonInfo = {};
                    end
                    tinsert(compairsonInfo, item1Info);
                end

                local item2Info;
                if slotID == 11 then
                    item2Info = BuildItemComparison(newItemStats, item, 12);
                elseif slotID == 13 then
                    item2Info = BuildItemComparison(newItemStats, item, 14);
                end

                if item2Info then
                    if not compairsonInfo then
                        compairsonInfo = {};
                    end
                    tinsert(compairsonInfo, item2Info);
                end

                return compairsonInfo, areItemsSameType
            end
        end
        API.GetItemComparisonInfo = GetItemComparisonInfo;
    end


    local ON_USE = ITEM_SPELL_TRIGGER_ONUSE or"Use:";
    local ON_EQUIP = ITEM_SPELL_TRIGGER_ONEQUIP or"Equip:";
    --local ON_PROC = ITEM_SPELL_TRIGGER_ONPROC or"Chance on hit:";

    local ITEMLINK_CACHED = {};

    local function GetItemEffect(itemLink)
        local cached = true;
        local classID, subClassID = select(6, GetItemInfoInstant(itemLink));
        if classID == 4 and subClassID == 0 then
            local tooltipData = addon.TooltipAPI.GetHyperlink(itemLink);
            if tooltipData and tooltipData.lines then
                if not ITEMLINK_CACHED[itemLink] then
                    ITEMLINK_CACHED[itemLink] = true;
                    cached = false;
                end
                local effectText, processed;
                for _, lineData in ipairs(tooltipData.lines) do
                    processed = false;
                    if lineData.leftText then
                        if find(lineData.leftText, ON_USE) then
                            processed = true;
                            if effectText then
                                effectText = effectText.."\n"..lineData.leftText;
                            else
                                effectText = lineData.leftText;
                            end
                        end

                        if (not processed) and find(lineData.leftText, ON_EQUIP) then
                            processed = true;
                            if effectText then
                                effectText = effectText.."\n"..lineData.leftText;
                            else
                                effectText = lineData.leftText;
                            end
                        end
                    end
                end
                return effectText, cached
            end
        end
        return nil, true
    end
    API.GetItemEffect = GetItemEffect;
end

do  -- Items
    local IsEquippableItem = C_Item.IsEquippableItem or IsEquippableItem or AlwaysFalse;
    local IsCosmeticItem = C_Item.IsCosmeticItem or IsCosmeticItem or AlwaysFalse;
    local GetTransmogItemInfo = (C_TransmogCollection and C_TransmogCollection.GetItemInfo) or AlwaysFalse;
    local GetItemLevel = C_Item.GetDetailedItemLevelInfo or GetDetailedItemLevelInfo or AlwaysZero;
    local GetItemInfoInstant = C_Item.GetItemInfoInstant;
    local GetItemInfo = C_Item.GetItemInfo;
    local IsDressableItem = C_Item.IsDressableItemByID or IsDressableItem or AlwaysFalse;
    local GetQuestItemLink = GetQuestItemLink;
    local GetToyInfo = C_ToyBox and C_ToyBox.GetToyInfo or AlwaysNil;

    API.IsEquippableItem = IsEquippableItem;
    API.IsCosmeticItem = IsCosmeticItem;
    API.GetTransmogItemInfo = GetTransmogItemInfo;
    API.GetItemInfo = GetItemInfo;
    API.IsDressableItem = IsDressableItem;

    local function _GetItemLevel(item)
        if item then
            return GetItemLevel(item) or 0
        end
    end
    API.GetItemLevel = _GetItemLevel;

    local function IsItemValidForComparison(itemID)
        return itemID and (not IsCosmeticItem(itemID)) and IsEquippableItem(itemID)
    end
    API.IsItemValidForComparison = IsItemValidForComparison;

    local function GetItemSellPrice(item)
        if item then
            local sellPrice = select(11, GetItemInfo(item));
            if sellPrice and sellPrice > 0 then
                return sellPrice
            end
        end
    end
    API.GetItemSellPrice = GetItemSellPrice;

    local function GetQuestChoiceSellPrice(index)
        local hyperlink = GetQuestItemLink("choice", index);
        if hyperlink and find(hyperlink, "[Ii]tem:") then
            return GetItemSellPrice(hyperlink) or 0
        else
            return 0
        end
    end
    API.GetQuestChoiceSellPrice = GetQuestChoiceSellPrice;

    local function GetItemClassification(item)
        if IsCosmeticItem(item) then
            return "cosmetic"
        end

        local itemID, _, _, _, _, classID, subClassID = GetItemInfoInstant(item);

        if API.IsContainerItem(itemID) then
            return "container"
        end

        if classID == 2 or classID == 4 then
            return "equipment"
        elseif classID == 17 then
            return "pet"
        elseif classID == 15 then
            if subClassID == 2 then
                return "pet"
            elseif subClassID == 5 then
                return "mount"
            end
        end

        local toyItemID = GetToyInfo(itemID);
        if toyItemID then
            return "toy"
        end
    end
    API.GetItemClassification = GetItemClassification;
end

do  -- Keybindings
    local GetBindingKey = GetBindingKey;

    local function GetBestInteractKey()
        local key1, key2 = GetBindingKey("INTERACTTARGET");
        local key, errorText;

        if key1 == "" then key1 = nil; end;
        if key2 == "" then key2 = nil; end;

        if key1 or key2 then
            if key1 then
                if not find(key1, "-") then
                    key = key1;
                end
            end

            if (not key) and key2 then
                if not find(key2, "-") then
                    key = key2;
                end
            end

            if not key then
                errorText = L["Cannot Use Key Combination"];
            end
        else
            errorText = L["Interact Key Not Set"];
        end

        return key, errorText
    end
    API.GetBestInteractKey = GetBestInteractKey;

    API.IsControllerMode = function()
        return addon.GetDBValue("InputDevice") ~= 1
    end
end

do  -- TextureUtil
    local function RemoveIconBorder(texture)
        texture:SetTexCoord(0.0625, 0.9375, 0.0625, 0.9375);
    end
    API.RemoveIconBorder = RemoveIconBorder;
end

do  -- Inventory Bags Container
    local NUM_BAG_SLOTS = 4;
    local GetItemCount = C_Item.GetItemCount or GetItemCount;
    local GetContainerNumSlots = C_Container.GetContainerNumSlots;
    local GetContainerItemID = C_Container.GetContainerItemID;
    local GetContainerItemQuestInfo = C_Container.GetContainerItemQuestInfo;
    local GetContainerItemInfo = C_Container.GetContainerItemInfo;

    local function GetItemBagPosition(itemID)
        local count = GetItemCount(itemID); --unused arg2: Include banks
        if count and count > 0 then 
            for bagID = 0, NUM_BAG_SLOTS do
                for slotID = 1, GetContainerNumSlots(bagID) do
                    if (GetContainerItemID(bagID, slotID) == itemID) then
                        return bagID, slotID
                    end
                end
            end
        end
    end
    API.GetItemBagPosition = GetItemBagPosition;

    local function GetBagQuestItemInfo(itemID)
        --used in Widget_QuestItemDisplay
        local bagID, slotID = GetItemBagPosition(itemID);
        if bagID and slotID then
            local containerInfo = GetContainerItemInfo(bagID, slotID);
            if containerInfo then
                local itemInfo = {};
                itemInfo.isReadable = containerInfo.isReadable;
                itemInfo.hasLoot = containerInfo.hasLoot;
                local questInfo = GetContainerItemQuestInfo(bagID, slotID);
                if questInfo then
                    itemInfo.questID = questInfo.questID;
                    itemInfo.isOnQuest = questInfo.isActive;
                end
                return itemInfo
            end
        end
    end
    API.GetBagQuestItemInfo = GetBagQuestItemInfo;

    local function GetItemLinkInBag(itemID)
        if not itemID then return end;

        local count = GetItemCount(itemID);
        if count and count > 0 then
            for bagID = 0, NUM_BAG_SLOTS do
                for slotID = 1, GetContainerNumSlots(bagID) do
                    if (GetContainerItemID(bagID, slotID) == itemID) then
                        local containerInfo = GetContainerItemInfo(bagID, slotID);
                        if containerInfo then
                            return containerInfo.hyperlink
                        end
                    end
                end
            end

            local GetInventoryItemID = GetInventoryItemID;
            for slotID = 1, 19 do
                if GetInventoryItemID("player", slotID) == itemID then
                    return GetInventoryItemLink("player", slotID)
                end
            end

            return string.format("|Hitem:%d|h", itemID)
        end
    end
    API.GetItemLinkInBag = GetItemLinkInBag;
end

do  -- Spell
    local DoesSpellExist = C_Spell.DoesSpellExist;
    local GetShapeshiftFormID = GetShapeshiftFormID or AlwaysZero;
    local GetCurrentGlyphNameForSpell = GetCurrentGlyphNameForSpell or AlwaysNil;

    API.GetShapeshiftFormID = GetShapeshiftFormID;

    local function GetGlyphIDForSpell(spellID)
        local _, glyphID = GetCurrentGlyphNameForSpell(spellID);
        return glyphID
    end
    API.GetGlyphIDForSpell = GetGlyphIDForSpell;

    if addon.IsToCVersionEqualOrNewerThan(110000) then
        local GetSpellInfo_Table = C_Spell.GetSpellInfo;    --{"name", "rank", "iconID", "castTime", "minRange", "maxRange", "spellID", "originalIconID"}

        local function GetSpellName(spellID)
            local info = spellID and DoesSpellExist(spellID) and GetSpellInfo_Table(spellID);
            if info then
                return info.name
            end

            if spellID then
                return "Unknown Spell: "..spellID
            else
                return "Unknown Spell"
            end
        end
        API.GetSpellName = GetSpellName;
    else
        local GetSpellInfo = GetSpellInfo;

        local function GetSpellName(spellID)
            if spellID and DoesSpellExist(spellID) then
                local name = GetSpellInfo(spellID);
                return name
            else
                if spellID then
                    return "Unknown Spell: "..spellID
                else
                    return "Unknown Spell"
                end
            end
        end
        API.GetSpellName = GetSpellName;
    end
end

do  -- Time -- Date
    local time = time;

    local D_DAYS = D_DAYS or "%d |4Day:Days;";
    local D_HOURS = D_HOURS or "%d |4Hour:Hours;";
    local D_MINUTES = D_MINUTES or "%d |4Minute:Minutes;";
    local D_SECONDS = D_SECONDS or "%d |4Second:Seconds;";

    local DAYS_ABBR = DAYS_ABBR or "%d |4Day:Days;"
    local HOURS_ABBR = HOURS_ABBR or "%d |4Hr:Hr;";
    local MINUTES_ABBR = MINUTES_ABBR or "%d |4Min:Min;";
    local SECONDS_ABBR = SECONDS_ABBR or "%d |4Sec:Sec;";

    local format = string.format;

    local function FormatTime(t, pattern)
        return format(pattern, t)
    end

    local function SecondsToTime(seconds, abbreviated, oneUnit)
        local intialSeconds = seconds;
        local timeString = "";
        local isComplete = false;
        local days = 0;
        local hours = 0;
        local minutes = 0;

        if seconds >= 86400 then
            days = floor(seconds / 86400);
            seconds = seconds - days * 86400;

            local dayText = FormatTime(days, (abbreviated and DAYS_ABBR) or D_DAYS);
            timeString = dayText;

            if oneUnit then
                isComplete = true;
            end
        end

        if not isComplete then
            hours = floor(seconds / 3600);
            seconds = seconds - hours * 3600;

            if hours > 0 then
                local hourText = FormatTime(hours, (abbreviated and HOURS_ABBR) or D_HOURS);
                if timeString == "" then
                    timeString = hourText;
                else
                    timeString = timeString.." "..hourText;
                end

                if oneUnit then
                    isComplete = true;
                end
            else
                if timeString ~= "" and oneUnit then
                    isComplete = true;
                end
            end
        end

        if oneUnit and days > 0 then
            isComplete = true;
        end

        if not isComplete then
            minutes = floor(seconds / 60);
            seconds = seconds - minutes * 60;

            if minutes > 0 then
                local minuteText = FormatTime(minutes, (abbreviated and MINUTES_ABBR) or D_MINUTES);
                if timeString == "" then
                    timeString = minuteText;
                else
                    timeString = timeString.." "..minuteText;
                end
                if oneUnit then
                    isComplete = true;
                end
            else
                if timeString ~= "" and oneUnit then
                    isComplete = true;
                end
            end
        end

        if (not isComplete) and seconds > 0 then
            seconds = floor(seconds);
            local secondText = FormatTime(seconds, (abbreviated and SECONDS_ABBR) or D_SECONDS);
            if timeString == "" then
                timeString = secondText;
            else
                timeString = timeString.." "..secondText;
            end
        end

        if intialSeconds < 0 then
            --WARNING_FONT_COLOR
            timeString = "|cffff4800"..timeString.."|r";
        end

        return timeString
    end
    API.SecondsToTime = SecondsToTime;

    local function SecondsToClock(seconds)
        --Clock: 00:00
        return format("%s:%02d", floor(seconds / 60), floor(seconds % 60))
    end
    API.SecondsToClock = SecondsToClock;


    local REF_TIME;
    local function GetRelativeTime()
        if not REF_TIME then
            REF_TIME = time();
            return 0
        end

        return time() - REF_TIME
    end
    API.GetRelativeTime = GetRelativeTime;
end

do  -- System
    if GetMouseFoci then
        local GetMouseFoci = GetMouseFoci;
        local function GetMouseFocus()
            local objects = GetMouseFoci();
            return objects and objects[1]
        end
        API.GetMouseFocus = GetMouseFocus;
    elseif GetMouseFocus then
        API.GetMouseFocus = GetMouseFocus;
    else
        API.GetMouseFocus = AlwaysNil;
    end

    local function TriggerQuestObjectiveTrackerDirty()
        --(Retail Only) Trigger a "SUPER_TRACKING_CHANGED" so QuestObjectiveTracker removes its popups after QuestObjectiveTrackerMixin:OnEvent
        if not C_SuperTrack then return end;

        local oldWaypoint C_Map.GetUserWaypoint();
        local hasWaypoints = oldWaypoint ~= nil;
        local isTracking = hasWaypoints and C_SuperTrack.IsSuperTrackingUserWaypoint();

        C_QuestLog.AddQuestWatch(0);
        if not hasWaypoints then
            oldWaypoint = {
                uiMapID = 84,
                position = {
                    x = 0.5,
                    y = 0.5,
                },
            };

            C_Map.SetUserWaypoint(oldWaypoint);
        end

        C_SuperTrack.SetSuperTrackedUserWaypoint(not isTracking);

        if hasWaypoints then
            C_SuperTrack.SetSuperTrackedUserWaypoint(isTracking);
        else
            C_Map.ClearUserWaypoint();
        end
    end
    API.TriggerQuestObjectiveTrackerDirty = TriggerQuestObjectiveTrackerDirty;

    local function RemoveQuestObjectiveTrackerQuestPopUp(questID)
        --QuestObjectiveTracker:RemoveAutoQuestPopUp() isn't safe
        --AutoQuest is usually auto-tracked, we change the tracking status to trigger "QUEST_WATCH_LIST_CHANGED";
        if not C_QuestLog.GetQuestWatchType then return end;

        local watchType = C_QuestLog.GetQuestWatchType(questID);
        local isWatched = watchType ~= nil;
        if isWatched then
            C_QuestLog.RemoveQuestWatch(questID);
            C_QuestLog.AddQuestWatch(questID);
        else
            C_QuestLog.AddQuestWatch(questID);
            C_QuestLog.RemoveQuestWatch(questID);
        end
    end
    API.RemoveQuestObjectiveTrackerQuestPopUp = RemoveQuestObjectiveTrackerQuestPopUp;


    local GetCurrentKeyBoardFocus = GetCurrentKeyBoardFocus;

    function API.ClearEditBoxFocus()
        local keyBoardFocus = GetCurrentKeyBoardFocus();
        if keyBoardFocus and keyBoardFocus.ClearFocus then
            securecallfunction(keyBoardFocus.ClearFocus, keyBoardFocus);
        end
    end
    CallbackRegistry:Register("DialogueUI.Show", API.ClearEditBoxFocus);
    CallbackRegistry:Register("BookUI.Show", API.ClearEditBoxFocus);


    if StaticPopup_FindVisible then
        function API.CloseGossipStaticPopups()
            local whiches = {"GOSSIP_CONFIRM", "GOSSIP_ENTER_CODE"};
            local soundUnmuted = true;
            for _, which in ipairs(whiches) do
                local dialog = StaticPopup_FindVisible(which);
                if dialog then
                    if soundUnmuted then
                        soundUnmuted = false;
                        API.BrieflyMuteUIOpenHideSound();
                    end
                    dialog:Show();
                    dialog:Hide();
                end
            end
        end
    else
        API.CloseGossipStaticPopups = AlwaysNil;
    end
end

do  -- Zone -- Location -- Area
    local function GetZoneName(areaID)
        return C_Map.GetAreaInfo(areaID)
    end
    API.GetZoneName = GetZoneName;
end

do  -- Dev Tool
    local DEV_MODE = false;

    if not DEV_MODE then return end;

    local GetQuestIDForLogIndex = C_QuestLog.GetQuestIDForLogIndex;
    local GetQuestInfo = C_QuestLog.GetInfo;

    local function GetNumQuestCanAccept()
        --numQuests include all types of quests.
        --(Account/Daily) quests don't count towards MaxQuest(35)
        if not MAX_QUESTS then
            MAX_QUESTS = C_QuestLog.GetMaxNumQuestsCanAccept();
        end

        local numShownEntries, numAllQuests = GetNumQuestLogEntries();
        local numQuests = 0;
        local questID;

        for i = 1, numShownEntries do
            questID = GetQuestIDForLogIndex(i);
            if questID ~= 0 then
                print(i, questID)
            end
            if questID ~= 0 and not API.IsAccountQuest(questID) then
                local info = GetQuestInfo(i);
                if info and (not (info.isHidden or info.isHeader)) and info.frequency == 1 then
                    numAllQuests = numAllQuests - 1;
                end
            end
        end

        return MAX_QUESTS - numAllQuests, MAX_QUESTS
    end

    local function TooltipAddInfo(tooltip, info, key)
        tooltip:AddDoubleLine(key, tostring(info[key]));
    end

    local QuestInfoFields = {
        "questID", "campaignID", "frequency", "isHeader", "isTask", "isBounty", "isStory", "isAutoComplete",
    };

    local function QuestMapLogTitleButton_OnEnter_Callback(_, button, questID)
        local tooltip = GameTooltip;
        if not tooltip:IsShown() then return end;

        local info = C_QuestLog.GetInfo(button.questLogIndex);

        for _, key in ipairs(QuestInfoFields) do
            TooltipAddInfo(tooltip, info, key)
        end
        tooltip:AddDoubleLine("Account", tostring(API.IsAccountQuest(questID)));
        tooltip:AddDoubleLine("isCalling", tostring(C_QuestLog.IsQuestCalling(questID)));
        tooltip:AddDoubleLine("QuestType", C_QuestLog.GetQuestType(questID));
        tooltip:AddDoubleLine("isRepeatable", tostring(C_QuestLog.IsRepeatableQuest(questID)));

        tooltip:Show();
    end

    EventRegistry:RegisterCallback("QuestMapLogTitleButton.OnEnter", QuestMapLogTitleButton_OnEnter_Callback, nil);
end