local _, ns = ...
local B, C, L, DB = unpack(ns)
-- Player Frame Hider: Blizzard player frame hider addon (Retail)
--## Author: Daniel Peters ## Version: 1.3.1 ## SavedVariables: PFH_DB
-- Shared namespace

local PFH = PlayerFrameHider or {}
-- =========================================================
-- Constants / State
-- =========================================================

local HURT_GRACE_SECONDS = 3

local function ClampHiddenAlpha(value)
  if type(value) ~= "number" then
    return 0
  elseif value < 0 then
    return 0
  elseif value > 1 then
    return 1
  end
  return value
end

PFH.state = PFH.state or {
  inCombat = false,

  -- grace window after health-related events
  hurtUntil = 0,
  hurtTicker = nil,

  -- init / hooks
  hooked = false,
  optionsCreated = false,
  optionsCategory = nil,
  optionsCategoryID = nil,
  didInit = false, -- guards double init between LOGIN / ENTERING_WORLD

  -- alpha-hidden tracking (combat-safe)
  playerFrameHidden = false,
  hoverOverride = false,

  -- hover hide delay
  hoverHideTimer = nil,

  -- widget frames
  EssentialCDFrame = nil,
  UtilityCDFrame = nil,
  TrackedBuffsFrame = nil,
}

local state = PFH.state

PFH.timers = PFH.timers or {}

-- =========================================================
-- Helpers
-- =========================================================

local function MarkHurt()
  state.hurtUntil = GetTime() + HURT_GRACE_SECONDS
end

local function IsRelevantInstance()
  local inInstance, instanceType = IsInInstance()
  if inInstance then
    if instanceType == "party"
      or instanceType == "raid"
      or instanceType == "pvp"
      or instanceType == "arena"
      or instanceType == "scenario"
      or instanceType == "delve" then
      return true
    end
  end

  if C_Scenario and C_Scenario.IsInScenario and C_Scenario.IsInScenario() then
    return true
  end

  return false
end

local function IsAlwaysShowInstance()
  --if not PFH_DB.alwaysShowInInstance then return false end
  return IsRelevantInstance()
end

local function IsInCombat()
  return state.inCombat or (UnitAffectingCombat("player") and true or false)
end

local function HasEnemyTarget()
  return UnitExists("target") and UnitCanAttack("player", "target")
end

local function ShouldShowPlayerFrame()
  if C.db["UFs"]["UFFade"] == false then
    return true -- addon disabled => do not hide anything
  end

  if IsAlwaysShowInstance() then return true end

  -- if not hiding at all, always show
  if not C.db["UFs"]["UFFade"] then return true end

  if IsInCombat() then return true end
  if UnitExists("target") then return true end
  if GetTime() < state.hurtUntil then return true end

  if state.hoverOverride and not IsInCombat() then
    return true
  end

  -- During a combat-hold window, keep the frame visible
  if state.combatHoldPlayer then
    return true
  end

  return false
end

local function ShouldShowWidgets()
  if C.db["UFs"]["UFFade"] == false then
    return true -- addon disabled => do not hide widgets
  end

  if IsAlwaysShowInstance() then return true end

  if state.combatHoldWidgets then
    return true
  end

  return IsInCombat() or HasEnemyTarget()
end

-- =========================================================
-- PlayerFrame visibility (combat-safe alpha)
-- =========================================================

local function SetPlayerFrameVisible(wantVisible)
  if not PlayerFrame then return end

  local inLockdown = InCombatLockdown()
  local hiddenAlpha = ClampHiddenAlpha(0)

  if wantVisible then
    state.playerFrameHidden = false
    if PlayerFrame:GetAlpha() ~= 1 then PlayerFrame:SetAlpha(1) end
    if not inLockdown then
      PlayerFrame:EnableMouse(true)
    end
  else
    state.playerFrameHidden = true
    if PlayerFrame:GetAlpha() ~= hiddenAlpha then PlayerFrame:SetAlpha(hiddenAlpha) end
    if not inLockdown then
        PlayerFrame:EnableMouse(true)  --PlayerFrame:EnableMouse(false)
    end
  end
end

-- =========================================================
-- Hover reveal (out of combat)
-- =========================================================

local function OnPlayerFrameEnter()
  if not C.db["UFs"]["UFFade"] then return end
  if IsInCombat() then return end
  if state.hoverHideTimer then
    state.hoverHideTimer:Cancel()
    state.hoverHideTimer = nil
  end
  state.hoverOverride = true
  Apply()
end

local function OnPlayerFrameLeave()
  if state.hoverHideTimer then
    state.hoverHideTimer:Cancel()
    state.hoverHideTimer = nil
  end

  state.hoverHideTimer = C_Timer.NewTimer(3, function()
    state.hoverHideTimer = nil
    if IsInCombat() then return end
    state.hoverOverride = false
    Apply()
  end)
end

-- =========================================================
-- Widget visibility
-- =========================================================

local function ApplyWidget(frame, enabled)
  if not enabled or not frame then return end

  -- Never try to show/hide protected frames during combat.
  if InCombatLockdown() then return end

  local want = ShouldShowWidgets()

  if want then
    if not frame:IsShown() then pcall(frame.Show, frame) end
  else
    if frame:IsShown() then pcall(frame.Hide, frame) end
  end
end

-- =========================================================
-- Core apply
-- =========================================================

function Apply()
  if not PlayerFrame then return end
  SetPlayerFrameVisible(ShouldShowPlayerFrame())
end

-- =========================================================
-- Hurt ticker
-- =========================================================

local function StopHurtTicker()
  if state.hurtTicker then
    state.hurtTicker:Cancel()
    state.hurtTicker = nil
  end
end

local function EnsureHurtTicker()
  if state.hurtTicker then return end

  state.hurtTicker = C_Timer.NewTicker(0.2, function()
    Apply()

    local expired = GetTime() >= state.hurtUntil
    if expired then
      StopHurtTicker()
    end
  end)
end

-- =========================================================
-- Show veto hooks (prevents other UI code forcing Show())
-- =========================================================

local function VetoIfNeeded()
  if not PlayerFrame then return end
  if InCombatLockdown() then return end
  if ShouldShowPlayerFrame() then return end

  -- alpha-hide again if anything tried to show it
  SetPlayerFrameVisible(false)
end

local function HookOnce()
  if state.hooked or not PlayerFrame then return end
  state.hooked = true

  PlayerFrame:HookScript("OnShow", VetoIfNeeded)
  hooksecurefunc(PlayerFrame, "Show", VetoIfNeeded)
  hooksecurefunc(PlayerFrame, "SetShown", function(_, shown)
    if shown then VetoIfNeeded() end
  end)

  -- Slightly expand the clickable area so hovering over
  -- the health portion still counts as being over the frame.
  if PlayerFrame.SetHitRectInsets then
    local l, r, t, b = PlayerFrame:GetHitRectInsets()
    l, r, t, b = l or 0, r or 0, t or 0, b or 0
    PlayerFrame:SetHitRectInsets(l - 10, r - 10, t - 10, b - 10)
  end

  PlayerFrame:HookScript("OnEnter", OnPlayerFrameEnter)
  PlayerFrame:HookScript("OnLeave", OnPlayerFrameLeave)

  Apply()
end

local function InitPlayerFrame()
  local tries = 0
  local ticker
  ticker = C_Timer.NewTicker(0.2, function()
    tries = tries + 1
    if PlayerFrame then
      HookOnce()
      ticker:Cancel()
    elseif tries >= 50 then
      ticker:Cancel()
    end
  end)
end

-- =========================================================
-- API exposure for other modules
-- =========================================================

PFH.MarkHurt = MarkHurt
PFH.EnsureHurtTicker = EnsureHurtTicker
PFH.StopHurtTicker = StopHurtTicker

PFH.IsAlwaysShowInstance = IsAlwaysShowInstance
PFH.IsInCombat = IsInCombat
PFH.HasEnemyTarget = HasEnemyTarget
PFH.ShouldShowPlayerFrame = ShouldShowPlayerFrame
PFH.ShouldShowWidgets = ShouldShowWidgets

PFH.ResolveWidgetFramesWithRetries = ResolveWidgetFramesWithRetries

PFH.SetPlayerFrameVisible = SetPlayerFrameVisible
PFH.ApplyWidget = ApplyWidget
PFH.Apply = Apply

PFH.HookOnce = HookOnce
PFH.InitPlayerFrame = InitPlayerFrame

-- Player Frame Hider event handling
local eventFrame = CreateFrame("Frame")

local function OnLogin()
  if state.didInit then
    -- Still refresh combat flag and apply, but don't re-run init/setup.
    state.inCombat = UnitAffectingCombat("player") and true or false
    PFH.Apply()
    return
  end
  state.didInit = true

  state.inCombat = UnitAffectingCombat("player") and true or false
  PFH.InitPlayerFrame()
  PFH.Apply()
end

local function OnRegenDisabled()
  state.inCombat = true
  state.justLeftCombat = false
  PFH.Apply()
end

local function OnRegenEnabled()
  state.inCombat = false
  state.justLeftCombat = true
  PFH.Apply()
end

local function OnTargetOrZoneChanged()
  PFH.Apply()
end

local function OnUnitHealthChanged(unit)
  if unit == "player" then
    PFH.MarkHurt("player")
  elseif unit == "pet" then
    PFH.MarkHurt("pet")
  else
    return
  end

  PFH.EnsureHurtTicker()
  PFH.Apply()
end

eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
eventFrame:RegisterEvent("PLAYER_SOFT_ENEMY_CHANGED")
eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
eventFrame:RegisterEvent("UNIT_HEALTH")
eventFrame:RegisterEvent("UNIT_MAXHEALTH")
eventFrame:RegisterEvent("UNIT_ABSORB_AMOUNT_CHANGED")
eventFrame:RegisterEvent("UNIT_HEAL_PREDICTION")

eventFrame:SetScript("OnEvent", function(_, event, unit)
  if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
    OnLogin()
    return
  end

  PFH.HookOnce()

  if event == "PLAYER_REGEN_DISABLED" then
    OnRegenDisabled()
    return
  end

  if event == "PLAYER_REGEN_ENABLED" then
    OnRegenEnabled()
    return
  end

  if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_SOFT_ENEMY_CHANGED" or event == "ZONE_CHANGED_NEW_AREA" then
    OnTargetOrZoneChanged()
    return
  end

  if event == "UNIT_HEALTH"
    or event == "UNIT_MAXHEALTH"
    or event == "UNIT_ABSORB_AMOUNT_CHANGED"
    or event == "UNIT_HEAL_PREDICTION"
  then
    OnUnitHealthChanged(unit)
    return
  end
end)