local pluginName, pluginShared = ...
local guarded = pluginShared.guarded
local reported = pluginShared.reported or guarded

local Alert = {}
pluginShared.Alert = Alert

local L = LibStub("AceLocale-3.0"):GetLocale("AppearanceMatic")

local AppearanceMaticPink = "|cffffb6d9"

-- Favorites.lua defines this first in the load order: the star, the addon's name, then the message,
-- out as an emote or printed depending on the Emote box. The fallback keeps this file usable on its own.
local say = pluginShared.say or print

local function db()
    return pluginShared.db and pluginShared.db.profile
end

local alertFramePool = {}

local function ReleaseAlertFrame(f)
    if f._amInPool then
        return
    end
    f._amInPool = true
    if f.bounce then
        f.bounce:Stop()
    end
    if f.iconBounce then
        f.iconBounce:Stop()
    end
    if f.fadeOut then
        f.fadeOut:Stop()
    end
    alertFramePool[#alertFramePool + 1] = f
end

local function CreateAlertFrame()
    local f = table.remove(alertFramePool)
    if not f then
        f = CreateFrame("Frame", nil, UIParent)
        f.icon = f:CreateTexture(nil, "ARTWORK")
        f.skull = f:CreateTexture(nil, "OVERLAY")
        f.text = f:CreateFontString(nil, "ARTWORK", "GameFontNormalHuge")
        f.bounce = f:CreateAnimationGroup()
        f.bounceUp = f.bounce:CreateAnimation("Scale")
        f.bounceUp:SetOrder(1)
        f.bouncePause = f.bounce:CreateAnimation("Pause")
        f.bouncePause:SetOrder(2)
        f.bounceDown = f.bounce:CreateAnimation("Scale")
        f.bounceDown:SetOrder(3)
        f.iconBounce = f:CreateAnimationGroup()
        f.iconBounceUp = f.iconBounce:CreateAnimation("Scale")
        f.iconBounceUp:SetOrder(1)
        f.iconBouncePause = f.iconBounce:CreateAnimation("Pause")
        f.iconBouncePause:SetOrder(2)
        f.iconBounceDown = f.iconBounce:CreateAnimation("Scale")
        f.iconBounceDown:SetOrder(3)
        f.fadeOut = f:CreateAnimationGroup()
        f.fadeAnim = f.fadeOut:CreateAnimation("Alpha")
        f.fadeAnim:SetFromAlpha(1)
        f.fadeAnim:SetToAlpha(0)
        f.fadeAnim:SetDuration(0.6)
        f.fadeAnim:SetOrder(1)
        f.fadeOut:SetScript("OnFinished", function()
            f:Hide()
        end)
        f:SetScript("OnHide", function(self)
            if self:IsShown() then
                return
            end
            ReleaseAlertFrame(self)
        end)
    end
    f._amInPool = false
    f:SetSize(1000, 30)
    f:SetAlpha(1)
    f:Show()
    return f
end

local function ApplyFont(f)
    local profile = db()
    if not profile then
        return
    end
    local fset = profile.fontSettings
    local LSM = LibStub("LibSharedMedia-3.0", true)
    local fontPath = LSM and LSM:Fetch("font", fset.font) or STANDARD_TEXT_FONT
    f.text:SetFont(fontPath, fset.fontSize, (fset.fontOutline ~= "" and fset.fontOutline) or nil)
    f.text:SetTextColor(fset.fontColor[1], fset.fontColor[2], fset.fontColor[3])
    local Shadow = LibStub("LibMatic-AlertShadow-1.0", true)
    if Shadow then
        Shadow:ApplyTextShadow(f.text, fset.shadowStrength, {
            color = fset.fontShadowColor,
            offset = { fset.fontShadowSizeX, fset.fontShadowSizeY },
        })
        if profile.alertNotificationFormat.showIcon then
            Shadow:ApplyIconShadow(f.icon, fset.shadowStrength)
        end
    end
end

local function ApplyBounce(f)
    local profile = db()
    local style = (profile and profile.alertNotificationFormat.bounceStyle) or "legacy"
    local Bounce = LibStub("LibMatic-CombatTextBounce-1.0", true)
    if Bounce then
        Bounce:PlayBounce(f, { style = style })
        return
    end
    local peak, up, pause, down
    if style == "mop" then
        peak, up, pause, down = 2.0, 0.05, 0, 0.15
    else
        peak, up, pause, down = 1.4, 0.15, 0.10, 0.15
    end
    f.bounceUp:SetScale(peak, peak)
    f.bounceUp:SetDuration(up)
    f.bouncePause:SetDuration(pause)
    f.bounceDown:SetScale(1 / peak, 1 / peak)
    f.bounceDown:SetDuration(down)
    f.bounce:Play()
end

local function ApplyIconPop(f)
    local profile = db()
    local fmt = profile and profile.alertNotificationFormat
    if not (fmt and fmt.iconPop) then
        return
    end
    local scale = fmt.iconPopScale or 1.5
    f.iconBounceUp:SetScale(scale, scale)
    f.iconBounceUp:SetDuration(0.15)
    f.iconBouncePause:SetDuration(0.12)
    f.iconBounceDown:SetScale(1 / scale, 1 / scale)
    f.iconBounceDown:SetDuration(0.15)
    f.iconBounce:Play()
end

local function PositionFrame(f)
    local profile = db()
    local fmt = profile and profile.alertNotificationFormat
    local xPos = (fmt and fmt.xPos) or 50
    local yPos = (fmt and fmt.yPos) or 72
    local screenWidth = UIParent:GetWidth()
    local screenHeight = UIParent:GetHeight()
    local xOffset = (xPos - 50) * screenWidth / 100
    local yOffset = (yPos - 50) * screenHeight / 100
    f:ClearAllPoints()
    f:SetPoint("CENTER", UIParent, "CENTER", xOffset, yOffset)
end

local function ShouldPlaySound()
    local profile = db()
    if not profile then
        return false
    end
    local sound = profile.soundSettings
    if sound.muteAll then
        return false
    end
    if sound.inCombat == false and UnitAffectingCombat and UnitAffectingCombat("player") then
        return false
    end
    return true
end

-- Seconds a collected alert may wait behind boss banners and loot toasts before it is shown. The guard
-- default (10 s) is shorter than a raid boss kill's banner plus loot toasts, which dropped alerts.
local COLLECTED_HOLD = 120

function Alert:PlaySound(kind)
    local profile = db()
    if not (profile and ShouldPlaySound()) then
        return
    end
    local sound = profile.soundSettings
    if sound.oncePerLogin then
        self._playedOncePerLogin = self._playedOncePerLogin or {}
        if self._playedOncePerLogin[kind] then
            return
        end
        self._playedOncePerLogin[kind] = true
    end
    local name = sound.slots[kind]
    if not name then
        return
    end
    local LSM = LibStub("LibSharedMedia-3.0", true)
    local path = LSM and LSM:Fetch("sound", name)
    if not path then
        return
    end
    local Guard = LibStub("LibMatic-OverlayGuard-1.0", true)
    local Throttle = LibStub("LibMatic-SoundThrottle-1.0", true)
    local function play()
        if Throttle then
            Throttle:Play(kind, path, sound.channel or "Master")
        else
            guarded(PlaySoundFile, path, sound.channel or "Master")
        end
    end
    if Guard then
        Guard:RunWhenClear(play, { maxHold = COLLECTED_HOLD })
    else
        play()
    end
end

local lastFiredAt = {}

local function ApplySkull(f, showSkull, iconSize)
    local profile = db()
    local fmt = profile and profile.alertNotificationFormat
    local Markers = LibStub("LibMatic-MarkerPack-1.0", true)
    if not (fmt and fmt.bossSkull and showSkull and Markers) then
        f.skull:Hide()
        return
    end
    f.skull:SetTexture(Markers:GetSkull("AppearanceMatic", fmt.skullStyle))
    local size = iconSize or 24
    f.skull:SetSize(size, size)
    f.skull:ClearAllPoints()
    if fmt.showIcon then
        f.skull:SetPoint("RIGHT", f.icon, "LEFT", -4, 0)
    else
        f.skull:SetPoint("RIGHT", f.text, "LEFT", -4, 0)
    end
    f.skull:Show()
end

function Alert:ShowAlert(kind, text, icon, opts)
    local profile = db()
    if not (profile and profile.generalSettings.alertNotificationsEnabled) then
        return
    end
    local fmt = profile.alertNotificationFormat
    local cooldown = fmt.cooldown or 0
    local now = GetTime and GetTime() or 0
    if cooldown > 0 and lastFiredAt[kind] and (now - lastFiredAt[kind]) < cooldown then
        return
    end
    lastFiredAt[kind] = now

    local Guard = LibStub("LibMatic-OverlayGuard-1.0", true)
    local function build()
        local f = CreateAlertFrame()
        f.text:SetText(text)
        f.text:ClearAllPoints()
        local showingIcon = fmt.showIcon and icon
        if showingIcon then
            f.icon:SetTexture(icon)
            f.icon:SetSize(fmt.iconSize or 24, fmt.iconSize or 24)
            f.icon:ClearAllPoints()
            f.icon:SetPoint("LEFT", f, "LEFT", 0, 0)
            f.icon:Show()
            f.text:SetPoint("LEFT", f.icon, "RIGHT", 4, 0)
        else
            f.icon:Hide()
            f.text:SetPoint("CENTER", f, "CENTER", 0, 0)
        end
        ApplyFont(f)
        local showSkull = opts and opts.bossDrop and fmt.bossSkull
        if showingIcon then
            f.icon:ClearAllPoints()
            f.icon:SetPoint("LEFT", f, "LEFT", showSkull and ((fmt.iconSize or 24) + 4) or 0, 0)
            f:SetSize((f.text:GetStringWidth() or 200) + (fmt.iconSize or 24) * (showSkull and 2 or 1) + 12, 30)
        end
        ApplySkull(f, opts and opts.bossDrop, fmt.iconSize)
        PositionFrame(f)
        ApplyBounce(f)
        ApplyIconPop(f)

        f.fadeAnim:SetDuration(fmt.fadeTime or 0.6)
        f.fadeAnim:SetStartDelay(fmt.displayDuration or 3.5)
        f.fadeOut:Stop()
        f.fadeOut:Play()

        local stack = LibStub("LibMatic-AlertStack-1.0", true)
        if stack and fmt.useSharedPosition ~= false then
            stack:Push("AppearanceMatic", f, fmt.maxMessages or 4)
        end

        self:PlaySound(kind)

        if profile.generalSettings.chatNotifications then
            say(text)
        end
    end

    local function launch()
        if Guard then
            Guard:RunWhenClear(build, { maxHold = COLLECTED_HOLD })
        else
            build()
        end
    end
    local delay = tonumber(fmt.startDelay) or 0
    if delay > 0 and type(C_Timer) == "table" and type(C_Timer.After) == "function" then
        C_Timer.After(delay, launch)
    else
        launch()
    end
end

function Alert:RegisterSharedAlertStack()
    local stack = LibStub("LibMatic-AlertStack-1.0", true)
    if not stack then
        return
    end
    stack:RegisterAddon("AppearanceMatic", {
        isEnabled = function()
            local profile = db()
            return profile and profile.alertNotificationFormat.useSharedPosition ~= false
        end,
        getSaved = function()
            local profile = db()
            return profile and profile.alertNotificationFormat.shared
        end,
        setSaved = function(x, y, rev, maxAlerts)
            local profile = db()
            if not profile then
                return
            end
            profile.alertNotificationFormat.shared = profile.alertNotificationFormat.shared or {}
            local s = profile.alertNotificationFormat.shared
            s.x, s.y, s.rev, s.maxAlerts = x, y, rev, maxAlerts
        end,
        demo = function()
            Alert:Preview("appearance")
        end,
    })
end

function Alert:Preview(kind)
    local profile = db()
    if not profile then
        return
    end
    kind = kind or "appearance"
    local formatString = profile.alertNotificationFormat.formats[kind] or "%s"
    local sampleName = kind == "mount" and L["alert_preview_sample_mount"] or L["alert_preview_sample_appearance"]
    local text = string.format(formatString, sampleName)
    if profile.alertNotificationFormat.isUpper then
        text = text:upper()
    end
    local icon = kind == "mount" and "Interface\\Icons\\Ability_Mount_RidingHorse" or "Interface\\Icons\\INV_Chest_Cloth_17"
    self:ShowAlert(kind, text, icon, { bossDrop = kind == "appearance" })
end

local function FireFor(kind, itemName, opts)
    local profile = db()
    if not profile then
        return
    end
    local formatString = profile.alertNotificationFormat.formats[kind] or "%s"
    local text = string.format(formatString, itemName or "")
    if profile.alertNotificationFormat.isUpper then
        text = text:upper()
    end
    local icon = kind == "mount" and "Interface\\Icons\\Ability_Mount_RidingHorse" or "Interface\\Icons\\INV_Chest_Cloth_17"
    if opts and opts.icon then
        icon = opts.icon
    end
    Alert:ShowAlert(kind, text, icon, opts)
end

local function OnSourceAdded(sourceID)
    local favorites = pluginShared.Favorites
    if not (favorites and C_TransmogCollection and C_TransmogCollection.GetSourceInfo) then
        return
    end
    local ok, info = reported(C_TransmogCollection.GetSourceInfo, sourceID)
    if not (ok and type(info) == "table" and info.visualID) then
        return
    end
    if not favorites:IsFavorite(info.visualID) then
        return
    end
    local name = info.itemID
    local itemIcon, itemLink
    if C_Item and C_Item.GetItemInfo and info.itemID then
        local itemName, link, _, _, _, _, _, _, _, itemTexture = C_Item.GetItemInfo(info.itemID)
        name = itemName or ("Item:" .. tostring(info.itemID))
        itemIcon, itemLink = itemTexture, link
    end
    local bossSource = Enum and Enum.TransmogSource and Enum.TransmogSource.JournalEncounter or 1
    FireFor("appearance", name or "", { bossDrop = info.sourceType == bossSource, icon = itemIcon or info.icon })
    if pluginShared.Social then
        pluginShared.Social:Announce("appearance", itemLink or name)
    end
    local lockouts, tracking = pluginShared.Lockouts, pluginShared.Tracking
    if lockouts and lockouts.OnTrackedCollected then
        lockouts:OnTrackedCollected(tracking and tracking.APPEARANCE or 0, sourceID)
    end
end

local function OnMountAdded(mountID)
    local favorites = pluginShared.Favorites
    if not (favorites and C_MountJournal) then
        return
    end
    -- a mount just learned drops off the starred list, so it is walked out of the journal again
    if favorites.InvalidateMountScan then
        favorites:InvalidateMountScan()
    end
    if not favorites:MountIsFavorite(mountID) then
        return
    end
    local name, mountIcon, mountLink
    if C_MountJournal.GetMountInfoByID then
        local mountName, spellID, iconFile = C_MountJournal.GetMountInfoByID(mountID)
        name, mountIcon = mountName, iconFile
        if spellID and C_MountJournal.GetMountLink then
            mountLink = C_MountJournal.GetMountLink(spellID)
        end
    end
    FireFor("mount", name or "", { icon = mountIcon })
    if pluginShared.Social then
        pluginShared.Social:Announce("mount", mountLink or name)
    end
    local lockouts, tracking = pluginShared.Lockouts, pluginShared.Tracking
    if lockouts and lockouts.OnTrackedCollected then
        lockouts:OnTrackedCollected(tracking and tracking.MOUNT or 1, mountID)
    end
end

local watcher = CreateFrame("Frame")
watcher:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_ADDED")
watcher:RegisterEvent("NEW_MOUNT_ADDED")
watcher:SetScript("OnEvent", function(_, event, arg1)
    if event == "TRANSMOG_COLLECTION_SOURCE_ADDED" then
        OnSourceAdded(arg1)
    elseif event == "NEW_MOUNT_ADDED" then
        OnMountAdded(arg1)
    end
end)
