local _, addon = ...
local L = addon.L;
local API = addon.API;
local UIFrameFade = API.UIFrameFade;
local GetDBBool = addon.GetDBBool;


local ACTION_BUTTON_SIZE = 46;
local ACTION_BUTTON_GAP = 4;
local REPOSITION_BUTTON_OFFSET = 46;


local GetItemCooldown = C_Container.GetItemCooldown;
local UnitCastingInfo = UnitCastingInfo;
local UnitChannelInfo = UnitChannelInfo;
local InCombatLockdown = InCombatLockdown;
local GetCursorPosition = GetCursorPosition;
local math = math;
local UIParent = UIParent;
local CreateFrame = CreateFrame;
local tinsert = table.insert;
local atan2 = math.atan2;
local ipairs = ipairs;


local QuickSlot = CreateFrame("Frame", nil, UIParent);
do  --QuickSlot "OnLoad"
    addon.QuickSlot = QuickSlot;
    QuickSlot:Hide();
    QuickSlot:SetSize(46, 46);
    QuickSlot:SetAlpha(0);
    QuickSlot:SetFrameStrata("HIGH");
    QuickSlot.Buttons = {};
    QuickSlot.ItemButtons = {};
    QuickSlot.SpellButtons = {};
    QuickSlot.numActiveButtons = 0;
    QuickSlot.SpellXButton = {};
    QuickSlot:SetClampedToScreen(true);
    QuickSlot:SetClampRectInsets(-ACTION_BUTTON_SIZE, ACTION_BUTTON_SIZE, 8, -8);
end

local ContextMenu;

local function ContextMenu_EditMode_OnClick(self, button)
    QuickSlot:EnableEditMode(true);
    return true
end

local function ContextMenu_HighContrast_OnClick(self, button)
    local state = not GetDBBool("QuickSlotHighContrastMode");
    addon.SetDBValue("QuickSlotHighContrastMode", state);
    QuickSlot:UseHighContrast(state);
    return false
end

local ContextMenuData = {
    {text = L["Quick Slot Reposition"], onClickFunc = ContextMenu_EditMode_OnClick},
    {text = L["Quick Slot High Contrast Mode"], onClickFunc = ContextMenu_HighContrast_OnClick,},
};


local Positioner = CreateFrame("Frame", nil, UIParent);
Positioner.alpha = 0;
Positioner:Hide();
Positioner:SetFrameStrata("BACKGROUND");
Positioner.buttonSize = 46;     --Constant
Positioner.buttonGap = 8;       --Constant
Positioner.fromRadian = 0;      --User customizable

function Positioner:GetButtonGap()
    --the gap between to round buttons
    return 8
end

function Positioner:GetRadius()
    return math.floor( (0.5 * UIParent:GetHeight()*16/9 /3) + (self.buttonSize*0.5) + 0.5 );
end

function Positioner:GetButtonCenterGap()
    local radius = self:GetRadius();
    local gapArc = self.buttonGap + self.buttonSize;
    local radianGap = gapArc/radius;
    return radianGap
end

function Positioner:GetButtonSpan(numActiveButtons)
    return (self.buttonSize + self.buttonGap) * numActiveButtons - self.buttonGap;
end

function Positioner:GetCustomPosition()
    if self.db then
        return self.db.quickslot_PositionX, self.db.quickslot_PositionY
    end
end

function Positioner:SetCustomPosition(x, y)
    self.db.quickslot_PositionX = x;
    self.db.quickslot_PositionY = y;
end

function Positioner:GetFromRadian()
    return self.fromRadian
end

function Positioner:SetFromRadian(radian)
    if not radian then return end;

    local snappedRadian = math.rad(45);

    if radian > snappedRadian then
        radian = snappedRadian;
    else
        snappedRadian = math.rad(-60) + self:GetButtonRadianByIndex(-1);
        if radian < snappedRadian then
            radian = snappedRadian;
        end
    end

    for i = 0, 1 do
        snappedRadian = self:GetButtonRadianByIndex(i);
        if radian < snappedRadian + 0.043 and radian > snappedRadian - 0.043 then
            radian = snappedRadian;
        end
    end

    self.fromRadian = radian;

    if self.db then
        self.db.quickslotFromRadian = radian;
    end
end

function Positioner:GetEditButtonRadian()
    local radius = self:GetRadius();
    return self.fromRadian + (self.buttonSize)/radius;
end

function Positioner:GetButtonRadianByIndex(index)
    local radius = self:GetRadius();
    local gapArc = self.buttonGap + self.buttonSize;
    local radianGap = gapArc/radius;

    return (1 - index)*radianGap;
end

function Positioner:GetRadianPerButton()
    local radius = self:GetRadius();
    local radianPerButton = self.buttonSize/radius;
    return radianPerButton
end

function Positioner:GetCastBar()
    return _G["PlayerCastingBarFrame"]
end

function Positioner:OnUpdate_FadeIn(elapsed)
    self.alpha = self.alpha + elapsed*5;
    if self.alpha >= 1 then
        self.alpha = 1;
        self:SetScript("OnUpdate", nil);
    end
    self:SetAlpha(self.alpha);
end

function Positioner:FadeInGuideLine()
    self:SetScript("OnUpdate", self.OnUpdate_FadeIn);
    self.t = nil;
    self:Show();
end

function Positioner:OnUpdate_FadeOut(elapsed)
    if self.t < 0.5 then
        self.t = self.t + elapsed;
        return
    end

    self.alpha = self.alpha - elapsed*2;
    if self.alpha <= 0 then
        self.alpha = 0;
        self.t = nil;
        self:Hide();
        self:SetScript("OnUpdate", nil);
    end
    self:SetAlpha(self.alpha);
end

function Positioner:FadeOutGuideLine()
    if not self.t then
        if self.alpha >= 0.8 then
            self.t = 0;
        else
            self.t = 1; --no delay
        end
    end
    self:SetScript("OnUpdate", self.OnUpdate_FadeOut);
end

function Positioner:ShowGuideLineCircle(state)
    self.showingGuideLine = state;
    if state then
        if not self.GuideLineCircle then
            self.GuideLineCircle = addon.CreateArc(self);
            self.GuideLineCircle:SetThickness(2);
            self.GuideLineCircle:SetPoint("CENTER", UIParent, "CENTER", 0, 0);

            local buttonRadian = self:GetRadianPerButton();
            local toRdian = math.rad(-45.5) + buttonRadian*0.5;
            local fromRadian = math.rad(30.5) - buttonRadian*0.5;
            self.GuideLineCircle:SetToRadian(toRdian);
            self.GuideLineCircle:SetFromRadian(fromRadian);
            self.GuideLineCircle:SetAlpha(0.5);

            --[[
            local snappedRadian = math.rad(45);

            if radian > snappedRadian then
                radian = snappedRadian;
            else
                snappedRadian = math.rad(-60) + self:GetButtonRadianByIndex(-1);
                if radian < snappedRadian then
                    radian = snappedRadian;
                end
            end
            --]]
        end

        local radius = self:GetRadius();
        self.GuideLineCircle:SetRadius(radius);

        --self:Show();
        self:FadeInGuideLine();
    else
        --self:Hide();
        self:FadeOutGuideLine();
    end
end

function Positioner:SetCircleMaskPosition(x, y)
    if self.CircleMask2 then
        self.CircleMask2:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y);
    end
end

function Positioner:HideGuideLine()
    self.showingGuideLine = false;
    self:Hide();
    self:SetScript("OnUpdate", nil);
end


local function RealActionButton_OnLeave(self)
    if not InCombatLockdown() then
        self:SetScript("OnLeave", nil);
        self:Release();
    end

    if self.owner then
        self.owner:UnlockHighlight();
        self.owner:SetStateNormal();
        self.owner.hasActionButton = nil;
        self.owner = nil;
        QuickSlot:SetHeaderText();
        QuickSlot:StartShowingDefaultHeaderCountdown(true);
    end

    GameTooltip:Hide();
end

local function RealActionButton_PostClick(self, button)
    local owner = self.owner;

    if owner then
        if owner.onClickFunc then
            owner.onClickFunc();
        end

        if button == "LeftButton" and owner:HasCharges() then
            owner:ShowPostClickEffect();
            return
        end

        if button == "RightButton" then
            if not ContextMenu then
                ContextMenu = addon.GetSharedContextMenu();
            end
            local menu = ContextMenu;
            if menu:IsShown() then
                menu:CloseMenu();
                return
            end
            menu:SetOwner(owner);
            menu:ClearAllPoints();
            menu:SetPoint("LEFT", owner, "RIGHT", 12, 0);

            local menuData;
            if QuickSlot.buttonData.developerInfo then
                menuData = API.CopyTable(ContextMenuData);
                tinsert(menuData, {
                    type = "divider",
                });
                tinsert(menuData, {
                    --type == "info",
                    color = {0.5, 0.5, 0.5},
                    text = L["Quickslot Module Info"];
                    tooltip = QuickSlot.buttonData.developerInfo,
                });
            else
                menuData = ContextMenuData;
            end
            menu:SetContent(menuData);
            menu:Show();
        end
    end
end

local function RealActionButton_OnMouseDown(self, button)
    if self.owner then
        if self.owner:HasCharges() then
            self.owner:SetStatePushed();
        end
    end
end

local function RealActionButton_OnMouseUp(self)
    if self.owner then
        self.owner:SetStateNormal();
    end
end

local function ItemButton_OnEnter(self)
    if self.overrideName then
        QuickSlot:SetHeaderText(self.overrideName);
    else
        if self.actionType == "item" then
            QuickSlot:SetHeaderText(API.GetColorizedItemName(self.id));
        elseif self.actionType == "spell" then
            QuickSlot:SetHeaderText(C_Spell.GetSpellName(self.id));
        end
    end

    QuickSlot:StartShowingDefaultHeaderCountdown(false);

    local privateKey = "QuickSlot";
    local RealActionButton = addon.AcquireSecureActionButton(privateKey);

    if RealActionButton then
        local w, h = self:GetSize();
        RealActionButton:SetFrameStrata("DIALOG");
        RealActionButton:SetFixedFrameStrata(true);
        RealActionButton:SetScript("OnEnter", nil);
        RealActionButton:SetScript("OnLeave", RealActionButton_OnLeave);
        RealActionButton:SetScript("PostClick", RealActionButton_PostClick);
        RealActionButton:SetScript("OnMouseDown", RealActionButton_OnMouseDown);
        RealActionButton:SetScript("OnMouseUp", RealActionButton_OnMouseUp);
        RealActionButton:ClearAllPoints();
        RealActionButton:SetParent(self);
        RealActionButton:SetSize(w, h);
        RealActionButton:SetPoint("CENTER", self, "CENTER", 0, 0);
        RealActionButton:Show();
        RealActionButton.owner = self;

        local macroText;
        if self.onClickFunc then

        elseif self.macroText then
            macroText = self.macroText;
        else
            if self.actionType == "item" then
                macroText = string.format("/use item:%s", self.id);
            elseif self.actionType == "spell" then
                local spellName = C_Spell.GetSpellName(self.id);
                if spellName then
                    macroText = string.format("/cast %s", spellName);
                end
            end
        end
        RealActionButton:SetAttribute("type1", "macro");
        RealActionButton:SetMacroText(macroText);
        RealActionButton:RegisterForClicks("LeftButtonDown", "LeftButtonUp", "RightButtonUp");

        self:LockHighlight();
        self.hasActionButton = true;
    end

    if self.tooltipLines then
        local tooltip = GameTooltip;
        tooltip:Hide();
        tooltip:SetOwner(self, "ANCHOR_RIGHT");
        for i, text in ipairs(self.tooltipLines) do
            if i == 1 then
                tooltip:SetText(text, 1, 1, 1, true);
            else
                tooltip:AddLine(text, 1, 1, 1, true);
            end
        end
        tooltip:Show();
    end
end

local function ItemButton_OnLeave(self)
    if not (self:IsVisible() and self:IsMouseOver()) then
        QuickSlot:SetHeaderText();
        QuickSlot:StartShowingDefaultHeaderCountdown(true);
        GameTooltip:Hide();
    end
end




function QuickSlot:Init()
    Positioner.db = PlumberDB;
    Positioner:SetFromRadian(Positioner.db.quickslotFromRadian);
    self.side = 1;

    local Header = self:CreateFontString(nil, "OVERLAY", "GameTooltipText");
    self.Header = Header;
    Header:SetJustifyH("CENTER");
    Header:SetJustifyV("MIDDLE");
    Header:SetPoint("BOTTOM", self, "TOP", 0, 8);
    Header:SetSpacing(2);

    local font, height = GameTooltipText:GetFont();
    Header:SetFont(font, height, "");   --OUTLINE
    Header:SetShadowColor(0, 0, 0);
    Header:SetShadowOffset(1, -1);

    --[[
    local HeaderShadow = self:CreateTexture(nil, "ARTWORK");
    HeaderShadow:SetPoint("TOPLEFT", Header, "TOPLEFT", -8, 6);
    HeaderShadow:SetPoint("BOTTOMRIGHT", Header, "BOTTOMRIGHT", 8, -8);
    HeaderShadow:SetTexture("Interface/AddOns/Plumber/Art/Button/GenericTextDropShadow");
    HeaderShadow:Hide();
    HeaderShadow:SetAlpha(0);
    --]]

    local HeaderShadow = self:CreateTexture(nil, "ARTWORK");
    HeaderShadow:SetTexture("Interface/AddOns/Plumber/Art/Frame/SubtitleShadow_NineSlice_Darker");
    HeaderShadow:SetTextureSliceMargins(30, 30, 30, 30);
    HeaderShadow:SetTextureSliceMode(0);
    HeaderShadow:Hide();
    HeaderShadow:SetAlpha(0);
    HeaderShadow:SetPoint("TOPLEFT", Header, "TOPLEFT", -20, 20);
    HeaderShadow:SetPoint("BOTTOMRIGHT", Header, "BOTTOMRIGHT", 20, -20);

    function QuickSlot:SetHeaderText(text, transparentText)
        if self:IsInEditMode() then return end;

        if text then
            Header:SetSize(0, 0);
            Header:SetText(text);
            if transparentText then
                local toAlpha = self.highContrastMode and 1.0 or 0.6;
                UIFrameFade(Header, 0.5, toAlpha);
                UIFrameFade(HeaderShadow, 0.25, 0);
            else
                API.UIFrameFadeIn(Header, 0.25);
                UIFrameFade(HeaderShadow, 0.25, 1);
            end

            local textWidth = Header:GetWrappedWidth() - 2;
            if textWidth > QuickSlot.headerMaxWidth then
                Header:SetSize(QuickSlot.headerMaxWidth, 64);
                local numLines = Header:GetNumLines();
                Header:SetHeight(numLines*18);
                textWidth = Header:GetWrappedWidth();
                Header:SetWidth(textWidth + 2);
            end
        else
            UIFrameFade(Header, 0.5, 0);
            UIFrameFade(HeaderShadow, 0.25, 0);
        end
    end

    self.SpellCastOverlay = addon.CreateActionButtonSpellCastOverlay(self);
    self.SpellCastOverlay:Hide();

    self.Init = nil;
end

local function ContainerFrame_OnUpdate(self, elapsed)
    self.t = self.t + elapsed;
    if self.t > 1 then
        self:SetScript("OnUpdate", nil);
        self:SetHeaderText(self.defaultHeaderText, true);
    end
end

function QuickSlot:SetDefaultHeaderText(text)
    self.defaultHeaderText = text;
end

function QuickSlot:StartShowingDefaultHeaderCountdown(state)
    if state and not self:IsInEditMode() then
        self.t = 0;
        self:SetScript("OnUpdate", ContainerFrame_OnUpdate);
    else
        self:SetScript("OnUpdate", nil);
    end
end

function QuickSlot:SetButtonData(buttonData)
    if buttonData == self.buttonData then
        return
    end

    local privateKey = "QuickSlot";
    addon.HideSecureActionButton(privateKey);

    self.buttonData = buttonData;
    self.systemName = buttonData.systemName;
    self.layoutDirty = true;
    self.anyItemAction = nil;
    self.numActiveButtons = #buttonData.buttons;
    self.spellcastType = buttonData.spellcastType;
    self.SpellXButton = {};
    self.ItemButtons = {};
    self.SpellButtons = {};

    local buttonSize = ACTION_BUTTON_SIZE;
    local gap = ACTION_BUTTON_GAP;
    local positionIndex = 0;
    local trackIndex = 0;

    local anyItemAction;
    local anySpellAction;

    for i, info in ipairs(buttonData.buttons) do
        positionIndex = positionIndex + 1;
        if info.spacer then
            --Used as a spacer

        elseif info.track then
            --reset radian, reduce radius
            positionIndex = 0;
            trackIndex = trackIndex + 1;
        else
            local button = self.Buttons[i];
            if not button then
                button = addon.CreatePeudoActionButton(self);
                tinsert(self.Buttons, button);
                button:SetPoint("LEFT", self, "LEFT", (i - 1) * (buttonSize +  gap), 0);
            end

            local spellID = info.spellID;
            if spellID then
                self.SpellXButton[spellID] = button;
            end

            if info.actionType == "item" then
                anyItemAction = true;
                button:SetItem(info.itemID, info.icon);
                tinsert(self.ItemButtons, button);
            elseif info.actionType == "spell" then
                anySpellAction = true;
                button:SetSpell(spellID, info.icon);
                tinsert(self.SpellButtons, button);
            end

            button.spellID = spellID;
            button.positionIndex = positionIndex;
            button.trackIndex = trackIndex;
            button.overrideName = info.name;
            button.macroText = info.macroText;
            button.onClickFunc = info.onClickFunc;
            button.tooltipLines = info.tooltipLines;
            button:SetScript("OnEnter", ItemButton_OnEnter);
            button:SetScript("OnLeave", ItemButton_OnLeave);
            button:Show();

            if info.enabled ~= nil then
                if info.enabled then
                    button:SetIconState(1);
                else
                    button:SetIconState(2);
                end
            end
        end
    end

    for i = self.numActiveButtons + 1, #self.Buttons do
        self.Buttons[i]:Hide();
    end

    if self.numActiveButtons > 1 then
        self.layoutIndex = 2;
    else
        self.layoutIndex = 1;
    end

    self.anyItemAction = anyItemAction;
    self.anySpellAction = anySpellAction;

    if anySpellAction then
        self:RegisterEvent("SPELL_UPDATE_CHARGES");
        self:RegisterEvent("SPELL_UPDATE_COOLDOWN");
    else
        self:UnregisterEvent("SPELL_UPDATE_CHARGES");
        self:UnregisterEvent("SPELL_UPDATE_COOLDOWN");
    end

    if not self.Init then
        self:UpdateFrameLayout();
    end
end

function QuickSlot:SetButtonOrder(side)
    if side ~= self.side then
        self.side = side;
    else
        return
    end

    if not self.buttonData then
        return
    end

    local items = self.itemData;
    local spells = self.spellData;

    if side > 0 then
        --right side of the screen
    else
        --left side
        --items = API.ReverseList(items);
        --spells = API.ReverseList(spells);
    end

    --for i, button in ipairs(self.Buttons) do
    --    button:SetItem(items[i]);
    --    button.spellID = spells[i];
    --    self.SpellXButton[ spells[i] ] = button;
    --end
end

function QuickSlot:SetFrameLayout(layoutIndex)
    local buttonSize = Positioner.buttonSize;
    local buttonGap = Positioner.buttonGap;

    local radius = math.floor( (0.5 * UIParent:GetHeight()*16/9 /3) + (buttonSize*0.5) + 0.5);
    local track0Radius = radius;
    local gapArc = buttonGap + buttonSize;
    local fromRadian = Positioner:GetFromRadian();
    local radianGap = gapArc/radius;
    local radian;
    local x, y;
    local cx, cy = UIParent:GetCenter();

    local cos = math.cos;
    local sin = math.sin;

    if layoutIndex == 1 then
        --Normal, below the center
        x, y = Positioner:GetCustomPosition();
        if not (x and y) then
            --x = cx + radius * cos(fromRadian);
            --y = cy + radius * sin(fromRadian);
            x = cx + radius;
            y = cy;
        end

        self:ClearAllPoints();
        self:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y);

        for i, button in ipairs(self.Buttons) do
            button:ClearAllPoints();
            button:SetPoint("CENTER", self, "CENTER", (i - 1) * (buttonSize +  buttonGap), 0);
        end

        if self.numActiveButtons > 1 then
            local buttonMiddlePoint = 0.5 * Positioner:GetButtonSpan(self.numActiveButtons or 1);
            self.Header:ClearAllPoints();
            self.Header:SetPoint("BOTTOM", self, "TOPLEFT", buttonMiddlePoint, 8);
            self.headerMaxWidth = 0;
        else
            self.Header:ClearAllPoints();
            self.Header:SetPoint("RIGHT", self, "LEFT", -16, 0);
            self.headerMaxWidth = 240;
        end

        if self.RepositionButton then
            self.RepositionButton:ClearAllPoints();
            self.RepositionButton:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y + REPOSITION_BUTTON_OFFSET);
        end
    else
        --Circular, on the right side
        local trackIndex = 0;

        for i, button in ipairs(self.Buttons) do
            button:ClearAllPoints();

            if trackIndex ~= button.trackIndex then
                trackIndex = button.trackIndex;
                radius = track0Radius - trackIndex * gapArc;
                radianGap = gapArc/radius;
            end

            radian = fromRadian + (1 - button.positionIndex or i)*radianGap;
            x = cx + radius * cos(radian);
            y = cy + radius * sin(radian);
            button:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y);

            if i == 2 then
                Positioner:SetCircleMaskPosition(x, y);
            end
        end

        local headerRadiusOffset = 112;  --Positive value moves towards center
        local headerMaxWidth = 2*(headerRadiusOffset - buttonSize*0.5) - 8;
        radian = fromRadian - (self.numActiveButtons - 1)*radianGap*0.5;
        x = cx + (radius - headerRadiusOffset) * cos(radian);
        y = cy + (radius - headerRadiusOffset) * sin(radian);

        self.headerMaxWidth = headerMaxWidth;
        self.Header:ClearAllPoints();
        self.Header:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y);

        if self.RepositionButton then
            --Adjust Radian:
            radian = Positioner:GetEditButtonRadian();
            self.RepositionButton:ClearAllPoints();
            self.RepositionButton:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx + radius * cos(radian), cy + radius * sin(radian));
            --self.RepositionButton:SetRotation(radian);

            --Adjust Radius:
            --[[
            radian = fromRadian;
            radius = radius + buttonSize;
            self.RepositionButton:ClearAllPoints();
            self.RepositionButton:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx + radius * cos(radian), cy + radius * sin(radian));
            self.RepositionButton:SetRotation(radian);
            --]]
        end
    end
end

function QuickSlot:UpdateFrameLayout()
    self:SetFrameLayout(self.layoutIndex or 2);
end

function QuickSlot:SetInteractable(state, dueToCombat)
    if state then
        UIFrameFade(self, 0.5, 1);
    else
        if dueToCombat then
            if self:IsShown() and not self:IsInEditMode() then
                local toAlpha = 0.25;
                UIFrameFade(self, 0.2, toAlpha);
            end
        else

        end
    end

    for i, button in ipairs(self.Buttons) do
        button:SetEnabled(state);
        button:EnableMouse(state);
        button:UnlockHighlight();
    end
end

function QuickSlot:UpdateItemCount()
    for i, button in ipairs(self.ItemButtons) do
        button:UpdateCount();
    end
end

function QuickSlot:UpdateSpellCharge()
    for i, button in ipairs(self.SpellButtons) do
        button:UpdateCount();
    end
end

function QuickSlot:OnSpellCastChanged(spellID, isStartCasting)
    local targetButton = self.SpellXButton[spellID];

    if self.lastSpellTargetButton then
        self.lastSpellTargetButton.Count:Show();
    end
    self.lastSpellTargetButton = targetButton;

    if targetButton then
        if isStartCasting then
            self.isPlayerMoving = false;
            self.isChanneling = true;
            for i, button in ipairs(self.Buttons) do
                if button.spellID == spellID then
                    local _, _, _, startTime, endTime = UnitChannelInfo("player");
                    if not _ then
                        _, _, _, startTime, endTime = UnitCastingInfo("player");
                    end

                    self.SpellCastOverlay:ClearAllPoints();
                    self.SpellCastOverlay:SetPoint("CENTER", button, "CENTER", 0, 0);
                    self.SpellCastOverlay:FadeIn();
                    self.SpellCastOverlay:SetDuration( (endTime - startTime) / 1000);
                    self.SpellCastOverlay:SetFrameStrata("HIGH");

                    button.Count:Hide();
                end
            end
        else
            self.isChanneling = false;
            self.lastSpellTargetButton = nil;
            self.SpellCastOverlay:FadeOut();
        end
    end
end

function QuickSlot:OnShow()
    self.isClosing = nil;
end

function QuickSlot:OnHide()
    self.isClosing = nil;
    self:EnableEditMode(false);
end
QuickSlot:SetScript("OnHide", QuickSlot.OnHide);


local function GetCursorRadianToPoint(cx, cy, uiRatio)
    local x, y = GetCursorPosition();
    x = x * uiRatio;
    y = y * uiRatio;
    return atan2(y - cy, x - cx);
end

local function RepositionButton_OnUpdate_Radial(self, elapsed)
    self.t = self.t + elapsed;
    if self.t >= 0.016 then
        self.t = 0;
        local radian = GetCursorRadianToPoint(self.cx, self.cy, self.uiRatio);
        if radian ~= self.radian then
            self.radian = radian;
            Positioner:SetFromRadian(self.frameRadian + radian - self.selfRadian);
            QuickSlot:UpdateFrameLayout();
            --[[
            if radian > -1.57 and radian < 1.57 then
                QuickSlot:SetButtonOrder(1);
            else
                QuickSlot:SetButtonOrder(-1);
            end
            --]]
        end
    end
end

local function RepositionButton_OnUpdate_FreeMove(self, elapsed)
    self.t = self.t + elapsed;
    if self.t >= 0.016 then
        self.t = 0;
        local x, y = GetCursorPosition();
        x = self.cxOffset + x * self.uiRatio;
        y = self.cyOffset + y * self.uiRatio - REPOSITION_BUTTON_OFFSET;
        Positioner:SetCustomPosition(x, y);
        QuickSlot:UpdateFrameLayout();
    end
end

local function RepositionButton_OnMouseDown(self, button)
    if button == "RightButton" then
        if QuickSlot.layoutIndex == 1 then
            Positioner:SetCustomPosition(nil, nil);
        else
            Positioner:SetFromRadian(0);
        end
        QuickSlot:UpdateFrameLayout();
        return
    end
    self.t = 0;
    self.cx, self.cy = UIParent:GetCenter();
    self.uiRatio = 1/ UIParent:GetEffectiveScale();
    self.radian = GetCursorRadianToPoint(self.cx, self.cy, self.uiRatio);
    self.selfRadian = Positioner:GetEditButtonRadian();
    self.frameRadian = Positioner:GetFromRadian();

    local cx0, cy0 = GetCursorPosition();
    cx0 = cx0 * self.uiRatio;
    cy0 = cy0 * self.uiRatio;
    local x0, y0 = self:GetCenter();
    self.cxOffset = x0 - cx0;
    self.cyOffset = y0 - cy0;

    local isRadial = QuickSlot.layoutIndex ~= 1;
    if isRadial then
        self:SetScript("OnUpdate", RepositionButton_OnUpdate_Radial);
        Positioner:ShowGuideLineCircle(true);
    else
        self:SetScript("OnUpdate", RepositionButton_OnUpdate_FreeMove);
        Positioner:ShowGuideLineCircle(false);
    end
    QuickSlot:SetInteractable(false);
    self:LockHighlight();
end

local function RepositionButton_OnMouseUp(self)
    self.t = nil;
    self.cx, self.cy = nil, nil;
    self:SetScript("OnUpdate", nil);
    self:UnlockHighlight();

    Positioner:ShowGuideLineCircle(false);
end

local function RepositionButton_OnClick(self)
    local delta = -1;   --clock-wise
    local oldRadian = Positioner:GetFromRadian();
    local dRadian = Positioner:GetButtonCenterGap();
    local newRadian = oldRadian + delta*dRadian;
    Positioner:SetFromRadian(newRadian);
    QuickSlot:UpdateFrameLayout();
end

local function RepositionButton_SetRotation(self, radian)
    self.Icon:SetRotation(radian);
    self.Highlight:SetRotation(radian);
end

function QuickSlot:IsInEditMode()
    return self.isEditing == true
end

local function SetControlNodeAnimation(obj)
    local ag = obj:CreateAnimationGroup();
    obj.AnimIn = ag;

    local s1 = ag:CreateAnimation("Scale");
    s1:SetOrder(1);
    s1:SetDuration(0);
    s1:SetScale(0.25, 0.25);

    local s2 = ag:CreateAnimation("Scale");
    s2:SetOrder(2);
    s2:SetDuration(0.3);
    s2:SetScale(6, 6);
    s2:SetSmoothing("IN_OUT");

    local s3 = ag:CreateAnimation("Scale");
    s3:SetOrder(3);
    s3:SetDuration(0.4);
    s3:SetScale(0.67, 0.67);
    s3:SetSmoothing("OUT");
end

function QuickSlot:EnableEditMode(state)
    if state then
        if not self.RepositionButton then
            local b = CreateFrame("Button", nil, self);
            b:SetSize(16, 16);
            self.RepositionButton = b;
            b:SetFrameStrata("DIALOG");
            b:SetFrameLevel(500);
            b:SetFixedFrameStrata(true);
            b:SetClampedToScreen(true);
            local offset = 46;
            b:SetClampRectInsets(-offset, offset, offset, -offset);

            local tex = "Interface/AddOns/Plumber/Art/Button/RepositionButton-Circle";

            b.Icon = b:CreateTexture(nil, "ARTWORK");
            b.Icon:SetSize(16, 16);
            b.Icon:SetPoint("CENTER", b, "CENTER", 0, 0);
            b.Icon:SetTexture(tex, nil, nil, "TRILINEAR");

            b.Highlight = b:CreateTexture(nil, "HIGHLIGHT");
            b.Highlight:SetSize(32, 32);
            b.Highlight:SetPoint("CENTER", b, "CENTER", 0, 0);
            b.Highlight:SetTexture(tex.."-Highlight", nil, nil, "TRILINEAR");

            b.SetRotation = RepositionButton_SetRotation;
            --b:SetScript("OnClick", RepositionButton_OnClick);
            b:SetScript("OnMouseDown", RepositionButton_OnMouseDown);
            b:SetScript("OnMouseUp", RepositionButton_OnMouseUp);

            SetControlNodeAnimation(b);
        end

        if not self.EditModeConfirmButton then
            local b = CreateFrame("Button", nil, self);
            b:SetSize(30, 30);
            self.EditModeConfirmButton = b;
            b:SetFrameStrata("DIALOG");
            b:SetFixedFrameStrata(true);
            b:SetClampedToScreen(true);
            local offset = 24;
            b:SetClampRectInsets(-offset, offset, offset, -offset);

            local tex = "Interface/AddOns/Plumber/Art/Button/EditMode-Confirm";

            b.Icon = b:CreateTexture(nil, "ARTWORK");
            b.Icon:SetSize(32, 32);
            b.Icon:SetPoint("CENTER", b, "CENTER", 0, 0);
            b.Icon:SetTexture(tex);
            API.DisableSharpening(b.Icon);

            b.Highlight = b:CreateTexture(nil, "HIGHLIGHT");
            b.Highlight:SetSize(32, 32);
            b.Highlight:SetPoint("CENTER", b, "CENTER", 0, 0);
            b.Highlight:SetTexture(tex.."-Highlight");
            API.DisableSharpening(b.Highlight);

            b:SetPoint("CENTER", self.Header, "CENTER", 0, 0);
            b:SetScript("OnClick", function()
                self:EnableEditMode(false);
            end);
        end

        if not self.isEditing then
            self.RepositionButton:Show();
            self.EditModeConfirmButton:Show();
            self.RepositionButton.AnimIn:Play();
            UIFrameFade(self.EditModeConfirmButton, 0.25, 1, 0);
            self:SetInteractable(false);
            self:SetHeaderText();   --HUD_EDIT_MODE_MENU
            self:StartShowingDefaultHeaderCountdown(false);
            for i, button in ipairs(self.Buttons) do
                button.Count:Hide();
            end
            self.isEditing = true;
            self:UpdateFrameLayout();

            self.EditModeConfirmButton:ClearAllPoints();
            if self.layoutIndex == 1 then
                self.EditModeConfirmButton:SetPoint("CENTER", self, "CENTER", -54, 0);
            else
                self.EditModeConfirmButton:SetPoint("CENTER", self.Header, "CENTER", 0, 0);
            end
        end
    else
        if self.isEditing then
            self.isEditing = nil;
            self.RepositionButton:Hide();
            self.RepositionButton:SetScript("OnUpdate", nil);
            self.EditModeConfirmButton:Hide();
            Positioner:HideGuideLine();
            for i, button in ipairs(self.Buttons) do
                if button ~= self.lastSpellTargetButton then
                    button.Count:Show();
                end
            end
            if not InCombatLockdown() then
                self:SetInteractable(true);
            end

            if self.closeUIAfterEditing then
                self.closeUIAfterEditing = nil;
                self:CloseUI();
            end
        else
            return
        end
    end
end

function QuickSlot:CloseUIAfterEditing()
    self.closeUIAfterEditing = true;
end

function QuickSlot:ShowUI()
    if self.Init then
        self:Init();
    end

    if self.layoutDirty then
        self.layoutDirty = nil;
        self:UpdateFrameLayout();
    end

    self:UpdateItemCount();

    self:RegisterEvent("BAG_UPDATE_DELAYED");
    self:RegisterEvent("PLAYER_REGEN_DISABLED");
    self:RegisterEvent("PLAYER_REGEN_ENABLED");
    self:RegisterEvent("UI_SCALE_CHANGED");
    self:RegisterEvent("LOADING_SCREEN_ENABLED");

    if self.anyItemAction then
        self:RegisterEvent("BAG_UPDATE_COOLDOWN");
        self:UpdateItemCooldowns();
    end

    if self.anySpellAction then
        self:RegisterEvent("SPELL_UPDATE_CHARGES");
        self:RegisterEvent("SPELL_UPDATE_COOLDOWN");
        self:RequestUpdateSpellCooldowns();
        self:UpdateSpellCharge();
    end

    if self.spellcastType == 1 then
        self:RegisterUnitEvent("UNIT_SPELLCAST_START", "player");
        self:RegisterUnitEvent("UNIT_SPELLCAST_STOP", "player");
    else
        self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player");
        self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player");
        self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", "player");
    end


    for _, button in ipairs(self.Buttons) do
        button.Count:Show();
    end

    self.closeUIAfterEditing = nil;
    self.isChanneling = nil;
    self.lastSpellTargetButton = nil;
    self:Show();

    if InCombatLockdown() then
        self:SetInteractable(false, true);
    else
        self:SetInteractable(true);
    end

    self:UseHighContrast(GetDBBool("QuickSlotHighContrastMode"));

    return true
end

function QuickSlot:CloseUI()
    if self:IsShown() then
        self:EnableEditMode(false);
        self.isClosing = true;
        UIFrameFade(self, 0.5, 0);
        self:UnregisterEvent("PLAYER_REGEN_DISABLED");
        self:UnregisterEvent("PLAYER_REGEN_ENABLED");
        self:UnregisterEvent("UI_SCALE_CHANGED");
        self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START");
        self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP");
        self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE");
        self:UnregisterEvent("UNIT_SPELLCAST_START");
        self:UnregisterEvent("UNIT_SPELLCAST_STOP");
        self:UnregisterEvent("LOADING_SCREEN_ENABLED");
        self:UnregisterEvent("BAG_UPDATE_DELAYED");
        self:UnregisterEvent("BAG_UPDATE_COOLDOWN");
        self:UnregisterEvent("SPELL_UPDATE_CHARGES");
        self:UnregisterEvent("SPELL_UPDATE_COOLDOWN");
        self:SetInteractable(false);
        self.isChanneling = nil;
        self.defaultHeaderText = nil;
        self.SpellCastOverlay:Hide();
    end
end

function QuickSlot:RequestCloseUI(systemName)
    if self:IsInEditMode() then
        self.closeUIAfterEditing = true;
    else
        if (not systemName) or (systemName and systemName == self.systemName) then
            self:CloseUI();
        end
    end
end

function QuickSlot:UseHighContrast(state)
    state = state == true;
    self.highContrastMode = state;

    for i, button in ipairs(self.Buttons) do
        button:UseHighContrast(state);
    end

    if self.Header then
        local font, height, flag = self.Header:GetFont();
        flag = state and "OUTLINE" or "";
        self.Header:SetFont(font, height, flag);
    end
end

function QuickSlot:UpdateItemCooldowns()
    if self.ItemButtons then
        local startTime, duration, enable;
        for _, button in ipairs(self.ItemButtons) do
            if button.id and button.actionType == "item" then
                startTime, duration, enable = GetItemCooldown(button.id);
                if enable == 1 and startTime and startTime > 0 and duration and duration > 0 then
                    button.Cooldown:SetCooldown(startTime, duration);
                    button.Cooldown:Show();
                    button.Cooldown:SetHideCountdownNumbers(false);
                else
                    button.Cooldown:Hide();
                end
            end
        end
    end
end

do  --Spell Cooldown
    local GetSpellCooldown = C_Spell.GetSpellCooldown;
    local GetSpellCharges = C_Spell.GetSpellCharges;

    local Throttler = CreateFrame("Frame", nil, QuickSlot);
    QuickSlot.Throttler = Throttler;
    Throttler:SetScript("OnHide", function(self)
        self:SetScript("OnUpdate", nil);
        self.pauseUpdate = nil;
    end);

    local function Throttler_OnUpdate(self, elapsed)
        self.t = self.t + elapsed;
        if self.t >= 0.5 then
            self.t = 0;
            self:SetScript("OnUpdate", nil);
            self.pauseUpdate = nil;
        end
    end

    function QuickSlot:RequestUpdateSpellCooldowns()
        if Throttler.pauseUpdate then
            
        else
            Throttler.pauseUpdate = true;
            Throttler.t = 0;
            Throttler:SetScript("OnUpdate", Throttler_OnUpdate);
            self:UpdateSpellCooldowns();
        end
    end

    function QuickSlot:UpdateSpellCooldowns()
        local cooldownInfo, chargeInfo, startTime, duration, modRate, fromChargeCooldown;
        for _, button in ipairs(self.SpellButtons) do
            if button.id and button.actionType == "spell" then
                startTime, duration, modRate, fromChargeCooldown = nil, nil, nil, nil;

                chargeInfo = GetSpellCharges(button.id);
                if chargeInfo and chargeInfo.currentCharges > 0 then
                    if chargeInfo.cooldownStartTime > 0 and chargeInfo.cooldownDuration > 0 then
                        startTime = chargeInfo.cooldownStartTime;
                        duration = chargeInfo.cooldownDuration;
                        modRate = chargeInfo.chargeModRate;
                        fromChargeCooldown = true;
                    end
                end

                if not (startTime and duration) then
                    cooldownInfo = GetSpellCooldown(button.id);
                    if cooldownInfo and cooldownInfo.isEnabled and cooldownInfo.startTime > 0 and cooldownInfo.duration > 0 then
                        startTime = cooldownInfo.startTime;
                        duration = cooldownInfo.duration;
                        modRate = cooldownInfo.modRate
                    end
                end

                if startTime and duration then
                    button.Cooldown:SetCooldown(startTime, duration, modRate);
                    button.Cooldown:Show();
                    if fromChargeCooldown then
                        button.Cooldown:SetHideCountdownNumbers(true);
                    else
                        button.Cooldown:SetHideCountdownNumbers(false);
                    end
                else
                    button.Cooldown:Hide();
                end
            end
        end
    end
end

function QuickSlot:OnEvent(event, ...)
    if event == "BAG_UPDATE_DELAYED" then
        self:UpdateItemCount();
    elseif event == "SPELL_UPDATE_COOLDOWN" then
        self:RequestUpdateSpellCooldowns();
    elseif event == "PLAYER_REGEN_DISABLED" then
        self:SetInteractable(false, true);
    elseif event == "PLAYER_REGEN_ENABLED" then
        if not self.isEditing then
            self:SetInteractable(true);
        end
    elseif event == "UI_SCALE_CHANGED" then
        self:UpdateFrameLayout();
    elseif event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_START" then
        local _, _, spellID = ...
        QuickSlot:OnSpellCastChanged(spellID, true);
    elseif event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then

    elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" or event == "UNIT_SPELLCAST_STOP" then
        local _, _, spellID = ...
        self:OnSpellCastChanged(spellID, false);
    elseif event == "LOADING_SCREEN_ENABLED" then
        self:CloseUI();
    elseif event == "BAG_UPDATE_COOLDOWN" then
        self:UpdateItemCooldowns();
    elseif event == "SPELL_UPDATE_CHARGES" then
        self:UpdateSpellCharge();
    end
end
QuickSlot:SetScript("OnEvent", QuickSlot.OnEvent);