local T, C, L, _ = unpack(select(2, ...))

--[[
    tullaCooldownCount
        A basic cooldown count addon

        The purpose of this addon is mainly for me to test performance optimizations
        and also for people who don't care about the extra features of OmniCC
--]]

local Timer = {}
local timers = {}

--local bindings!
local Config = C.Module.Actionbar.cooldown  --pull in the addon table
local UIParent = _G['UIParent']
local GetTime = _G['GetTime']
local floor = math.floor
local max = math.max
local min = math.min
local round = function(x) return floor(x + 0.5) end

--sexy constants!
local ICON_SIZE = 36 --the normal size for an icon (don't change this)
local DAY, HOUR, MINUTE = 86400, 3600, 60 --used for formatting text
local DAYISH, HOURISH, MINUTEISH = 3600 * 23.5, 60 * 59.5, 59.5 --used for formatting text at transition points
local HALFDAYISH, HALFHOURISH, HALFMINUTEISH = DAY/2 + 0.5, HOUR/2 + 0.5, MINUTE/2 + 0.5 --used for calculating next update times
local MIN_DELAY = 0.01

--returns both what text to display, and how long until the next update
local function getTimeText(s)
    --format text as seconds when at 90 seconds or below
    if s < MINUTEISH then
        local seconds = round(s)
        local formatString = seconds > Config.expiringDuration and Config.secondsFormat or Config.expiringFormat
        return formatString, seconds, s - (seconds - 0.51)
    --format text as minutes when below an hour
    elseif s < HOURISH then
        local minutes = round(s/MINUTE)
        return Config.minutesFormat, minutes, minutes > 1 and (s - (minutes*MINUTE - HALFMINUTEISH)) or (s - MINUTEISH)
    --format text as hours when below a day
    elseif s < DAYISH then
        local hours = round(s/HOUR)
        return Config.hoursFormat, hours, hours > 1 and (s - (hours*HOUR - HALFHOURISH)) or (s - HOURISH)
    --format text as days
    else
        local days = round(s/DAY)
        return Config.daysFormat, days,  days > 1 and (s - (days*DAY - HALFDAYISH)) or (s - DAYISH)
    end
end

function Timer.SetNextUpdate(self, duration)
    C_Timer.After(max(duration, MIN_DELAY), self.OnTimerDone)
end

--stops the timer
function Timer.Stop(self)
    self.enabled = nil
    self.start = nil
    self.duration = nil
    self.charges = nil
    self.maxCharges = nil
    self:Hide()
end

function Timer.UpdateText(self)
    local remain = self.enabled and (self.duration - (GetTime() - self.start)) or 0

    if round(remain) > 0 then
        if (self.fontScale * self:GetEffectiveScale() / UIParent:GetScale()) < Config.minScale then
            self.text:SetText('')
            Timer.SetNextUpdate(self, 1)
        else
            local formatStr, time, timeUntilNextUpdate = getTimeText(remain)
            self.text:SetFormattedText(formatStr, time)
            Timer.SetNextUpdate(self, timeUntilNextUpdate)
        end
    else
        Timer.Stop(self)
    end
end

--forces the given timer to update on the next frame
function Timer.ForceUpdate(self)
    Timer.UpdateText(self)
    self:Show()
end

--adjust font size whenever the timer's parent size changes
--hide if it gets too tiny
function Timer.OnSizeChanged(self, width, height)
    local fontScale = round(width) / ICON_SIZE
    if fontScale == self.fontScale then
        return
    end

    self.fontScale = fontScale
    if fontScale < Config.minScale then
        self:Hide()
    else
        self.text:SetFont(Config.fontFace, fontScale * Config.fontSize, 'OUTLINE')
        self.text:SetShadowColor(0, 0, 0, 0.8)
        self.text:SetShadowOffset(1, -1)
        if self.enabled then
            Timer.ForceUpdate(self)
        end
    end
end

--returns a new timer object
function Timer.Create(cooldown)
    --a frame to watch for OnSizeChanged events
    --needed since OnSizeChanged has funny triggering if the frame with the handler is not shown
    local scaler = CreateFrame('Frame', nil, cooldown)
    scaler:SetAllPoints(cooldown)

    local timer = CreateFrame('Frame', nil, scaler); timer:Hide()
    timer:SetAllPoints(scaler)

    timer.OnTimerDone = function() Timer.UpdateText(timer) end

    local text = timer:CreateFontString(nil, 'OVERLAY')
    text:SetPoint('CENTER', 0, 0)
    text:SetFont(Config.fontFace, Config.fontSize, 'OUTLINE')
    timer.text = text

    Timer.OnSizeChanged(timer, scaler:GetSize())
    scaler:SetScript('OnSizeChanged', function(self, ...) Timer.OnSizeChanged(timer, ...) end)

    -- prevent display of blizzard cooldown text
    cooldown:SetHideCountdownNumbers(true)

    timers[cooldown] = timer

    return timer
end

function Timer.Start(cooldown, start, duration, charges, maxCharges)
    local remainingCharges = charges or 0

    --start timer
    if start > 0 and duration > Config.minDuration and remainingCharges == 0 and (not cooldown.noCooldownCount) then
        cooldown:SetDrawBling(Config.drawBling)
        cooldown:SetDrawSwipe(Config.drawSwipe)
        cooldown:SetDrawEdge(Config.drawEdge)

        local timer = timers[cooldown] or Timer.Create(cooldown)

        timer.enabled = true
        timer.start = start
        timer.duration = duration
        timer.charges = remainingCharges
        timer.maxCharges = maxCharges

        Timer.UpdateText(timer)
        if timer.fontScale >= Config.minScale then timer:Show() end
    --stop timer
    else
        local timer = timers[cooldown]
        if timer then
            Timer.Stop(timer)
        end
    end
end


do
    local f = CreateFrame('Frame'); f:Hide()

    f:SetScript('OnEvent', function()
        for cooldown, timer in pairs(timers) do
            Timer.ForceUpdate(timer)
        end
    end)

    f:RegisterEvent('PLAYER_ENTERING_WORLD')

    --hook the SetCooldown method of all cooldown frames
    --ActionButton1Cooldown is used here since its likely to always exist
    --and I'd rather not create my own cooldown frame to preserve a tiny bit of memory
    hooksecurefunc(getmetatable(_G['ActionButton1Cooldown']).__index, 'SetCooldown', Timer.Start)
end


--[[
    In WoW 4.3 and later, action buttons can completely bypass lua for updating cooldown timers
    This set of code is there to check and force tullaCC to update timers on standard action buttons
    (henceforth defined as anything that reuses blizzard's ActionButton.lua code)
--]]

local ActionBarButtonEventsFrame = _G['ActionBarButtonEventsFrame']
if not ActionBarButtonEventsFrame then return end


--[[ cooldown timer updating ]]--

local active = {}

local function cooldown_OnShow(self)
    active[self] = true
end

local function cooldown_OnHide(self)
    active[self] = nil
end

--returns true if the cooldown timer should be updated and false otherwise
local function cooldown_ShouldUpdateTimer(self, start, duration, charges, maxCharges)
    local timer = self.timer

    return not(
        timer
        and timer.start == start
        and timer.duration == duration
        and timer.charges == charges
        and timer.maxCharges == maxCharges
    )
end

local function cooldown_Update(self)
    local button = self:GetParent()
    local action = button.action

    local start, duration, enable = GetActionCooldown(action)
    local charges, maxCharges, chargeStart, chargeDuration = GetActionCharges(action)

    if cooldown_ShouldUpdateTimer(self, start, duration, charges, maxCharges) then
        Timer.Start(self, start, duration, charges, maxCharges)
    end
end

do
    local watcher = CreateFrame('Frame')

    watcher:Hide()

    watcher:SetScript('OnEvent', function(self, event)
        for cooldown in pairs(active) do
            cooldown_Update(cooldown)
        end
    end)

    watcher:RegisterEvent('ACTIONBAR_UPDATE_COOLDOWN')
end


--[[ hook action button registration ]]--

do
    local hooked = {}

    local function actionButton_Register(frame)
        local cooldown = frame.cooldown

        if not hooked[cooldown] then
            cooldown:HookScript('OnShow', cooldown_OnShow)
            cooldown:HookScript('OnHide', cooldown_OnHide)
            hooked[cooldown] = true
        end
    end

    if ActionBarButtonEventsFrame.frames then
        for i, frame in pairs(ActionBarButtonEventsFrame.frames) do
            actionButton_Register(frame)
        end
    end

    hooksecurefunc('ActionBarButtonEventsFrame_RegisterFrame', actionButton_Register)
end
