local MAJOR_VERSION = "LibGetFrame-1.0"
local MINOR_VERSION = 9
if not LibStub then error(MAJOR_VERSION .. " requires LibStub.") end
local lib = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION)
if not lib then return end

lib.callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib)
local callbacks = lib.callbacks

local GetPlayerInfoByGUID, UnitExists, IsAddOnLoaded, C_Timer, UnitIsUnit, SecureButton_GetUnit = GetPlayerInfoByGUID, UnitExists, IsAddOnLoaded, C_Timer, UnitIsUnit, SecureButton_GetUnit
local tinsert, CopyTable, wipe = tinsert, CopyTable, wipe

local maxDepth = 50

local defaultFramePriorities = {
    -- raid frames
    [1] = "^Vd1", -- vuhdo
    [2] = "^Vd2", -- vuhdo
    [3] = "^Vd3", -- vuhdo
    [4] = "^Vd4", -- vuhdo
    [5] = "^Vd5", -- vuhdo
    [6] = "^Vd", -- vuhdo
    [7] = "^HealBot", -- healbot
    [8] = "^GridLayout", -- grid
    [9] = "^Grid2Layout", -- grid2
    [10] = "^ElvUF_RaidGroup", -- elv
    [11] = "^oUF_bdGrid", -- bdgrid
    [12] = "^oUF.*raid", -- generic oUF
    [13] = "^LimeGroup", -- lime
    [14] = "^SUFHeaderraid", -- suf
    [15] = "^CompactRaid", -- blizz
    -- party frames
    [16] = "^AleaUI_GroupHeader", -- Alea
    [17] = "^SUFHeaderparty", --suf
    [18] = "^ElvUF_PartyGroup", -- elv
    [19] = "^oUF.*party", -- generic oUF
    [20] = "^PitBull4_Groups_Party", -- pitbull4
    [21] = "^CompactParty", -- blizz
    -- player frame
    [22] = "^SUFUnitplayer",
    [23] = "^PitBull4_Frames_Player",
    [24] = "^ElvUF_Player",
    [25] = "^oUF.*player",
    [26] = "^PlayerFrame",
}

local defaultPlayerFrames = {
    "SUFUnitplayer",
    "PitBull4_Frames_Player",
    "ElvUF_Player",
    "oUF_TukuiPlayer",
    "PlayerFrame",
}
local defaultTargetFrames = {
    "SUFUnittarget",
    "PitBull4_Frames_Target",
    "ElvUF_Target",
    "TargetFrame",
    "oUF_TukuiTarget",
}
local defaultTargettargetFrames = {
    "SUFUnittargetarget",
    "PitBull4_Frames_Target's target",
    "ElvUF_TargetTarget",
    "TargetTargetFrame",
    "oUF_TukuiTargetTarget",
}

local GetFramesCache = {}

local function ScanFrames(depth, frame, ...)
    if not frame then return end
    if depth < maxDepth
    and frame.IsForbidden
    and not frame:IsForbidden()
    then
        local frameType = frame:GetObjectType()
        if frameType == "Frame" or frameType == "Button" then
            ScanFrames(depth + 1, frame:GetChildren())
        end
        if frameType == "Button" then
            local unit = SecureButton_GetUnit(frame)
            local name = frame:GetName()
            if unit and frame:IsVisible() and name then
                GetFramesCache[frame] = name
            end
        end
    end
    ScanFrames(depth, ...)
end

local wait = false
local function ScanForUnitFrames(noDelay)
    if noDelay then
        wipe(GetFramesCache)
        ScanFrames(0, UIParent)
        callbacks:Fire("GETFRAME_REFRESH")
    elseif not wait then
        wait = true
        C_Timer.After(1, function()
            wipe(GetFramesCache)
            ScanFrames(0, UIParent)
            wait = false
            callbacks:Fire("GETFRAME_REFRESH")
        end)
    end
end

local function isFrameFiltered(name, ignoredFrames)
    for _, filter in pairs(ignoredFrames) do
        if name:find(filter) then
            return true
        end
    end
    return false
end

local function GetUnitFrames(target, ignoredFrames)
    if not UnitExists(target) then
        if type(target) == "string" and target:find("Player") then
            target = select(6, GetPlayerInfoByGUID(target))
        else
            target = target:gsub(" .*", "")
            if not UnitExists(target) then
                return
            end
        end
    end

    local frames
    for frame, frameName in pairs(GetFramesCache) do
        local unit = SecureButton_GetUnit(frame)
        if unit and UnitIsUnit(unit, target)
        and not isFrameFiltered(frameName, ignoredFrames)
        then
            frames = frames or {}
            frames[frame] = frameName
        end
    end
    return frames
end

local function ElvuiWorkaround(frame)
    if IsAddOnLoaded("ElvUI") and frame and frame:GetName():find("^ElvUF_") and frame.Health then
        return frame.Health
    else
        return frame
    end
end

local defaultOptions = {
    framePriorities = defaultFramePriorities,
    ignorePlayerFrame = true,
    ignoreTargetFrame = true,
    ignoreTargettargetFrame = true,
    playerFrames = defaultPlayerFrames,
    targetFrames = defaultTargetFrames,
    targettargetFrames = defaultTargettargetFrames,
    ignoreFrames = {
        "PitBull4_Frames_Target's target's target"
    },
    returnAll = false,
}

local GetFramesCacheListener
local function Init(noDelay)
    GetFramesCacheListener = CreateFrame("Frame")
    GetFramesCacheListener:RegisterEvent("PLAYER_REGEN_DISABLED")
    GetFramesCacheListener:RegisterEvent("PLAYER_REGEN_ENABLED")
    GetFramesCacheListener:RegisterEvent("PLAYER_ENTERING_WORLD")
    GetFramesCacheListener:RegisterEvent("PARTY_MEMBERS_CHANGED")
    GetFramesCacheListener:RegisterEvent("RAID_ROSTER_UPDATE")
    GetFramesCacheListener:SetScript("OnEvent", function() ScanForUnitFrames(false) end)
    ScanForUnitFrames(noDelay)
end

function lib.GetUnitFrame(target, opt)
    if type(GetFramesCacheListener) ~= "table" then Init(true) end
    opt = opt or {}
    setmetatable(opt, { __index = defaultOptions })

    if not target then return end

    local ignoredFrames = CopyTable(opt.ignoreFrames)
    if opt.ignorePlayerFrame then
        for _,v in pairs(opt.playerFrames) do
            tinsert(ignoredFrames, v)
        end
    end
    if opt.ignoreTargetFrame then
        for _,v in pairs(opt.targetFrames) do
            tinsert(ignoredFrames, v)
        end
    end
    if opt.ignoreTargettargetFrame then
        for _,v in pairs(opt.targettargetFrames) do
            tinsert(ignoredFrames, v)
        end
    end

    local frames = GetUnitFrames(target, ignoredFrames)
    if not frames then return end

    if not opt.returnAll then
        for i = 1, #opt.framePriorities do
            for frame, frameName in pairs(frames) do
                if frameName:find(opt.framePriorities[i]) then
                    return ElvuiWorkaround(frame)
                end
            end
        end
        local next = next
        return ElvuiWorkaround(next(frames))
    else
        for frame in pairs(frames) do
            frames[frame] = ElvuiWorkaround(frame)
        end
        return frames
    end
end
lib.GetFrame = lib.GetUnitFrame -- compatibility
