-- [ ORBIT UNIT FRAME MIXIN ]-------------------------------------------------------------------------
-- Shared functionality for unit frame plugins (Player, Target, Focus, Pet)

local _, addonTable = ...
local Orbit = addonTable
local OrbitEngine = Orbit.Engine
local LSM = LibStub("LibSharedMedia-3.0")

---@class OrbitUnitFrameMixin
Orbit.UnitFrameMixin = {}
local Mixin = Orbit.UnitFrameMixin

-- [ SETTINGS INHERITANCE ] --------------------------------------------------------------------------
local INHERIT_PLUGIN = Orbit.Constants.UnitFrame.InheritPlugin
local INHERIT_INDEX = Orbit.Constants.UnitFrame.InheritIndex

local SYNC_PLUGINS = {
    [Enum.EditModeUnitFrameSystemIndices.Player] = "Orbit_PlayerFrame",
    [Enum.EditModeUnitFrameSystemIndices.Target] = "Orbit_TargetFrame",
    [Enum.EditModeUnitFrameSystemIndices.Focus or 3] = "Orbit_FocusFrame",
}

function Mixin:GetPlayerSetting(key)
    return Orbit:ReadPluginSetting(self.inheritPlugin or INHERIT_PLUGIN, self.inheritIndex or INHERIT_INDEX, key)
end

function Mixin:GetInheritedSetting(systemIndex, key, inheritFromPlayer)
    if inheritFromPlayer then
        return self:GetPlayerSetting(key)
    end
    return self:GetSetting(systemIndex, key)
end

function Mixin:GetSyncSource(systemIndex)
    return self:GetSetting(systemIndex, "SyncSize") or systemIndex
end

function Mixin:GetSyncedSize(systemIndex, visited)
    visited = visited or {}
    local source = visited[systemIndex] and systemIndex or self:GetSyncSource(systemIndex)
    visited[systemIndex] = true
    if source == systemIndex then
        return self:GetSetting(systemIndex, "Width"), self:GetSetting(systemIndex, "Height")
    end
    local sourcePlugin = Orbit:GetPlugin(SYNC_PLUGINS[source])
    return sourcePlugin:GetSyncedSize(source, visited)
end

-- [ NATIVE FRAME HIDING ] ---------------------------------------------------------------------------
function Mixin:HideNativeUnitFrame(nativeFrame)
    if nativeFrame then OrbitEngine.NativeFrame:Park(nativeFrame) end
end

-- [ BACKGROUND CREATION ] ---------------------------------------------------------------------------
function Mixin:CreateBackground(frame)
    if not frame then
        return
    end
    if frame.bg then
        return frame.bg
    end
    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()
    local colors = Orbit.Constants.Colors.Background
    frame.bg:SetColorTexture(colors.r, colors.g, colors.b, colors.a)
    return frame.bg
end

-- [ TEXTURE APPLICATION ] ---------------------------------------------------------------------------
function Mixin:ApplyTexture(frame, textureName)
    if not frame or not frame.Health then
        return
    end
    Orbit.Skin:SkinStatusBar(frame.Health, textureName, nil, true)
    if frame.TotalAbsorbBar then
        local absorbTextureName = Orbit.db.GlobalSettings and Orbit.db.GlobalSettings.AbsorbTexture
        frame.TotalAbsorbBar:SetStatusBarTexture(LSM:Fetch("statusbar", absorbTextureName or "Blizzard"))
    end
    self:CreateBackground(frame)
end

-- [ TEXT STYLING ] ----------------------------------------------------------------------------------
local function ReapplyTextOverride(self, frame, key, textSize, fontPath)
    if not frame[key] then return end
    local systemIndex = frame.systemIndex or 1
    local positions = self.GetComponentPositions and self:GetComponentPositions(systemIndex)
        or (self.GetSetting and self:GetSetting(systemIndex, "ComponentPositions"))
    local overrides = positions and positions[key] and positions[key].overrides
    if overrides then
        OrbitEngine.OverrideUtils.ApplyFontOverrides(frame[key], overrides, textSize, fontPath)
        OrbitEngine.OverrideUtils.ApplyTextColor(frame[key], overrides, nil, frame.unit)
    end
end

function Mixin:ApplyTextStyling(frame, textSize)
    if not frame then
        return
    end
    if not textSize or textSize <= 0 then
        local height = frame:GetHeight() or 40
        textSize = 14
    end
    Orbit.Skin:ApplyUnitFrameText(frame.Name, "LEFT", nil, textSize)
    Orbit.Skin:ApplyUnitFrameText(frame.HealthText, "RIGHT", nil, textSize)
    if frame.HealthText then
        frame.HealthText:SetDrawLayer("OVERLAY", 7)
        frame.HealthText:SetAlpha(1)
        frame.HealthText:SetTextColor(1, 1, 1, 1)
    end
    local globalFontName = Orbit.db.GlobalSettings.Font
    local fontPath = LSM:Fetch("font", globalFontName)
    ReapplyTextOverride(self, frame, "Name", textSize, fontPath)
    ReapplyTextOverride(self, frame, "HealthText", textSize, fontPath)
end

-- Updates only font size without repositioning (preserves Canvas Mode custom positions)
function Mixin:UpdateTextSize(frame, textSize)
    if not frame then return end
    if not textSize or textSize <= 0 then
        local height = frame:GetHeight() or 40
        textSize = 14
    end
    local globalFontName = Orbit.db.GlobalSettings.Font
    local fontPath = LSM:Fetch("font", globalFontName) or "Fonts\\FRIZQT__.TTF"
    local outline = Orbit.Skin:GetFontOutline()
    if frame.Name then
        frame.Name:SetFont(fontPath, textSize, outline)
        if frame._fullName then frame.Name:SetText(frame._fullName) end
        if frame.GetNameAvailableWidth then
            local available = frame:GetNameAvailableWidth()
            if available then frame.Name:SetWidth(math.max(available, 20)) end
        end
    end
    if frame.HealthText then frame.HealthText:SetFont(fontPath, textSize, outline) end
    ReapplyTextOverride(self, frame, "Name", textSize, fontPath)
    ReapplyTextOverride(self, frame, "HealthText", textSize, fontPath)
end

-- [ PREVIEW COLOR HELPERS ] -------------------------------------------------------------------------
function Mixin:GetPreviewHealthColor(isPlayer, className, reaction)
    local globalSettings = Orbit.db.GlobalSettings or {}
    local barCurve = globalSettings.BarColorCurve
    
    -- Check if curve has class pins
    if barCurve and barCurve.pins and #barCurve.pins > 0 then
        local hasClassPin = OrbitEngine.ColorCurve:CurveHasClassPin(barCurve)
        
        if hasClassPin then
            -- Resolve class pin based on preview context
            local resolvedColor
            if isPlayer and className then
                local classColor = RAID_CLASS_COLORS[className]
                if classColor then resolvedColor = { r = classColor.r, g = classColor.g, b = classColor.b } end
            elseif reaction then
                resolvedColor = OrbitEngine.ReactionColor:GetReactionColor(reaction)
            end
            if resolvedColor then return resolvedColor.r, resolvedColor.g, resolvedColor.b end
        end
    end
    
    -- No class pins or fallback - use first color from curve
    local barColor = (OrbitEngine.ColorCurve:GetFirstColorFromCurve(barCurve)) or { r = 0.2, g = 0.8, b = 0.2 }
    return barColor.r, barColor.g, barColor.b
end

function Mixin:GetPreviewTextColor(isPlayer, className, reaction)
    local globalSettings = Orbit.db.GlobalSettings or {}
    local fontCurve = globalSettings.FontColorCurve

    if fontCurve and fontCurve.pins and #fontCurve.pins > 0 then
        local hasClassPin = OrbitEngine.ColorCurve:CurveHasClassPin(fontCurve)
        if hasClassPin then
            local resolvedColor
            if isPlayer and className then
                local classColor = RAID_CLASS_COLORS[className]
                if classColor then resolvedColor = { r = classColor.r, g = classColor.g, b = classColor.b, a = 1 } end
            elseif reaction then
                resolvedColor = OrbitEngine.ReactionColor:GetReactionColor(reaction)
            end
            if resolvedColor then return resolvedColor.r, resolvedColor.g, resolvedColor.b, resolvedColor.a or 1 end
        end
    end

    local fontColor = (OrbitEngine.ColorCurve:GetFirstColorFromCurve(fontCurve)) or { r = 1, g = 1, b = 1, a = 1 }
    return fontColor.r, fontColor.g, fontColor.b, fontColor.a or 1
end

function Mixin:ApplyPreviewBackdrop(frame)
    if not frame then return end
    self:CreateBackground(frame)
    local globalSettings = Orbit.db.GlobalSettings or {}
    Orbit.Skin:ApplyGradientBackground(frame, globalSettings.UnitFrameBackdropColourCurve, { r = 0.08, g = 0.08, b = 0.08, a = 0.5 })
end

function Mixin:UpdateBackdropColor(frame, systemIndex, inheritFromPlayer)
    if not frame then return end
    self:CreateBackground(frame)
    local globalSettings = Orbit.db.GlobalSettings or {}
    Orbit.Skin:ApplyGradientBackground(frame, globalSettings.UnitFrameBackdropColourCurve, Orbit.Constants.Colors.Background)
end

-- [ BASE VISUALS APPLICATION ] ----------------------------------------------------------------------
function Mixin:ApplyBaseVisuals(frame, systemIndex, options)
    if not frame then
        return
    end
    options = options or {}

    local borderSize = Orbit.db.GlobalSettings.BorderSize
    local textureName = self:GetSetting(systemIndex, "Texture")
    local healthTextMode = self:GetInheritedSetting(systemIndex, "HealthTextMode", options.inheritFromPlayer) or "percent_short"

    -- Apply texture
    self:ApplyTexture(frame, textureName)

    -- Apply Backrop Colour
    self:UpdateBackdropColor(frame, systemIndex, options.inheritFromPlayer)

    -- Apply border
    if frame.SetBorder then
        frame:SetBorder(borderSize)
    end

    -- Apply text styling
    self:ApplyTextStyling(frame)

    -- Apply health text mode (replaces simple enabled boolean)
    if frame.SetHealthTextMode then
        frame:SetHealthTextMode(healthTextMode)
    end

    -- Apply health value visibility
    local showHealthValue = self:GetInheritedSetting(systemIndex, "ShowHealthValue", options.inheritFromPlayer)
    if showHealthValue == nil then showHealthValue = true end
    if frame.SetHealthTextEnabled then frame:SetHealthTextEnabled(showHealthValue) end

    -- Apply absorbs (if available)
    if frame.SetAbsorbsEnabled then
        frame:SetAbsorbsEnabled(true)
        frame:SetHealAbsorbsEnabled(true)
    end
end

-- [ FRAME LAYOUT ] ----------------------------------------------------------------------------------
local DEFAULT_POWER_BAR_RATIO = 0.2


function Mixin:UpdateFrameLayout(frame, borderSize, options)
    if not frame then
        return
    end
    local height = frame:GetHeight()
    if height < 1 then
        return
    end
    options = options or {}
    local showPowerBar = (options.showPowerBar == nil) and true or options.showPowerBar

    local Pixel = OrbitEngine.Pixel
    local scale = frame:GetEffectiveScale()
    local powerBarRatio = options.powerBarRatio or DEFAULT_POWER_BAR_RATIO
    local powerHeight = showPowerBar and Pixel:Snap(height * powerBarRatio, scale) or 0

    if frame.Power then
        if showPowerBar then
            frame.Power:ClearAllPoints()
            frame.Power:SetPoint("BOTTOMLEFT", 0, 0)
            frame.Power:SetPoint("BOTTOMRIGHT", 0, 0)
            frame.Power:SetHeight(powerHeight)
            frame.Power:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.StatusBar)
            frame.Power:Show()
        else
            frame.Power:Hide()
        end
    end

    if frame.Health then
        frame.Health:ClearAllPoints()
        frame.Health:SetPoint("TOPLEFT", 0, 0)
        if showPowerBar then
            frame.Health:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, powerHeight)
        else
            frame.Health:SetPoint("BOTTOMRIGHT", 0, 0)
        end
        frame.Health:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.StatusBar)
        if frame.HealthDamageBar then
            frame.HealthDamageBar:ClearAllPoints()
            frame.HealthDamageBar:SetPoint("TOPLEFT", frame.Health, "TOPLEFT", 0, 0)
            frame.HealthDamageBar:SetPoint("BOTTOMRIGHT", frame.Health, "BOTTOMRIGHT", 0, 0)
            frame.HealthDamageBar:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.StatusBar)
        end
    end
end

-- [ COMBAT-SAFE SIZE APPLICATION ] ------------------------------------------------------------------
function Mixin:ApplySize(frame, width, height)
    if not frame then
        return
    end
    Orbit:SafeAction(function()
        local anchor = Orbit.Engine.FrameAnchor and Orbit.Engine.FrameAnchor.anchors[frame]
        local isHorizontalAnchor = anchor and (anchor.edge == "LEFT" or anchor.edge == "RIGHT")
        local opts = isHorizontalAnchor and frame.anchorOptions
        if isHorizontalAnchor and not (opts and opts.independentHeight) then
            frame:SetWidth(width)
        else
            frame:SetSize(width, height)
        end
    end)
end

-- [ VISIBILITY CONTAINER ] --------------------------------------------------------------------------
local function PluginHasMountedHide(plugin)
    local VE = Orbit.VisibilityEngine
    if not VE then return false end
    return VE:HasMountedHideSetting(plugin.name, 1)
end

function Mixin:CreateVisibilityContainer(parent, combatEssential)
    if InCombatLockdown() then
        if Orbit.ErrorHandler then
            Orbit.ErrorHandler:LogError("UnitFrameMixin", "CreateVisibilityContainer", "called in combat lockdown")
        end
        return nil
    end
    local container = CreateFrame("Frame", nil, parent or UIParent, "SecureHandlerStateTemplate")
    container:SetAttribute("_onstate-visibility", [[ if newstate == "hide" then self:Hide() else self:Show() end ]])
    container:SetAllPoints()
    container:SetClampedToScreen(true)
    local baseDriver = "[petbattle] hide; show"
    local driver = PluginHasMountedHide(self) and (Orbit.MountedVisibility:GetMountedDriver(baseDriver, combatEssential) or baseDriver) or baseDriver
    RegisterStateDriver(container, "visibility", driver)
    container.orbitBaseDriver = baseDriver
    container.orbitCombatEssential = combatEssential
    return container
end

function Mixin:UpdateVisibilityDriver()
    if not self.container or not self.container.orbitBaseDriver or InCombatLockdown() then return end
    local base = self.container.orbitBaseDriver
    local skipMountedDriver = Orbit:IsEditMode() or (self.mountedConfig ~= nil) or not PluginHasMountedHide(self)
    local driver = (skipMountedDriver and base) or (Orbit.MountedVisibility:GetMountedDriver(base, self.container.orbitCombatEssential) or base)
    RegisterStateDriver(self.container, "visibility", driver)
end

-- [ STANDARD RESTORE POSITION ] ---------------------------------------------------------------------
function Mixin:RestoreFramePosition(frame, systemIndex)
    if not frame then
        return
    end
    OrbitEngine.Frame:RestorePosition(frame, self, systemIndex)
end

-- [ COMPLETE APPLY SETTINGS HELPER ] ----------------------------------------------------------------
function Mixin:ApplyUnitFrameSettings(frame, systemIndex, options)
    if not frame then
        return
    end
    options = options or {}

    -- Get size settings
    local width = options.width or self:GetSetting(systemIndex, "Width")
    local height = options.height or self:GetSetting(systemIndex, "Height")

    if options.inheritFromPlayer then
        width = width or self:GetPlayerSetting("Width")
        height = height or self:GetPlayerSetting("Height")
    end

    width = width or 200
    height = height or 40

    -- Apply size (combat-safe)
    self:ApplySize(frame, width, height)

    -- Apply visuals
    self:ApplyBaseVisuals(frame, systemIndex, options)

    -- Update frame if it has UpdateAll
    if frame.UpdateAll then
        frame:UpdateAll()
    end

    -- Restore position
    self:RestoreFramePosition(frame, systemIndex)
end

-- [ OVERLAY ICON BUILDER ] --------------------------------------------------------------------------
-- Creates OverlayFrame, LevelText, RareEliteIcon, MarkerIcon, Portrait + ComponentDrag registration
function Mixin:CreateOverlayIcons(frame, systemIndex)
    local Constants = Orbit.Constants
    local iconSize = Constants.UnitFrame.StatusIconSize

    if not frame.OverlayFrame then
        frame.OverlayFrame = CreateFrame("Frame", nil, frame)
        frame.OverlayFrame:SetAllPoints()
        frame.OverlayFrame:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.Overlay)
    end

    if not frame.LevelText then
        frame.LevelText = frame.OverlayFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        frame.LevelText:SetDrawLayer("OVERLAY", 7)
        frame.LevelText:SetPoint("TOPLEFT", frame, "TOPRIGHT", 4, 0)
    end

    if not frame.RareEliteIcon then
        frame.RareEliteIcon = frame.OverlayFrame:CreateTexture(nil, "OVERLAY", nil, 7)
        frame.RareEliteIcon:SetSize(iconSize, iconSize)
        frame.RareEliteIcon:SetPoint("BOTTOMLEFT", frame, "BOTTOMRIGHT", 2, 0)
        frame.RareEliteIcon:Hide()
    end

    if not frame.MarkerIcon then
        frame.MarkerIcon = frame.OverlayFrame:CreateTexture(nil, "OVERLAY", nil, 7)
        frame.MarkerIcon:SetSize(iconSize, iconSize)
        frame.MarkerIcon.orbitOriginalWidth = iconSize
        frame.MarkerIcon.orbitOriginalHeight = iconSize
        frame.MarkerIcon:SetPoint("TOP", frame, "TOP", 0, -2)
        frame.MarkerIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
        frame.MarkerIcon:Hide()
    end

    frame:CreatePortrait()

    local MPC = function(key) return OrbitEngine.ComponentDrag:MakePositionCallback(self, systemIndex, key) end
    OrbitEngine.ComponentDrag:Attach(frame.LevelText, frame, { key = "LevelText", onPositionChange = MPC("LevelText") })
    OrbitEngine.ComponentDrag:Attach(frame.RareEliteIcon, frame, { key = "RareEliteIcon", onPositionChange = MPC("RareEliteIcon") })
    OrbitEngine.ComponentDrag:Attach(frame.MarkerIcon, frame, { key = "MarkerIcon", onPositionChange = MPC("MarkerIcon") })
    OrbitEngine.ComponentDrag:Attach(frame.Portrait, frame, { key = "Portrait", onPositionChange = MPC("Portrait") })
end
