local MAJOR, MINOR = "LibMatic-CombatTextBounce-1.0", 7
assert(LibStub, MAJOR .. " requires LibStub")
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end

local CreateFrame = CreateFrame
local UIParent = UIParent
local WorldFrame = WorldFrame
local cos, sin, floor, max, abs = math.cos, math.sin, math.floor, math.max, math.abs
local tinsert, tremove = table.insert, table.remove
local fastrandom = _G.fastrandom or math.random
local STANDARD_TEXT_FONT = _G.STANDARD_TEXT_FONT
local PI_OVER_180 = math.pi / 180

lib.SCROLLSPEED       = 1.9
lib.FADEOUT_TIME      = 1.3
lib.HEIGHT            = 25
lib.CRIT_MAXHEIGHT    = 60
lib.CRIT_MINHEIGHT    = 30
lib.CRIT_SCALE_TIME   = 0.05
lib.CRIT_SHRINKTIME   = 0.2
lib.STAGGER_RANGE     = 20
lib.SPACING           = 10
lib.MAX_OFFSET        = 130
lib.X_ADJUSTMENT      = 80
lib.FOUNTAIN_RADIUS   = 150

lib.active = lib.active or {}
lib.pool = lib.pool or {}
lib.xDir = lib.xDir or 1
lib.host = lib.host or nil
lib.defaults = lib.defaults or {
    font = STANDARD_TEXT_FONT,
    size = 25,
    outline = "OUTLINE",
    critOutline = "OUTLINE",
    alwaysBounce = true,
    normalBounceScale = 1.5,
    bounceStyle = "mop",
}

lib.STYLES = {
    mop = {
        peak = 2.0,
        upDuration = 0.05,
        downDuration = 0.15,
        pauseDuration = 0,
    },
    legacy = {
        peak = 1.4,
        upDuration = 0.15,
        downDuration = 0.15,
        pauseDuration = 0.10,
    },
}

function lib:SetDefaultFont(path, size, outline, critOutline)
    if path then self.defaults.font = path end
    if size then self.defaults.size = size end
    if outline then self.defaults.outline = outline end
    if critOutline then self.defaults.critOutline = critOutline end
end

function lib:SetAlwaysBounce(enabled, scale)
    self.defaults.alwaysBounce = enabled and true or false
    if scale then self.defaults.normalBounceScale = scale end
end

function lib:SetBounceStyle(style)
    if self.STYLES[style] then
        self.defaults.bounceStyle = style
    end
end

function lib:GetBounceStyle()
    return self.defaults.bounceStyle
end

function lib:AttachBounce(target, opts)
    if not target or not target.CreateAnimationGroup then return nil end
    opts = opts or {}
    local style = opts.style or self.defaults.bounceStyle or "mop"
    local cfg = self.STYLES[style] or self.STYLES.mop
    local peak = opts.peak or cfg.peak
    local upDur = opts.upDuration or cfg.upDuration
    local downDur = opts.downDuration or cfg.downDuration
    local pauseDur = opts.pauseDuration or cfg.pauseDuration

    local ag = target:CreateAnimationGroup()
    local up = ag:CreateAnimation("Scale")
    up:SetScale(peak, peak)
    up:SetDuration(upDur)
    up:SetOrder(1)

    local nextOrder = 2
    if pauseDur > 0 then
        local p = ag:CreateAnimation("Pause")
        p:SetDuration(pauseDur)
        p:SetOrder(nextOrder)
        nextOrder = nextOrder + 1
    end

    local down = ag:CreateAnimation("Scale")
    down:SetScale(1 / peak, 1 / peak)
    down:SetDuration(downDur)
    down:SetOrder(nextOrder)

    ag.lctbStyle = style
    return ag
end

function lib:PlayBounce(target, opts)
    if not target or not target.CreateAnimationGroup then return nil end
    local style = (opts and opts.style) or self.defaults.bounceStyle or "mop"
    target.__lctbBounces = target.__lctbBounces or {}
    local ag = target.__lctbBounces[style]
    if not ag then
        ag = self:AttachBounce(target, { style = style })
        target.__lctbBounces[style] = ag
    end
    if ag then
        ag:Stop()
        ag:Play()
    end
    return ag
end

local function hostOnUpdate(frame, elapsed)
    local list = lib.active
    for i = #list, 1, -1 do
        local s = list[i]
        if s == nil or s.scrollTime >= lib.SCROLLSPEED then
            if s then
                s:SetAlpha(0)
                s:Hide()
                s.isCrit = nil
                s.bounceMin = nil
                s.bounceMax = nil
                s.scrollTime = 0
                tinsert(lib.pool, s)
            end
            tremove(list, i)
        else
            s.scrollTime = s.scrollTime + elapsed
            local xPos, yPos = s.scrollFunction(s)
            s.yPos = yPos
            s:ClearAllPoints()
            s:SetPoint("TOP", s.host, "BOTTOM", xPos, yPos)
            if s.scrollTime >= lib.FADEOUT_TIME then
                local a = 1 - ((s.scrollTime - lib.FADEOUT_TIME) / (lib.SCROLLSPEED - lib.FADEOUT_TIME))
                if a < 0 then a = 0 end
                s:SetAlpha(a)
            end
            if s.bounceMin and s.bounceMax then
                if s.scrollTime <= lib.CRIT_SCALE_TIME then
                    s:SetTextHeight(floor(s.bounceMin + ((s.bounceMax - s.bounceMin) * s.scrollTime / lib.CRIT_SCALE_TIME)))
                elseif s.scrollTime <= lib.CRIT_SHRINKTIME then
                    s:SetTextHeight(floor(s.bounceMax - ((s.bounceMax - s.bounceMin) * (s.scrollTime - lib.CRIT_SCALE_TIME) / (lib.CRIT_SHRINKTIME - lib.CRIT_SCALE_TIME))))
                else
                    s.bounceMin = nil
                    s.bounceMax = nil
                end
            end
        end
    end
    -- Stop ticking once nothing is scrolling; Show() re-arms us. Otherwise this
    -- OnUpdate runs every frame for the rest of the session doing nothing.
    if #list == 0 then
        frame:SetScript("OnUpdate", nil)
    end
end

local function ensureHost()
    if lib.host then return lib.host end
    local h = CreateFrame("Frame", nil, UIParent)
    h:SetSize(1, 1)
    h:SetPoint("CENTER")
    h:SetFrameStrata("HIGH")
    lib.host = h
    return h
end

local function newString(host)
    local fs = host:CreateFontString(nil, "OVERLAY")
    fs.scrollTime = 0
    return fs
end

local function acquireString(host)
    local n = #lib.pool
    if n > 0 then
        local s = lib.pool[n]
        lib.pool[n] = nil
        s.host = host
        return s
    end
    local s = newString(host)
    s.host = host
    return s
end

local function standardScroll(s)
    local t = s.scrollTime / lib.SCROLLSPEED
    local xPos = s.startX + (s.endX - s.startX) * t
    local yPos = s.startY + (s.endY - s.startY) * t
    return xPos, yPos
end

local function fountainScroll(s)
    local r = lib.FOUNTAIN_RADIUS
    local theta = 90 * s.scrollTime / lib.SCROLLSPEED
    local xPos = s.startX - s.xDir * (r * (1 - cos(theta * PI_OVER_180)))
    local yPos = s.startY + r * sin(theta * PI_OVER_180)
    return xPos, yPos
end

lib.StandardScroll = standardScroll
lib.FountainScroll = fountainScroll

local function pickLowest(s, displayType, list)
    local lowestMessage = s:GetBottom() or 0
    for i = 1, #list do
        local v = list[i]
        if v and v.yPos then
            if lowestMessage >= v.yPos - 16 - lib.SPACING then
                lowestMessage = v.yPos - 16 - lib.SPACING
            end
        end
    end
    return lowestMessage
end

function lib:Show(text, opts)
    if text == nil or text == "" then return end
    opts = opts or {}

    local host = ensureHost()
    local s = acquireString(host)

    local crit = opts.crit and true or false
    local r, g, b
    if opts.r ~= nil then r, g, b = opts.r, opts.g or 1, opts.b or 1
    elseif crit then r, g, b = 1.0, 0.1, 0.1
    else r, g, b = 1.0, 1.0, 1.0 end

    local defaults = self.defaults
    local font = opts.font or defaults.font or STANDARD_TEXT_FONT
    local outline = opts.outline or (crit and (defaults.critOutline or defaults.outline) or defaults.outline) or "OUTLINE"
    local baseSize = opts.size or defaults.size or self.HEIGHT
    s:SetFont(font, baseSize, outline)

    s:SetText(tostring(text))
    s:SetTextColor(r, g, b)
    s.scrollTime = 0

    local mode = opts.mode or "fountain"
    if mode == "fountain" then
        s.scrollFunction = fountainScroll
    elseif mode == "down" then
        s.scrollFunction = standardScroll
    else
        s.scrollFunction = standardScroll
    end

    local screenScaleY = (WorldFrame and WorldFrame:GetHeight() or 768) / 768
    local screenScaleX = (WorldFrame and WorldFrame:GetWidth() or 1024) / 1024

    local startY, endY
    if mode == "down" then
        startY = (opts.startY or 384) * screenScaleY
        endY   = (opts.endY   or 159) * screenScaleY
    else
        startY = (opts.startY or 384) * screenScaleY
        endY   = (opts.endY   or 609) * screenScaleY
    end

    local stagger = 0
    if opts.staggered ~= false then
        stagger = fastrandom(0, lib.STAGGER_RANGE) - lib.STAGGER_RANGE / 2
    end

    lib.xDir = -lib.xDir
    s.xDir = lib.xDir

    s.startX = (opts.startX or 0) * screenScaleX + stagger
    s.startY = startY
    s.endX   = (opts.endX or 0) * screenScaleX
    s.endY   = (crit or opts.sticky) and startY or endY
    s.yPos   = s.startY

    local alwaysBounce = opts.alwaysBounce
    if alwaysBounce == nil then alwaysBounce = self.defaults.alwaysBounce end
    local bounceScale = opts.bounceScale or self.defaults.normalBounceScale or 1.5

    if crit then
        s.isCrit = 1
        s.bounceMin = lib.CRIT_MINHEIGHT
        s.bounceMax = lib.CRIT_MAXHEIGHT
        s:SetTextHeight(lib.CRIT_MINHEIGHT)
    elseif alwaysBounce then
        s.isCrit = nil
        s.bounceMin = baseSize
        s.bounceMax = floor(baseSize * bounceScale)
        s:SetTextHeight(baseSize)
    else
        s.isCrit = nil
        s.bounceMin = nil
        s.bounceMax = nil
        s:SetTextHeight(baseSize)
    end

    s:ClearAllPoints()
    s:SetPoint("TOP", host, "BOTTOM", s.startX, s.startY)
    s:SetAlpha(1)
    s:Show()
    tinsert(lib.active, s)
    host:SetScript("OnUpdate", hostOnUpdate)
    return s
end

function lib:CancelAll()
    local list = lib.active
    for i = #list, 1, -1 do
        local s = list[i]
        if s then
            s:Hide()
            s:SetAlpha(0)
            s.scrollTime = lib.SCROLLSPEED
            s.isCrit = nil
            tinsert(lib.pool, s)
        end
        tremove(list, i)
    end
end

function lib:Demo()
    self:Show("1234", { r=1, g=1, b=1 })
    if C_Timer and C_Timer.After then
        C_Timer.After(0.10, function() self:Show("CRIT 9999!", { crit=true }) end)
        C_Timer.After(0.30, function() self:Show("+777", { r=0.1, g=1, b=0.1 }) end)
        C_Timer.After(0.55, function() self:Show("BIG CRIT 15321!", { crit=true, r=1, g=0.82, b=0 }) end)
        C_Timer.After(0.80, function() self:Show("2222", { mode="down", r=1, g=1, b=0.4 }) end)
    end
end
