-- NP_CastbarTest/castbar.lua
local ADDON, ns = ...

ns.Castbar = ns.Castbar or {}
local C = ns.Castbar

-- =========================================================
--  DATA (fonts/skins)
-- =========================================================
local FONTS = {
    { name = "Friz Quadrata", path = "Fonts\\FRIZQT__.TTF" },
    { name = "Arial Narrow",  path = "Fonts\\ARIALN.TTF" },
    { name = "Morpheus",      path = "Fonts\\MORPHEUS.TTF" },
    { name = "Skurri",        path = "Fonts\\SKURRI.TTF" },
    { name = "2002",          path = "Fonts\\2002.TTF" },
}
C.FONTS = FONTS

-- 6 skins (you asked for 5+)
local SKINS = {
    { name = "Plater Flat (WHITE8x8)", texture = "Interface\\Buttons\\WHITE8x8" },
    { name = "Blizzard",               texture = "Interface\\TargetingFrame\\UI-StatusBar" },
    { name = "Raid HP Fill",           texture = "Interface\\RaidFrame\\Raid-Bar-Hp-Fill" },
    { name = "Minimalist",             texture = "Interface\\AddOns\\SharedMedia\\statusbar\\Minimalist" },
    { name = "BantoBar",               texture = "Interface\\AddOns\\SharedMedia\\statusbar\\BantoBar" },
    { name = "Smooth",                 texture = "Interface\\AddOns\\SharedMedia\\statusbar\\Smooth" },
}
C.SKINS = SKINS

-- =========================================================
--  UTIL
-- =========================================================
local function RGBA(t)
    if not t then return 1,1,1,1 end
    return t.r or 1, t.g or 1, t.b or 1, t.a or 1
end

local function SafeSetFont(fs, fontPath, size, outline)
    if not fs then return end
    fs:SetFontObject("GameFontNormalSmall")
    pcall(fs.SetFont, fs, fontPath or "Fonts\\FRIZQT__.TTF", size or 10, outline or "OUTLINE")
end

local function GetSkinTexture()
    local db = ns.DB
    local idx = 1
    if db and db.skins and db.skins.index then idx = db.skins.index end
    local s = SKINS[idx] or SKINS[1]
    return (s and s.texture) or "Interface\\TargetingFrame\\UI-StatusBar"
end

local function GetFont()
    local db = ns.DB
    local fidx = 1
    local size = 10
    local outline = "OUTLINE"
    if db and db.fonts then
        if db.fonts.index then fidx = db.fonts.index end
        if db.fonts.size then size = db.fonts.size end
        if db.fonts.outline then outline = db.fonts.outline end
    end
    local f = FONTS[fidx] or FONTS[1]
    return f.path, size, outline
end

local function GetBlizzCastBar(plate)
    local uf = plate and plate.UnitFrame
    if not uf then return nil end
    return uf.castBar or uf.CastBar or uf.castbar
end

-- =========================================================
--  POSITION / PREVIEW
-- =========================================================
local function GetPosDB()
    local db = ns.DB
    if not db then return nil end
    db.position = db.position or {}
    local p = db.position
    if p.attachToBlizz == nil then p.attachToBlizz = true end
    p.point = p.point or "CENTER"
    p.relPoint = p.relPoint or "CENTER"
    p.x = tonumber(p.x) or 0
    p.y = tonumber(p.y) or 0
    if p.unlocked == nil then p.unlocked = false end
    if p.alwaysShow == nil then p.alwaysShow = false end
    return p
end

local function IsTargetPlate(plate)
    if not (plate and plate.unitToken) then return false end
    if not UnitExists("target") then return false end
    local ok = pcall(UnitIsUnit, "target", plate.unitToken)
    return ok and UnitIsUnit("target", plate.unitToken) or false
end

local function ShouldPreview(plate)
    local db = ns.DB
    if not (db and db.position and db.position.alwaysShow) then return false end
    -- Preview only on your current target's nameplate.
    -- This avoids turning every single nameplate into a permanent castbar.
    return IsTargetPlate(plate)
end

local function LockHide(obj)
    if not obj or obj._npctLocked then return end
    obj._npctLocked = true

    pcall(obj.Hide, obj)
    pcall(obj.SetAlpha, obj, 0)

    if obj.SetShown then
        hooksecurefunc(obj, "SetShown", function(self, shown)
            if shown then
                pcall(self.Hide, self)
                pcall(self.SetAlpha, self, 0)
            end
        end)
    end
    if obj.Show then
        hooksecurefunc(obj, "Show", function(self)
            pcall(self.Hide, self)
            pcall(self.SetAlpha, self, 0)
        end)
    end
end

-- =========================================================
--  12.0 secret-token safe helpers
-- =========================================================
local function SafeAlphaFromToken(frame, token, aTrue, aFalse)
    if not frame then return end
    if aTrue == nil then aTrue = 1 end
    if aFalse == nil then aFalse = 0 end

    if frame.SetAlphaFromBoolean then
        local ok = pcall(frame.SetAlphaFromBoolean, frame, token, aTrue, aFalse)
        if ok then return end
    end

    frame:SetAlpha(aFalse)
end

local function SafeSetText(fs, value)
    if not fs then return end
    local ok = pcall(fs.SetText, fs, value)
    if not ok then
        pcall(fs.SetText, fs, "")
    end
end

local function SafeSetTexture(tex, value)
    if not tex then return end
    local ok = pcall(tex.SetTexture, tex, value)
    if not ok then
        pcall(tex.SetTexture, tex, nil)
    end
end

-- =========================================================
--  MANUAL BORDER
-- =========================================================
local function CreateSolidBorder(parentFrame, thickness)
    thickness = thickness or 1
    local b = CreateFrame("Frame", nil, parentFrame)
    b:SetAllPoints(parentFrame)

    b.t = b:CreateTexture(nil, "BORDER")
    b.t:SetColorTexture(1,1,1,1)
    b.t:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
    b.t:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
    b.t:SetHeight(thickness)

    b.bt = b:CreateTexture(nil, "BORDER")
    b.bt:SetColorTexture(1,1,1,1)
    b.bt:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
    b.bt:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
    b.bt:SetHeight(thickness)

    b.l = b:CreateTexture(nil, "BORDER")
    b.l:SetColorTexture(1,1,1,1)
    b.l:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
    b.l:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
    b.l:SetWidth(thickness)

    b.r = b:CreateTexture(nil, "BORDER")
    b.r:SetColorTexture(1,1,1,1)
    b.r:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
    b.r:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
    b.r:SetWidth(thickness)

    function b:SetColor(r,g,bb,a)
        r,g,bb,a = r or 1, g or 1, bb or 1, a or 1
        self.t:SetVertexColor(r,g,bb,a)
        self.bt:SetVertexColor(r,g,bb,a)
        self.l:SetVertexColor(r,g,bb,a)
        self.r:SetVertexColor(r,g,bb,a)
    end

    return b
end

-- =========================================================
--  CREATE BAR
-- =========================================================
local function MakeStatusBar(parent)
    local s = CreateFrame("StatusBar", nil, parent)
    s:SetAllPoints(parent)
    s:SetStatusBarTexture(GetSkinTexture())
    s:SetMinMaxValues(0, 1)
    s:SetValue(0)
    return s
end

function C:CreateBar(plate)
    local f = CreateFrame("Frame", nil, plate)
    f:SetFrameStrata("HIGH")
    f:Hide()

    f.bg = f:CreateTexture(nil, "BACKGROUND")
    f.bg:SetAllPoints()

    f.border = CreateSolidBorder(f, 1)

    f.iconFrame = CreateFrame("Frame", nil, f)
    f.iconFrame:Hide()

    f.icon = f.iconFrame:CreateTexture(nil, "ARTWORK")
    f.icon:SetAllPoints()
    f.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    f.iconBorder = CreateSolidBorder(f.iconFrame, 1)

    f.barArea = CreateFrame("Frame", nil, f)
    f.barArea:SetAllPoints()

    f.base      = MakeStatusBar(f.barArea)
    f.important = MakeStatusBar(f.barArea)
    f.noKick    = MakeStatusBar(f.barArea)
    f.interrupt = MakeStatusBar(f.barArea)

    f.important:SetAlpha(0)
    f.noKick:SetAlpha(0)
    f.interrupt:SetAlpha(0)

    f.shieldTint = f.barArea:CreateTexture(nil, "OVERLAY")
    f.shieldTint:SetAllPoints()
    f.shieldTint:SetTexture("Interface\\Buttons\\WHITE8x8")
    f.shieldTint:SetBlendMode("ADD")
    f.shieldTint:SetAlpha(0)

    f.textFrame = CreateFrame("Frame", nil, f.barArea)
    f.textFrame:SetAllPoints()
    f.textFrame:SetFrameStrata("TOOLTIP")
    f.textFrame:SetFrameLevel((f.interrupt:GetFrameLevel() or 1) + 100)

    f.text = f.textFrame:CreateFontString(nil, "OVERLAY")
    f.text:SetDrawLayer("OVERLAY", 7)
    f.text:SetPoint("LEFT", 4, 0)
    f.text:SetPoint("RIGHT", -4, 0)
    f.text:SetJustifyH("LEFT")
    f.text:SetMaxLines(1)
    f.text:SetWordWrap(false)
    f.text:SetNonSpaceWrap(false)

    return f
end

function C:ApplyLayout(cb)
    local db = ns.DB
    if not (cb and db) then return end

    cb:SetScale(db.scale or 1.0)
    cb:SetSize(db.width or 140, db.height or 12)

    cb.barArea:ClearAllPoints()
    cb.barArea:SetAllPoints(cb)

    local showIcon = (db.showIcon == true)
    local h = db.height or 12
    local iconSize = h + 2

    if showIcon then
        cb.iconFrame:Show()
        cb.iconFrame:SetSize(iconSize, iconSize)
        cb.iconFrame:ClearAllPoints()
        cb.iconFrame:SetPoint("LEFT", cb, "LEFT", -iconSize - 2, 0)
    else
        cb.iconFrame:Hide()
    end

    if db.showBorder then
        cb.border:Show()
        if showIcon then cb.iconBorder:Show() else cb.iconBorder:Hide() end
    else
        cb.border:Hide()
        cb.iconBorder:Hide()
    end
end

function C:ApplyStyle(cb)
    local db = ns.DB
    if not (cb and db) then return end

    self:ApplyLayout(cb)

    local tex = GetSkinTexture()
    local c = db.colors

    cb.bg:SetColorTexture(RGBA(c.bg))

    local br,bg,bb,ba = RGBA(c.border)
    cb.border:SetColor(br,bg,bb,ba)
    cb.iconBorder:SetColor(br,bg,bb,ba)

    cb.base:SetStatusBarTexture(tex)
    cb.important:SetStatusBarTexture(tex)
    cb.noKick:SetStatusBarTexture(tex)
    cb.interrupt:SetStatusBarTexture(tex)

    cb.base:SetStatusBarColor(RGBA(c.normal))
    cb.important:SetStatusBarColor(RGBA(c.important))
    cb.noKick:SetStatusBarColor(RGBA(c.noKick))
    cb.interrupt:SetStatusBarColor(RGBA(c.interrupted))

    cb.shieldTint:SetVertexColor(RGBA(c.shield))

    local fp, fs, fo = GetFont()
    SafeSetFont(cb.text, fp, fs, fo)
    cb.text:SetTextColor(RGBA(c.text))
end

function C:RefreshAllPlates()
    if not (C_NamePlate and C_NamePlate.GetNamePlates) then return end
    for _, plate in pairs(C_NamePlate.GetNamePlates()) do
        if plate and plate.NPCT_Bar then
            self:ApplyStyle(plate.NPCT_Bar)
        end
    end
end

function C:Attach(plate, cb)
    local blizz = GetBlizzCastBar(plate)
    local pos = GetPosDB()

    local function ApplyPoint(rel)
        cb:ClearAllPoints()
        if pos then
            cb:SetPoint(pos.point or "CENTER", rel, pos.relPoint or pos.point or "CENTER", pos.x or 0, pos.y or 0)
        else
            cb:SetPoint("CENTER", rel, "CENTER", 0, 0)
        end
    end

    if blizz then
        if ns.Interrupt and ns.Interrupt.HookBlizzCastbar then
            ns.Interrupt:HookBlizzCastbar(blizz, plate)
        end

        LockHide(blizz)
        if blizz.BorderShield then LockHide(blizz.BorderShield) end
        if blizz.Border then LockHide(blizz.Border) end
        if blizz.Flash then LockHide(blizz.Flash) end
        if blizz.Icon then LockHide(blizz.Icon) end
        if blizz.Text then LockHide(blizz.Text) end
        if blizz.Background then LockHide(blizz.Background) end

        -- Anchor relative to Blizzard castbar by default, but allow switching.
        if pos and pos.attachToBlizz == false then
            ApplyPoint(plate)
            return blizz
        end

        ApplyPoint(blizz)
        return blizz
    end

    -- No Blizzard castbar found (rare). Anchor to plate.
    if pos and pos.point then
        ApplyPoint(plate)
    else
        cb:ClearAllPoints()
        cb:SetPoint("TOP", plate, "TOP", 0, -22)
    end
    return nil
end

-- =========================================================
--  DRAG-TO-MOVE (target preview only)
-- =========================================================
local function EnableDrag(cb, plate, anchorTo)
    if not cb then return end

    local pos = GetPosDB()
    local unlocked = pos and pos.unlocked
    local canPreview = plate and IsTargetPlate(plate)

    -- Only allow dragging when:
    --  1) unlocked in settings
    --  2) you have a target plate (so you're not dragging 30 plates)
    --  3) not in combat (avoid weirdness)
    if not (unlocked and canPreview) or InCombatLockdown() then
        if cb._npctDragEnabled then
            cb._npctDragEnabled = false
            cb:EnableMouse(false)
            cb:RegisterForDrag()
        end
        return
    end

    if cb._npctDragEnabled then
        -- still update cursor behavior if needed
        cb:EnableMouse(true)
        return
    end

    cb._npctDragEnabled = true
    cb:SetMovable(true)
    cb:EnableMouse(true)
    cb:RegisterForDrag("LeftButton")

    cb:SetScript("OnDragStart", function(self)
        if InCombatLockdown() then return end
        self:StartMoving()
    end)

    cb:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        if not anchorTo then return end

        local point, relTo, relPoint, x, y = self:GetPoint(1)
        point = point or "CENTER"
        relPoint = relPoint or point
        x = tonumber(x) or 0
        y = tonumber(y) or 0

        local p = GetPosDB()
        if p then
            p.point = point
            p.relPoint = relPoint
            p.x = x
            p.y = y

            -- Persist what we're anchoring to when possible.
            -- If the current anchor is the Blizzard castbar, keep attachToBlizz true.
            if relTo and plate then
                local blizz = GetBlizzCastBar(plate)
                if blizz and relTo == blizz then
                    p.attachToBlizz = true
                elseif relTo == plate then
                    p.attachToBlizz = false
                end
            end
        end

        -- Re-apply our saved point cleanly (prevents drifting)
        self:ClearAllPoints()
        self:SetPoint(point, anchorTo, relPoint, x, y)

        if ns.Castbar and ns.Castbar.RefreshAllPlates then
            ns.Castbar:RefreshAllPlates()
        end
    end)
end

local function TrySetDurationAll(cb, dur)
    if not (cb and cb.base and cb.base.SetTimerDuration) then return false end
    if dur == nil then return false end

    local ok = pcall(cb.base.SetTimerDuration, cb.base, dur)
    if not ok then return false end

    pcall(cb.important.SetTimerDuration, cb.important, dur)
    pcall(cb.noKick.SetTimerDuration, cb.noKick, dur)
    pcall(cb.interrupt.SetTimerDuration, cb.interrupt, dur)
    return true
end

function C:UpdatePlate(plate)
    local db = ns.DB
    if not (db and db.enabled) then return end
    if not (plate and plate.unitToken) then return end

    local unit = plate.unitToken

    if not plate.NPCT_Bar then
        plate.NPCT_Bar = self:CreateBar(plate)
        self:ApplyStyle(plate.NPCT_Bar)
    end
    local cb = plate.NPCT_Bar

    local blizz = self:Attach(plate, cb)

    -- Enable drag only for target preview when unlocked
    -- Pick the same anchor we used in Attach.
    do
        local pos = GetPosDB()
        local anchor
        if blizz and (not pos or pos.attachToBlizz ~= false) then
            anchor = blizz
        else
            anchor = plate
        end
        EnableDrag(cb, plate, anchor)
    end

    if ns.Interrupt and ns.Interrupt.IsActive and ns.Interrupt:IsActive(plate) then
        SafeSetText(cb.text, "Interrupted")

        cb.base:SetMinMaxValues(0, 1); cb.base:SetValue(1)
        cb.important:SetMinMaxValues(0, 1); cb.important:SetValue(1)
        cb.noKick:SetMinMaxValues(0, 1); cb.noKick:SetValue(1)
        cb.interrupt:SetMinMaxValues(0, 1); cb.interrupt:SetValue(1)

        cb.important:SetAlpha(0)
        cb.noKick:SetAlpha(0)
        cb.shieldTint:SetAlpha(0)
        cb.interrupt:SetAlpha(1)

        cb:SetAlpha(1)
        cb:Show()
        return
    else
        if ns.Interrupt and ns.Interrupt.Clear then ns.Interrupt:Clear(plate) end
        cb.interrupt:SetAlpha(0)
    end

    local castDur = nil
    if UnitCastingDuration then
        castDur = UnitCastingDuration(unit)
    end
    local okCast = TrySetDurationAll(cb, castDur)

    local okChan = false
    if not okCast then
        local chanDur = nil
        if UnitChannelDuration then
            chanDur = UnitChannelDuration(unit)
        end
        okChan = TrySetDurationAll(cb, chanDur)
    end

    if not (okCast or okChan) then
        if ShouldPreview(plate) then
            -- Preview mode (for positioning): show a fake, stable bar
            SafeSetText(cb.text, "Adjust Castbar")

            cb.base:SetMinMaxValues(0, 1)
            cb.base:SetValue(0.65)
            cb.important:SetMinMaxValues(0, 1)
            cb.important:SetValue(0.65)
            cb.noKick:SetMinMaxValues(0, 1)
            cb.noKick:SetValue(0.65)

            cb.important:SetAlpha(0)
            cb.noKick:SetAlpha(0)
            cb.shieldTint:SetAlpha(0)
            cb.interrupt:SetAlpha(0)

            if db.showIcon and cb.icon then
                -- Generic icon (won't error if missing)
                SafeSetTexture(cb.icon, 136243) -- spell_nature_lightning
            end

            cb:SetAlpha(1)
            cb:Show()
        else
            SafeSetText(cb.text, "")
            cb.important:SetAlpha(0)
            cb.noKick:SetAlpha(0)
            cb.shieldTint:SetAlpha(0)
            cb:SetAlpha(0)
            cb:Show()
        end
        return
    end

    cb:SetAlpha(1)
    cb:Show()

    local name, texture, notInterruptibleToken = nil, nil, nil
    if okCast then
        name, _, texture, _, _, _, _, notInterruptibleToken = UnitCastingInfo(unit)
    else
        name, _, texture, _, _, _, notInterruptibleToken = UnitChannelInfo(unit)
    end

    SafeSetText(cb.text, name)

    if db.showIcon and cb.icon then
        SafeSetTexture(cb.icon, texture)
    end

    local importantToken = nil
    if blizz then
        importantToken = blizz.isHighlightedImportantCast
    end

    SafeAlphaFromToken(cb.important, importantToken, 1, 0)
    SafeAlphaFromToken(cb.noKick, notInterruptibleToken, 1, 0)

    local shieldA = 0.18
    if db.colors and db.colors.shield and db.colors.shield.a then
        shieldA = db.colors.shield.a
    end
    SafeAlphaFromToken(cb.shieldTint, notInterruptibleToken, shieldA, 0)
end
