local _, addon = ...
local API = addon.API;
local PixelUtil = addon.PixelUtil;
local L = addon.L;
local Round = API.Round;
local GetItemQualityColor = API.GetItemQualityColor;
local IsItemValidForComparison = API.IsItemValidForComparison;
local GetTransmogItemInfo = API.GetTransmogItemInfo;
local C_TooltipInfo = addon.TooltipAPI;
local C_TransmogCollection = C_TransmogCollection;
local IsDressableItemByID = C_Item.IsDressableItemByID;
local GetItemInfoInstant = GetItemInfoInstant;

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

SharedTooltip:Hide();
SharedTooltip:SetSize(16, 16);
SharedTooltip:SetIgnoreParentScale(true);
SharedTooltip:SetIgnoreParentAlpha(true);
SharedTooltip:SetFrameStrata("TOOLTIP");
SharedTooltip:SetFixedFrameStrata(true);
SharedTooltip:SetClampedToScreen(true);
SharedTooltip:SetClampRectInsets(-4, 4, 4, -4);

SharedTooltip.ShowFrame = SharedTooltip.Show;
SharedTooltip.HideFrame = SharedTooltip.Hide;

local LINE_TYPE_PRICE = Enum.TooltipDataLineType.SellPrice or 11;

local FONT_LARGE = "DUIFont_Tooltip_Large";
local FONT_MEDIUM = "DUIFont_Tooltip_Medium";
local FONT_SMALL = "DUIFont_Tooltip_Small";
local FONT_HEIGHT_MEDIUM = 12;
local FONTSTRING_MIN_GAP = 24;  --Betweeb the left and the right text of the same line
local SELL_PRICE_TEXT = (SELL_PRICE or "Sell Price").."  ";
local NORMAL_FONT_COLOR = NORMAL_FONT_COLOR;

do
    local _;
    _, FONT_HEIGHT_MEDIUM = _G[FONT_MEDIUM]:GetFont();
    FONT_HEIGHT_MEDIUM = Round(FONT_HEIGHT_MEDIUM);
end

local SPACING_NEW_LINE = 4;     --Between paragraphs
local SPACING_INTERNAL = 2;     --Within the same paragraph
local TOOLTIP_PADDING = 12;
local TOOLTIP_MAX_WIDTH = 256;
local FONTSTRING_MAX_WIDTH = TOOLTIP_MAX_WIDTH - 2*TOOLTIP_PADDING;
local FONTSTRING_SHRINK_IF_MODEL = 36;
local MODEL_WIDTH, MODEL_HEIGHT = 78, 104;
local FORMAT_ICON_TEXT = "|T%s:0:0:0:-"..SPACING_NEW_LINE.."|t %s";

local HOTKEY_ALTERNATE_MODE = "Shift";

local unpack = unpack;
local pairs = pairs;
local max = math.max;
local PI2 = math.floor(1000*math.pi*2)/1000;
local format = string.format;


local function PreviewModel_Turntable_OnUpdate(self, elapsed)
    self.yaw = self.yaw + elapsed * PI2 * 0.1;
    if self.yaw > PI2 then
        self.yaw = self.yaw - PI2;
    end
    self:SetFacing(self.yaw);
end

local Model_ApplyUICamera = Model_ApplyUICamera;
local function PreviewModel_OnModelLoaded(self)
    if self.cameraID then
        Model_ApplyUICamera(self, self.cameraID);
    end

    self.parent:SyncAnimation();
end

local DualModelMixin = {};

function DualModelMixin:Init(parent)
    local model = CreateFrame("DressUpModel", nil, parent);
    self.Model1 = model;
    model.parent = self;

    API.SetModelLight(model, true, false, -1, 1, -1, 0.8, 1, 1, 1, 0.5, 1, 1, 1);
    model:SetKeepModelOnHide(true);
    model:SetModelDrawLayer("ARTWORK");
    model:SetAutoDress(false);
    model:SetDoBlend(false);
    model:SetScript("OnModelLoaded", PreviewModel_OnModelLoaded);

    local modelShadow = CreateFrame("DressUpModel", nil, parent);
    self.Model2 = modelShadow;
    modelShadow.parent = self;

    API.SetModelLight(modelShadow, false);
    modelShadow:SetPoint("CENTER", model, "CENTER", 4, -4);
    modelShadow:SetKeepModelOnHide(true);
    modelShadow:SetModelDrawLayer("BORDER");
    modelShadow:SetAutoDress(false);
    modelShadow:SetDoBlend(false);
    modelShadow:SetScript("OnModelLoaded", PreviewModel_OnModelLoaded);

    local a = 0;
    modelShadow:SetFogColor(a, a, a);
    modelShadow:SetParticlesEnabled(false);

    --local inset = 12;
    --model:SetViewInsets(inset, inset, inset, inset);  --Push model farther
    --modelShadow:SetViewInsets(inset, inset, inset, inset);
end

function DualModelMixin:SetModelSize(width, height)
    self.width, self.height = width, height;
    self.Model1:SetSize(width, height);
    self.Model2:SetSize(width, height);
end

function DualModelMixin:GetWidth()
    return self.width
end

function DualModelMixin:SetAnimation(animID)
    self.animID = animID;
    self:SyncAnimation();
end

function DualModelMixin:SyncAnimation()
    if self.animID then
        self.Model1:SetAnimation(self.animID, 0);
        self.Model2:SetAnimation(self.animID, 0);
    end
end

function DualModelMixin:UseModelCenterToTransform(state)
    self.Model1:UseModelCenterToTransform(state);
    self.Model2:UseModelCenterToTransform(state);
end

function DualModelMixin:SetPoint(point, relativeTo, relativePoint, x, y)
    self.point = point;
    self.relativeTo = relativeTo;
    self.relativePoint = relativePoint;
    self.x = x;
    self.y = y;
    self.Model1:SetPoint(point, relativeTo, relativePoint, x, y);
end

function DualModelMixin:SetOffsetY(ratio)
    local y = self.height * ratio;
    self.y = y;
    self:SetPoint(self.point, self.relativeTo, self.relativePoint, self.x, y);
end

function DualModelMixin:ClearModel()
    if self.hasModel then
        self.hasModel = false;
        self.Model1:ClearModel();
        self.Model2:ClearModel();
    end
end

function DualModelMixin:SetCameraID(cameraID)
    self.Model1.cameraID = cameraID;
    self.Model2.cameraID = cameraID;
end

function DualModelMixin:ResetPosition()
    self.Model1:SetPosition(0, 0, 0);
    self.Model1:SetPitch(0);
    self.Model1:SetRoll(0);
    self.Model2:SetPosition(0, 0, 0);
    self.Model2:SetPitch(0);
    self.Model2:SetRoll(0);
end

function DualModelMixin:SetYaw(yaw)
    self.Model1:SetFacing(yaw);
    self.Model2:SetFacing(yaw);
end

function DualModelMixin:SetDisplayInfo(creatureDisplayID)
    self.Model1:SetDisplayInfo(creatureDisplayID);
    self.Model2:SetDisplayInfo(creatureDisplayID);
end

function DualModelMixin:SetModelByUnit(unit)
    API.SetModelByUnit(self.Model1, unit);
    API.SetModelByUnit(self.Model2, unit);
end

function DualModelMixin:SetItem(item)
    self.Model1:SetItem(item);
    self.Model2:SetItem(item);
end

function DualModelMixin:FreezeAnimation(animID, variation, frame)
    self.Model1:FreezeAnimation(animID, variation, frame);
    self.Model2:FreezeAnimation(animID, variation, frame);
end

function DualModelMixin:TryOn(item)
    self.Model1:TryOn(item);
    self.Model2:TryOn(item);
end

function DualModelMixin:SetUseTransmogChoices(state)
    self.Model1:SetUseTransmogChoices(state);
    self.Model2:SetUseTransmogChoices(state);

    self.Model1:SetUseTransmogSkin(state);
    self.Model2:SetUseTransmogSkin(state);
end

function DualModelMixin:SetUseTurntable(useTurntable)
    if useTurntable then
        self.Model1.yaw = -0.78;
        self.Model1:SetScript("OnUpdate", PreviewModel_Turntable_OnUpdate);
        self.Model2.yaw = -0.78;
        self.Model2:SetScript("OnUpdate", PreviewModel_Turntable_OnUpdate);
    else
        self.Model1:SetScript("OnUpdate", nil);
        self.Model2:SetScript("OnUpdate", nil);
        self.Model1.yaw = 0;
        self.Model2.yaw = 0;
    end
end

function DualModelMixin:SetModelAlpha(alpha)
    self.Model1:SetModelAlpha(alpha);
    self.Model2:SetModelAlpha(0.8 * alpha);
end


function SharedTooltip:UpdatePixel(scale)
    if not self.Background then return end;

    if not scale then
        scale = self:GetEffectiveScale();
    end

    local pixelOffset = 16.0;
    local offset = API.GetPixelForScale(scale, pixelOffset);
    self.Background:ClearAllPoints();
    self.Background:SetPoint("TOPLEFT", self, "TOPLEFT", -offset, offset);
    self.Background:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", offset, -offset);
end

function SharedTooltip:Init()
    if not self.Background then
        local texture = self:CreateTexture(nil, "BACKGROUND", nil, -1);
        local corner = 32;
        self.Background = texture;
        texture:SetTextureSliceMargins(corner, corner, corner, corner);
        texture:SetTextureSliceMode(1);
        texture:SetTexture(addon.ThemeUtil:GetTextureFile("TooltipBackground.png"));

        PixelUtil:AddPixelPerfectObject(self);
    end

    if not self.Content then
        self.Content = CreateFrame("Frame", nil, self);
        self.Content:SetWidth(8);   --Temp
        self.Content:SetPoint("TOPLEFT", self, "TOPLEFT", TOOLTIP_PADDING, -TOOLTIP_PADDING);
        self.Content:SetPoint("BOTTOMLEFT", self, "BOTTOMLEFT", TOOLTIP_PADDING, TOOLTIP_PADDING);
        --self.Content:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -TOOLTIP_PADDING, TOOLTIP_PADDING);
    end

    if not self.fontStrings then
        self.fontStrings = {};
    end

    if not self.PreviewFrame then
        self.PreviewFrame = CreateFrame("Frame", nil, self);
        self.PreviewFrame:SetPoint("TOPRIGHT", self, "TOPRIGHT", -TOOLTIP_PADDING, -TOOLTIP_PADDING);
        self.PreviewFrame:Hide();

        self.modelWidth, self.modelHeight = MODEL_WIDTH, MODEL_HEIGHT;
        self.PreviewFrame:SetSize(MODEL_WIDTH, MODEL_HEIGHT);

        self.DualModel = API.CreateFromMixins(DualModelMixin);
        self.DualModel:Init(self.PreviewFrame);
        self.DualModel:SetModelSize(MODEL_WIDTH, MODEL_HEIGHT);
        self.DualModel:SetPoint("TOPRIGHT", self, "TOPRIGHT", -TOOLTIP_PADDING, 0.33*MODEL_HEIGHT);
    end

    self:UpdatePixel();
    self.Init = nil;
end

function SharedTooltip:ClearLines()
    if self.numLines == 0 then return end;

    self.numLines = 0;
    self.numCols = 1;
    self.numFontStrings = 0;
    self.dataInstanceID = nil;
    self.hyperlink = nil;
    self.fontChanged = false;
    self.grid = {};
    self.useGridLayout = false;

    if self.triggeredByEvent then
        self.triggeredByEvent = nil;
    else
        self.itemID = nil;
    end

    if self.fontStrings then
        for _, fontString in pairs(self.fontStrings) do
            fontString:Hide();
            fontString:SetText(nil);
        end
    end

    if self.usePreviewModel then
        self.usePreviewModel = false;
        self.DualModel:ClearModel();
    end

    if self.PreviewFrame then
        self.PreviewFrame:Hide();
    end

    self:HideHotkey();
end

function SharedTooltip:GetHyperlink()
    return self.hyperlink
end

function SharedTooltip:GetItemID()
    if self.hyperlink then
        return GetItemInfoInstant(self.hyperlink)
    end
end

do
    function SharedTooltip:DisplayModel()
        local itemID = self:GetItemID();
        local usePreview;
    
        if itemID then
            local DualModel = self.DualModel;
            local useTurntable;
    
            if IsDressableItemByID(itemID) then
                usePreview = true;
    
                local appearanceID, sourceID = GetTransmogItemInfo(itemID);
    
                if appearanceID then
                    if API.IsHoldableItem(itemID) then  --Weapons
                        local cameraID = C_TransmogCollection.GetAppearanceCameraIDBySource(sourceID);
    
                        DualModel:SetCameraID(cameraID);
                        DualModel:SetItem(itemID, sourceID);
                    else    --Armor
                        local cameraID = C_TransmogCollection.GetAppearanceCameraIDBySource(sourceID);
    
                        local useTransmogSkin, setupGear = API.GetTransmogSetup(itemID);
                        DualModel:SetUseTransmogChoices(useTransmogSkin);
                        DualModel:SetCameraID(cameraID);
                        DualModel:SetModelByUnit("player");
                        DualModel:FreezeAnimation(0, 0, 0);
                        DualModel:TryOn(sourceID);
    
                        if setupGear then
                            for _, v in ipairs(setupGear) do
                                DualModel:TryOn(v);
                            end
                        end
                    end
                else    --Transmog Set
                    useTurntable = true;
                    local detailsCameraID, vendorCameraID = C_TransmogSets.GetCameraIDs()
    
                    DualModel:SetCameraID(vendorCameraID);
                    DualModel:SetModelByUnit("player");
                    DualModel:FreezeAnimation(0, 0, 0);
                    DualModel:TryOn("item:"..itemID);
                end
                DualModel:SetModelSize(MODEL_WIDTH, MODEL_HEIGHT);
                DualModel:SetOffsetY(0.33);
            else
                local mountID = C_MountJournal.GetMountFromItem(itemID);
                local displayID;
    
                if mountID then
                    usePreview = true;
                    local creatureDisplayID, description, _, isSelfMount, _, modelSceneID, animID, spellVisualKitID, disablePlayerMountPreview = C_MountJournal.GetMountInfoExtraByID(mountID);
                    displayID = creatureDisplayID;
                    if isSelfMount then
                        DualModel:SetAnimation(618);
                    else
                        DualModel:SetAnimation(0);
                    end
                else
                    local _, _, _, creatureID, _, description, _, _, _, _, _, creatureDisplayID, speciesID = C_PetJournal.GetPetInfoByItemID(itemID);
                    if creatureDisplayID then
                        displayID = creatureDisplayID;
                        usePreview = true;
                        DualModel:SetAnimation(0);
    
                    else
    
                    end
                end
    
                if displayID then
                    useTurntable = true;
                    DualModel:ClearModel();
                    DualModel:SetCameraID(nil);
                    DualModel:UseModelCenterToTransform(false);
                    DualModel:SetModelSize(MODEL_HEIGHT, MODEL_HEIGHT); --Square
                    DualModel:ResetPosition();
                    DualModel:SetDisplayInfo(displayID);
                    DualModel:SetOffsetY(0.5);
                    --DualModel:SetYaw(0.44);
                end
            end
    
            DualModel:SetUseTurntable(useTurntable);
        end
    
        if usePreview then
            self.usePreviewModel = true;
            self.PreviewFrame:Show();
        else
            self.usePreviewModel = false;
            self.PreviewFrame:Hide();
        end
    end

    if not (IsDressableItemByID and C_PetJournal and C_PetJournal.GetPetInfoByItemID) then
        function SharedTooltip:DisplayModel()
        end
    end
end


function SharedTooltip:ShowHotkey(key, description, callback)
    if not self.HotkeyFrame then
        self.HotkeyFrame = CreateFrame("Frame", nil, self, "DUIDialogHotkeyTemplate");
    end

    local success = self.HotkeyFrame:SetKey(key);
    if success then
        self:AddBlankLine();
        local gap = 4;
        local width = self.HotkeyFrame:GetWidth();
        local fontString = self:AddLeftLine(description, 0.5, 0.5, 0.5, true, nil, 2, width + gap);
        self.HotkeyFrame:ClearAllPoints();
        self.HotkeyFrame:SetPoint("RIGHT", fontString, "LEFT", -gap, 0);
        self.HotkeyFrame:Show();
        self.hasHotkey = true;
        self:RegisterEvent("MODIFIER_STATE_CHANGED");
    else
        self.hasHotkey = false;
    end

    self.onHotkeyPressedCallback = callback;
end

function SharedTooltip:HideHotkey()
    if self.hasHotkey then
        self.HotkeyFrame:Hide();
        self.HotkeyFrame:ClearAllPoints();
        self:UnregisterEvent("MODIFIER_STATE_CHANGED");
        self.onHotkeyPressedCallback = nil;
    end
end

function SharedTooltip:ToggleAlternateInfo()
    if self.onHotkeyPressedCallback then
        self.onHotkeyPressedCallback(self);
    end
end

local function AlternateModeCallback_ItemComparison(self)
    DialogueUI_DB.TooltipShowItemComparison = not DialogueUI_DB.TooltipShowItemComparison;

    SharedTooltip:ReprocessInfo();
end

do
    if C_TooltipComparison and C_TooltipComparison.GetItemComparisonInfo and TooltipComparisonManager then
        function SharedTooltip:ShowItemComparison()
            local itemID = self:GetItemID();
            if not IsItemValidForComparison(itemID) then return end;

            local shouldShowComparison = DialogueUI_DB.TooltipShowItemComparison == true;
            local canCompare = false;

            local comparisonItem = TooltipComparisonManager:CreateComparisonItem(self.tooltipData);
            local compairsonInfo = comparisonItem and C_TooltipComparison.GetItemComparisonInfo(comparisonItem);

            if compairsonInfo and compairsonInfo.item and compairsonInfo.item.guid then
                local displayedItem = compairsonInfo.item;
                local delta = C_TooltipComparison.GetItemComparisonDelta(comparisonItem, displayedItem);
                --self:SetItemByGUID(compairsonInfo.item.guid);

                if delta and #delta > 0 then
                    canCompare = true;
                    if shouldShowComparison then
                        self:AddBlankLine();

                        local equippedItemLink = API.GetEquippedItemLink(itemID);
                        if equippedItemLink then
                            self:AddLeftLine(L["Format Replace Item"]:format(equippedItemLink), 1, 0.82, 0, true);
                        else
                            self:AddLeftLine(ITEM_DELTA_DESCRIPTION, 1, 0.82, 0, true);
                        end

                        local itemLevelDelta = API.GetItemLevelDelta(itemID, true);
                        if itemLevelDelta then
                            self:AddLeftLine(itemLevelDelta, 1, 1, 1, false, nil, 2, TOOLTIP_PADDING);
                        end

                        for i, deltaLine in ipairs(delta) do
                            self:AddLeftLine(deltaLine, 1, 1, 1, false, nil, 2, TOOLTIP_PADDING);
                        end
                    end
                end
            end

            if canCompare then
                local description = shouldShowComparison and L["Hide Comparison"] or L["Show Comparison"];
                self:ShowHotkey(HOTKEY_ALTERNATE_MODE, description, AlternateModeCallback_ItemComparison);
                self:Show();
            end
        end
    else
        function SharedTooltip:ShowItemComparison()
            local itemID = self:GetItemID();
            if not IsItemValidForComparison(itemID) then return end;

            local shouldShowComparison = DialogueUI_DB.TooltipShowItemComparison == true;
            local canCompare = false;

            local compairsonInfo, equippedItemLink, areItemsSameType = API.GetItemComparisonInfo(itemID);

            if compairsonInfo then
                canCompare = true;
                if shouldShowComparison then
                    self:AddBlankLine();
                    self:AddLeftLine(L["Format Replace Item"]:format(equippedItemLink), 1, 0.82, 0, true);

                    if not areItemsSameType then
                        self:AddLeftLine(L["Different Item Types Alert"], 1.000, 0.125, 0.125, true);   --TODO: this red on the Dark theme doesn't look comforting.
                    end

                    for i, deltaLine in ipairs(compairsonInfo) do
                        self:AddLeftLine(deltaLine, 1, 1, 1, false, nil, 2, TOOLTIP_PADDING);
                    end
                end
            end

            if canCompare then
                local description = shouldShowComparison and L["Hide Comparison"] or L["Show Comparison"];
                self:ShowHotkey(HOTKEY_ALTERNATE_MODE, description, AlternateModeCallback_ItemComparison);
                self:Show();
            end
        end
    end
end


function SharedTooltip:SetOwner(owner, anchor, offsetX, offsetY)
    if self.Init then
        self:Init();
    end

    self.owner = owner;
    anchor = anchor or "ANCHOR_BOTTOM";
    offsetX = offsetX or 0;
    offsetY = offsetY or 0;

    self:ClearAllPoints();

    if anchor == "ANCHOR_NONE" then
        return
    else
        self:SetPoint("BOTTOMLEFT", owner, "TOPRIGHT", offsetX, offsetY);
    end
end

function SharedTooltip:SetLineFont(fontString, sizeIndex)
    if fontString.sizeIndex ~= sizeIndex then
        fontString.sizeIndex = sizeIndex;
        if sizeIndex == 1 then
            fontString:SetFontObject(FONT_LARGE);
        elseif sizeIndex == 2 then
            fontString:SetFontObject(FONT_MEDIUM);
        elseif sizeIndex == 3 then
            fontString:SetFontObject(FONT_SMALL);
        end
        return true
    end
end

function SharedTooltip:SetLineAlignment(fontString, alignIndex)
    fontString.alignIndex = alignIndex;
    if alignIndex == 1 then
        fontString:SetJustifyH("LEFT");
    elseif alignIndex == 2 then
        fontString:SetJustifyH("CENTER");
    elseif alignIndex == 3 then
        fontString:SetJustifyH("RIGHT");
    end
    return true
end

function SharedTooltip:AcquireFontString()
    local n = self.numFontStrings + 1;
    self.numFontStrings = n;

    if not self.fontStrings[n] then
        self.fontStrings[n] = self.Content:CreateFontString(nil, "OVERLAY", FONT_MEDIUM);
        self.fontStrings[n]:SetSpacing(SPACING_INTERNAL);
        self.fontStrings[n].sizeIndex = 2;
        self.fontStrings[n].alignIndex = 1;
        self.fontStrings[n]:SetJustifyV("MIDDLE");
    end

    self.fontStrings[n].horizontalOffset = 0;

    return self.fontStrings[n]
end

function SharedTooltip:AddText(text, r, g, b, wrapText, offsetY, sizeIndex, alignIndex, horizontalOffset)
    r = r or 1;
    g = g or 1;
    b = b or 1;
    wrapText = (wrapText == true) or false;
    offsetY = offsetY or -SPACING_NEW_LINE;
    sizeIndex = sizeIndex or 2;
    alignIndex = alignIndex or 1;
    horizontalOffset = horizontalOffset or 0;

    local fs = self:AcquireFontString();

    local fontChanged = self:SetLineFont(fs, sizeIndex);
    if fontChanged then
        self.fontChanged = true;
    end
    self:SetLineAlignment(fs, alignIndex);

    fs:ClearAllPoints();
    fs:SetPoint("TOPLEFT", self.Content, "TOPLEFT", 0, 0);

    fs:SetWidth(FONTSTRING_MAX_WIDTH);
    fs:SetText(text);
    fs:SetTextColor(r, g, b, 1);
    fs:Show();
    fs.inGrid = nil;
    fs.horizontalOffset = horizontalOffset;

    return fs
end

function SharedTooltip:SetGridLine(row, col, text, r, g, b, sizeIndex, alignIndex)
    if not text then return end

    if self.grid[row] and self.grid[row][col] then
        self.grid[row][col]:SetText("(Occupied)")
        return
    end

    local fs = self:AddText(text, r, g, b, nil, nil, sizeIndex, alignIndex);
    fs.inGrid = true;

    if not self.grid[row] then
        self.grid[row] = {};
    end
    self.grid[row][col] = fs;

    if row > self.numLines then
        self.numLines = row;
    end

    if col > self.numCols then
        self.numCols = col;
    end

    self.useGridLayout = true;
end

function SharedTooltip:AddLeftLine(text, r, g, b, wrapText, offsetY, sizeIndex, horizontalOffset)
    --This will start a new line

    if (not text) or (text == "") then return end
    local n = self.numLines + 1;
    self.numLines = n;

    local alignIndex = 1;
    local fs = self:AddText(text, r, g, b, wrapText, offsetY, sizeIndex, alignIndex, horizontalOffset);

    if not self.grid[n] then
        self.grid[n] = {};
    end
    self.grid[n][1] = fs;

    return fs
end

function SharedTooltip:AddCenterLine(text, r, g, b, wrapText, offsetY, sizeIndex)
    --This will also start a new line
    --Align to the center

    if not text then return end
    local n = self.numLines + 1;
    self.numLines = n;

    local alignIndex = 2;
    local fs = self:AddText(text, r, g, b, wrapText, offsetY, sizeIndex, alignIndex);

    if not self.grid[n] then
        self.grid[n] = {};
    end
    self.grid[n][1] = fs;

    return fs
end

function SharedTooltip:SetTitle(text, r, g, b)
    self:AddLeftLine(text, r, g, b, true, nil, 1);
end

function SharedTooltip:AddRightLine(text, r, g, b, wrapText, offsetY, sizeIndex)
    --Right line must come in pairs with a LeftLine
    --This will NOT start a new line

    if not text then return end
    local alignIndex = 3;
    local fs = self:AddText(text, r, g, b, wrapText, offsetY, sizeIndex, alignIndex);
    local n = self.numLines;

    if not self.grid[n] then
        self.grid[n] = {};
    end
    self.grid[n][2] = fs;

    self.numCols = 2;

    return fs
end

function SharedTooltip:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB)
    self:AddLeftLine(leftText, leftR, leftG, leftB);
    self:AddRightLine(rightText, rightR, rightG, rightB);
end

function SharedTooltip:AddBlankLine()
    local n = self.numLines + 1;
    self.numLines = n;
    self.grid[n] = {
        gap = SPACING_NEW_LINE,
    };
end

function SharedTooltip:ProcessInfo(info)
    self.tooltipInfo = info;

    if not info then
		return false
	end

    local tooltipData;
    if info.getterArgs then
        tooltipData = C_TooltipInfo[info.getterName](unpack(info.getterArgs));
    else
        tooltipData = C_TooltipInfo[info.getterName]();
    end

    self:ClearLines();
    self:SetScript("OnUpdate", nil);

    if tooltipData then
        tooltipData.isItem = info.isItem;
    end

    local success = self:ProcessTooltipData(tooltipData);

    if success then
        self:Show();
    else
        self:Hide();
    end
end

function SharedTooltip:ReprocessInfo()
    if self.tooltipData then
        self:ClearLines();
        self:ProcessTooltipData(self.tooltipData);
        self:Show();
    end
end

local OVERRIDE_COLORS = {
    ["ffa335ee"] = 4,
    ["ff0070dd"] = 3,
};

function SharedTooltip:ProcessTooltipData(tooltipData)
    if not (tooltipData and tooltipData.lines) then
        self.tooltipData = nil;
        return false
    end

    self.tooltipData = tooltipData;
    self.dataInstanceID = tooltipData.dataInstanceID;
    self.hyperlink = tooltipData.hyperlink;

    self:RegisterEvent("TOOLTIP_DATA_UPDATE");

    local leftText, leftColor, wrapText, rightText, rightColor, leftOffset;
    local r, g, b;

    for i, lineData in ipairs(tooltipData.lines) do
        leftText = lineData.leftText;
        leftColor = lineData.leftColor or NORMAL_FONT_COLOR;
        rightText = lineData.rightText;
        wrapText = lineData.wrapText or false;
        leftOffset = lineData.leftOffset;

        if leftText then
            if leftText == " "then
                --A whole blank line is too tall, so we change its height
                self:AddBlankLine();
            else
                if i == 1 then
                    local hex = leftColor:GenerateHexColor();
                    if OVERRIDE_COLORS[hex] then
                        leftColor = GetItemQualityColor( OVERRIDE_COLORS[hex] );
                    end
                end

                if lineData.price and lineData.price > 0 then
                    local colorized = true;
                    local cointText = API.GenerateMoneyText(lineData.price, colorized);
                    if cointText then
                        leftText = SELL_PRICE_TEXT .. cointText;
                        --self:AddBlankLine();
                        self:AddLeftLine(leftText, 1, 1, 1, false, nil, 2);
                    end
                else    --lineData.type ~= LINE_TYPE_PRICE
                    r, g, b = leftColor:GetRGB();
                    self:AddLeftLine(leftText, r, g, b, wrapText, nil, (i == 1 and 1) or 2);
                end

                if rightText then
                    rightColor = lineData.rightColor or NORMAL_FONT_COLOR;
                    r, g, b = rightColor:GetRGB();
                    self:AddRightLine(rightText, r, g, b, wrapText, nil, 2);
                end
            end
        end
	end

    if tooltipData.isItem then
        self:ShowItemComparison();
    end

    return true
end

local TOPLINE_MAX_ROW = 2;

function SharedTooltip:Layout()
    local textWidth, textHeight, lineWidth;
    local topLinesWidth;
    local totalHeight = 0;
    local maxLineWidth = 0;
    local maxLineHeight = 0;
    local grid = self.grid;
    local fs;
    local ref = self.Content;
    local usePreviewModel = self.usePreviewModel;

    local colMaxWidths, rowOffsetYs;
    local numCols = self.numCols;
    local useGridLayout = self.useGridLayout;
    if useGridLayout then
        colMaxWidths = {};
        rowOffsetYs = {};
        for i = 1, numCols do
            colMaxWidths[i] = 0;
        end
    end

    for row = 1, self.numLines do
        if grid[row] then
            lineWidth = 0;
            maxLineHeight = 0;
            if grid[row].gap then
                --Positive value increase the distance between lines
                totalHeight = totalHeight + grid[row].gap;
            end
            for col = 1, numCols do
                fs = grid[row][col];
                if fs then
                    if usePreviewModel and col == 1 and row <= 3 then
                        fs:SetWidth(FONTSTRING_MAX_WIDTH - FONTSTRING_SHRINK_IF_MODEL);
                    end

                    textWidth = fs:GetWrappedWidth() + fs.horizontalOffset;
                    textHeight = fs:GetHeight();

                    if col == 1 then
                        lineWidth = textWidth;
                    else
                        lineWidth = lineWidth + FONTSTRING_MIN_GAP + textWidth;
                    end

                    if lineWidth > maxLineWidth then
                        maxLineWidth = lineWidth;
                    end

                    if textHeight > maxLineHeight then
                        maxLineHeight = textHeight;
                    end

                    if useGridLayout and textWidth > colMaxWidths[col] and fs.inGrid then
                        colMaxWidths[col] = textWidth;
                    end
                end
            end

            if row ~= 1 then
                totalHeight = totalHeight + SPACING_NEW_LINE;
            end
            totalHeight = Round(totalHeight);

            if useGridLayout then
                rowOffsetYs[row] = -totalHeight;
            else
                for col = 1, numCols do
                    fs = grid[row][col];
                    if fs then
                        fs:ClearAllPoints();
                        if fs.alignIndex == 2 then
                            fs:SetPoint("TOP", ref, "TOP", 0 + fs.horizontalOffset, -totalHeight);
                        elseif fs.alignIndex == 3 then
                            fs:SetPoint("TOPRIGHT", ref, "TOPRIGHT", 0 + fs.horizontalOffset, -totalHeight);
                        else
                            fs:SetPoint("TOPLEFT", ref, "TOPLEFT", 0 + fs.horizontalOffset, -totalHeight);
                        end
                    end
                end
            end

            totalHeight = totalHeight + maxLineHeight;
            totalHeight = Round(totalHeight);
        end

        if not topLinesWidth then
            topLinesWidth = maxLineWidth;
        elseif row <= TOPLINE_MAX_ROW then
            if maxLineWidth > topLinesWidth then
                topLinesWidth = maxLineWidth;
            end
        end
    end

    if useGridLayout then
        local offsetX, offsetY;
        for row = 1, self.numLines do
            offsetX = 0;
            offsetY = rowOffsetYs[row];
            for col = 1, numCols do
                fs = grid[row][col];
                textWidth = colMaxWidths[col] + 1;
                if fs then
                    fs:ClearAllPoints();
                    if fs.alignIndex == 2 then
                        if fs.inGrid then
                            fs:SetPoint("TOPLEFT", ref, "TOPLEFT", offsetX, offsetY);
                            fs:SetWidth(textWidth);
                        else
                            fs:SetPoint("TOP", ref, "TOP", offsetX, offsetY);
                        end
                    elseif fs.alignIndex == 3 then
                        if col == numCols then
                            fs:SetPoint("TOPRIGHT", ref, "TOPRIGHT", 0, offsetY);
                        else
                            fs:SetPoint("TOPLEFT", ref, "TOPLEFT", offsetX, offsetY);
                            fs:SetWidth(textWidth);
                        end
                    else
                        fs:SetPoint("TOPLEFT", ref, "TOPLEFT", offsetX, offsetY);
                    end
                end

                offsetX = offsetX + colMaxWidths[col] + FONTSTRING_MIN_GAP;
            end
        end

        maxLineWidth = 0;
        for col = 1, numCols do
            maxLineWidth = maxLineWidth + colMaxWidths[col];
            if col > 1 then
                maxLineWidth = maxLineWidth + FONTSTRING_MIN_GAP;
            end
        end
    end

    local contentWidth = Round(maxLineWidth);
    self.Content:SetWidth(contentWidth);

    if usePreviewModel then
        local modelWidth = self.DualModel:GetWidth();
        topLinesWidth = topLinesWidth or 0;
        topLinesWidth = topLinesWidth + modelWidth + TOOLTIP_PADDING;
        contentWidth = max(topLinesWidth, maxLineWidth);
        self:SetClampRectInsets(-4, 4, 56, -4);
    else
        self:SetClampRectInsets(-4, 4, 4, -4);
    end

    local fullWidth = Round(contentWidth) + 2*TOOLTIP_PADDING;
    local fullHeight = Round(totalHeight) + 2*TOOLTIP_PADDING;

    self:SetSize(fullWidth, fullHeight);
end

function SharedTooltip:SetFrameAlpha(alpha)
    self:SetAlpha(alpha);
    if self.DualModel then
        self.DualModel:SetModelAlpha(alpha);
    end
end

function SharedTooltip:LoadTheme()
    if self.Background then
        self.Background:SetTexture(addon.ThemeUtil:GetTextureFile("TooltipBackground.png"));
    end

    if self.HotkeyFrame then
        self.HotkeyFrame:LoadTheme();
    end
end

local function SharedTooltip_OnUpdate_FadeIn(self, elapsed)
    self.t = self.t + elapsed;
    if self.t > 0 then
        local alpha = 10*self.t;
        if alpha >= 1 then
            alpha = 1;
            self.t = nil;
            self:SetScript("OnUpdate", nil);
        end
        self:SetFrameAlpha(alpha);
    else
        self:SetFrameAlpha(0);
    end
end

local function SharedTooltip_OnUpdate_Layout(self, elapsed)
    self:SetScript("OnUpdate", nil);
    self:Layout();

    if self.showDelay then
        self:SetScript("OnUpdate", SharedTooltip_OnUpdate_FadeIn);
    end
end


function SharedTooltip:LayoutNextUpdate()
    self:SetScript("OnUpdate", SharedTooltip_OnUpdate_Layout);
end

function SharedTooltip:Show()
    local layoutComplete;

    self:DisplayModel();

    if self.fontChanged or self.useGridLayout then
        --fontString width will take one frame to change
        layoutComplete = false;
        self:LayoutNextUpdate();
    else
        layoutComplete = true;
        self:Layout();
    end

    if self.showDelay then
        self.t = self.showDelay;
        self:SetFrameAlpha(0);
        if layoutComplete then
           self:SetScript("OnUpdate", SharedTooltip_OnUpdate_FadeIn);
        end
    else
        self.t = nil;
        self:SetFrameAlpha(1);
    end

    self:ShowFrame();
end

function SharedTooltip:SetShowDelay(delay)
    if delay and delay > 0 then
        self.showDelay = -delay;
    else
        self.showDelay = nil;
    end
end

function SharedTooltip:Hide()
    self:HideFrame();
    self:ClearAllPoints();
    self:SetScript("OnUpdate", nil);
    self:ClearLines();
end

function SharedTooltip:OnEvent(event, ...)
	if event == "TOOLTIP_DATA_UPDATE" then
		local dataInstanceID = ...
		if dataInstanceID and dataInstanceID == self.dataInstanceID then
            self.triggeredByEvent = true;
			self:ProcessInfo(self.tooltipInfo);
		end
    elseif event == "MODIFIER_STATE_CHANGED" then
        local key, down = ...
        if key == "LSHIFT" and down == 1 then
            self:ToggleAlternateInfo();
        end
	end
end

SharedTooltip:SetScript("OnEvent", SharedTooltip.OnEvent);

SharedTooltip:SetScript("OnHide", function(self)
    self:UnregisterEvent("TOOLTIP_DATA_UPDATE");
    self:UnregisterEvent("MODIFIER_STATE_CHANGED");
    self.tooltipInfo = nil;
    self.tooltipData = nil;
end);


do
    --Emulate the default GameTooltip
    --Code from Interface/SharedXML/Tooltip/TooltipDataHandler.lua

    local function AddTooltipDataAccessor(handler, accessor, getterName)
        local isItem = string.find(getterName, "Item");

        handler[accessor] = function(self, ...)
            local tooltipInfo = {
                getterName = getterName,
                getterArgs = { ... };
                isItem = (isItem ~= nil) or nil,
            };
            return self:ProcessInfo(tooltipInfo);
        end
    end


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

    local handler = SharedTooltip;
    for accessor, getterName in pairs(accessors) do
		AddTooltipDataAccessor(handler, accessor, getterName);
	end
end

SharedTooltip:ClearLines();

do
    local C_Garrison = C_Garrison;
    local NUM_ABILITIES = 4;

    local function AddFollowerAbility(self, getterName, followerID, title)
        local getterFunc = C_Garrison[getterName];
        if not getterFunc then return end;

        local titleAdded;

        for index = 1, NUM_ABILITIES do
            local abilityID = getterFunc(followerID, index);
            if (not abilityID) or (abilityID == 0) then
                break
            end
            if not titleAdded then
                titleAdded = true;
                self:AddLeftLine(title, 1, 0.82, 0);
            end

            local ability = C_Garrison.GetFollowerAbilityInfo(abilityID);
            local abilityName = ability.name;
            if ability.icon then
                abilityName = format(FORMAT_ICON_TEXT, ability.icon, abilityName);
            end

            local sizeIndex = 1;
            self:AddLeftLine(abilityName, 1, 0.82, 0, false, nil, sizeIndex);

            if ability.description then
                local descriptionOffset = 16;
                self:AddLeftLine(ability.description, 1, 1, 1, true, nil, 2, descriptionOffset);
            end
        end
    end

    function SharedTooltip:SetFollowerByID(followerID)
        local info = C_Garrison.GetFollowerInfo(followerID);

        if not info then
            return false
        end

        self:ClearLines();
        self:SetScript("OnUpdate", nil);

        local name = info.name;
        self:SetTitle(name, 1, 0.82, 0);

        if info.level and info.className then
            local title = format(L["Format Follower Level Class"], info.level, info.className);
            self:AddLeftLine(title, 1, 1, 1);
        end

        local abilities = C_Garrison.GetFollowerAbilities(followerID)

        if abilities and #abilities > 0 then
            AddFollowerAbility(self, "GetFollowerAbilityAtIndexByID", followerID, L["Abilities"])
            AddFollowerAbility(self, "GetFollowerTraitAtIndexByID", followerID, L["Traits"])
        end

        --print(followerID);

        self:Show();
        return true
    end
end

do
    --Emulate for Classic
    local function TOOLTIP_DATA_UPDATE(dataInstanceID)
        if SharedTooltip:IsVisible() then
            if dataInstanceID and dataInstanceID == SharedTooltip.dataInstanceID and SharedTooltip.tooltipInfo and SharedTooltip.tooltipData then
                SharedTooltip.triggeredByEvent = true;
                local oldData = SharedTooltip.tooltipData;
                local newData;
                local info = SharedTooltip.tooltipInfo;
                if info.getterArgs then
                    newData = C_TooltipInfo[info.getterName](unpack(info.getterArgs));
                else
                    newData = C_TooltipInfo[info.getterName]();
                end
                if newData and oldData.lines and newData.lines and (#oldData.lines ~= #newData.lines) then
                    SharedTooltip:ClearLines();
                    SharedTooltip:SetScript("OnUpdate", nil);

                    newData.isItem = oldData.isItem;

                    local success = SharedTooltip:ProcessTooltipData(newData);

                    if success then
                        SharedTooltip:Show();
                    else
                        SharedTooltip:Hide();
                    end
                end
            end
        end
    end

    addon.CallbackRegistry:Register("SharedTooltip.TOOLTIP_DATA_UPDATE", TOOLTIP_DATA_UPDATE);
end


do
    local function PostFontSizeChanged()
        if SharedTooltip.HotkeyFrame then
            SharedTooltip.HotkeyFrame:UpdateBaseHeight();
        end
    end
    addon.CallbackRegistry:Register("PostFontSizeChanged", PostFontSizeChanged);

    local function PostInputDeviceChanged(dbValue)
        if SharedTooltip.HotkeyFrame then
            SharedTooltip.HotkeyFrame:UpdateBaseHeight();
        end
    end
    addon.CallbackRegistry:Register("PostInputDeviceChanged", PostInputDeviceChanged);
end