local ADDON_NAME = "Beef Snapshot"
local CHAT_PREFIX = "|cFFFF00FF[Beef]|r Snapshot"
local SFS = {}

local DEFAULT_MAX_COMBAT_EVENTS = 50000
local DEFAULT_MAX_OBSERVATIONS_PER_NPC = 200
local DB_VERSION = 2
local MAX_CAST_TIMESTAMPS = 200
local MAX_CAST_TIMESTAMPS_PER_GUID = 50

local frame = CreateFrame("Frame")
local scanElapsed = 0
local wipeArmedAt = nil

local function now()
    return time()
end

local function msg(text)
    DEFAULT_CHAT_FRAME:AddMessage(CHAT_PREFIX .. " " .. text)
end

local function tostringOrNil(value)
    if value == nil then
        return nil
    end
    return tostring(value)
end

local function shallowIncrement(tbl, key)
    if key == nil then
        return
    end
    key = tostring(key)
    tbl[key] = (tbl[key] or 0) + 1
end

local function ensureCooldownBucket(bucket)
    if type(bucket) ~= "table" then
        bucket = {}
    end
    bucket.min = bucket.min or nil
    bucket.max = bucket.max or nil
    bucket.count = bucket.count or 0
    return bucket
end

local function updateCooldownBucket(bucket, delta)
    if not delta or delta <= 0 then
        return ensureCooldownBucket(bucket)
    end

    bucket = ensureCooldownBucket(bucket)
    bucket.min = bucket.min and min(bucket.min, delta) or delta
    bucket.max = bucket.max and max(bucket.max, delta) or delta
    bucket.count = (bucket.count or 0) + 1
    return bucket
end

local function ensureDB()
    if type(BeefSnapshotDB) ~= "table" then
        BeefSnapshotDB = {}
    end

    local db = BeefSnapshotDB
    db.version = DB_VERSION
    db.enabled = db.enabled or false
    db.currentLabel = db.currentLabel or ""
    db.maxCombatEvents = db.maxCombatEvents or DEFAULT_MAX_COMBAT_EVENTS
    db.maxObservationsPerNpc = db.maxObservationsPerNpc or DEFAULT_MAX_OBSERVATIONS_PER_NPC
    db.sessions = db.sessions or {}
    db.npcs = db.npcs or {}
    db.totals = db.totals or {
        observations = 0,
        combatEvents = 0,
    }

    return db
end

local function getZoneInfo()
    local zone = "Unknown"
    local subzone = ""

    if GetRealZoneText then
        zone = GetRealZoneText() or zone
    elseif GetZoneText then
        zone = GetZoneText() or zone
    end

    if GetSubZoneText then
        subzone = GetSubZoneText() or ""
    end

    return zone, subzone
end

local function getCoords()
    if not GetPlayerMapPosition then
        return nil, nil
    end

    if SetMapToCurrentZone then
        SetMapToCurrentZone()
    end

    local x, y = GetPlayerMapPosition("player")
    if not x or not y or (x == 0 and y == 0) then
        return nil, nil
    end

    return floor(x * 10000 + 0.5) / 100, floor(y * 10000 + 0.5) / 100
end

local function parseNpcEntry(guid)
    if type(guid) ~= "string" then
        return nil
    end

    local hex = guid
    if strsub(hex, 1, 2) == "0x" or strsub(hex, 1, 2) == "0X" then
        hex = strsub(hex, 3)
    end
    hex = strupper(hex)

    -- Common 2.4.3 creature GUID shape: F130 + six hex entry id + spawn id.
    if strlen(hex) >= 12 and strsub(hex, 1, 4) == "F130" then
        local entryHex = strsub(hex, 5, 10)
        local entry = tonumber(entryHex, 16)
        if entry and entry > 0 then
            return entry
        end
    end

    -- Some private cores expose a shorter creature GUID where the entry follows F13.
    if strlen(hex) >= 10 and strsub(hex, 1, 3) == "F13" then
        local entryHex = strsub(hex, 4, 9)
        local entry = tonumber(entryHex, 16)
        if entry and entry > 0 then
            return entry
        end
    end

    return nil
end

local function isNpcGuid(guid)
    if type(guid) ~= "string" then
        return false
    end

    local hex = guid
    if strsub(hex, 1, 2) == "0x" or strsub(hex, 1, 2) == "0X" then
        hex = strsub(hex, 3)
    end
    hex = strupper(hex)

    return strsub(hex, 1, 3) == "F13"
end

local function currentSession()
    local db = ensureDB()
    local session = db.sessions[getn(db.sessions)]
    if not session or session.closedAt then
        local zone, subzone = getZoneInfo()
        session = {
            startedAt = now(),
            label = db.currentLabel or "",
            zone = zone,
            subzone = subzone,
            observations = {},
            combat = {},
            chat = {},
            aggregates = {},
        }
        tinsert(db.sessions, session)
    end
    return session
end

local function startRecording()
    local db = ensureDB()
    db.enabled = true
    currentSession()
    msg("recording started.")
end

local function stopRecording()
    local db = ensureDB()
    db.enabled = false
    local session = db.sessions[getn(db.sessions)]
    if session and not session.closedAt then
        session.closedAt = now()
    end
    msg("recording stopped.")
end

local function npcKey(entry, guid, name)
    if entry then
        return tostring(entry)
    end
    if guid then
        return "guid:" .. tostring(guid)
    end
    return "name:" .. tostring(name or "unknown")
end

local function ensureNpcRecord(entry, guid, name)
    local db = ensureDB()
    local key = npcKey(entry, guid, name)

    if not db.npcs[key] then
        db.npcs[key] = {
            entry = entry,
            name = name,
            guids = {},
            observations = {},
            maxHealthSeen = 0,
            levels = {},
            classifications = {},
            creatureTypes = {},
            reactions = {},
            spells = {},
            guidStats = {},
            melee = {
                min = nil,
                max = nil,
                total = 0,
                count = 0,
            },
            firstSeen = now(),
            lastSeen = now(),
            observationCount = 0,
        }
    end

    local npc = db.npcs[key]
    npc.entry = npc.entry or entry
    npc.name = npc.name or name
    npc.lastSeen = now()
    if guid then
        npc.guids[tostring(guid)] = true
        npc.guidStats = npc.guidStats or {}
        npc.guidStats[tostring(guid)] = npc.guidStats[tostring(guid)] or {
            guid = tostring(guid),
            entry = entry,
            name = name,
            firstSeen = now(),
            lastSeen = now(),
            spellCasts = {},
        }
        npc.guidStats[tostring(guid)].entry = npc.guidStats[tostring(guid)].entry or entry
        npc.guidStats[tostring(guid)].name = npc.guidStats[tostring(guid)].name or name
        npc.guidStats[tostring(guid)].lastSeen = now()
    end

    return npc, key
end

local function trimList(list, maxCount)
    while getn(list) > maxCount do
        tremove(list, 1)
    end
end

local function observeUnit(unit, reason)
    if not UnitExists or not UnitExists(unit) or not UnitName(unit) then
        return nil
    end

    if UnitIsPlayer and UnitIsPlayer(unit) then
        return nil
    end

    local guid = UnitGUID and UnitGUID(unit) or nil
    local entry = parseNpcEntry(guid)
    local name = UnitName(unit)
    local level = UnitLevel and UnitLevel(unit) or nil
    local classification = UnitClassification and UnitClassification(unit) or nil
    local creatureType = UnitCreatureType and UnitCreatureType(unit) or nil
    local reaction = UnitReaction and UnitReaction(unit, "player") or nil
    local hp = UnitHealth and UnitHealth(unit) or nil
    local maxHp = UnitHealthMax and UnitHealthMax(unit) or nil
    local zone, subzone = getZoneInfo()
    local x, y = getCoords()
    local db = ensureDB()
    local session = currentSession()
    local npc, key = ensureNpcRecord(entry, guid, name)

    local obs = {
        t = now(),
        reason = reason,
        guid = guid,
        entry = entry,
        name = name,
        level = level,
        classification = classification,
        creatureType = creatureType,
        reaction = reaction,
        zone = zone,
        subzone = subzone,
        x = x,
        y = y,
        hp = hp,
        maxHp = maxHp,
        label = db.currentLabel,
    }

    tinsert(session.observations, obs)
    tinsert(npc.observations, obs)
    trimList(npc.observations, db.maxObservationsPerNpc)

    npc.observationCount = (npc.observationCount or 0) + 1
    npc.lastSeen = obs.t
    if maxHp and maxHp > (npc.maxHealthSeen or 0) then
        npc.maxHealthSeen = maxHp
    end
    shallowIncrement(npc.levels, level)
    shallowIncrement(npc.classifications, classification)
    shallowIncrement(npc.creatureTypes, creatureType)
    shallowIncrement(npc.reactions, reaction)

    db.totals.observations = (db.totals.observations or 0) + 1

    return obs, key
end

local function recordDamage(npc, amount)
    if not npc or not amount then
        return
    end

    local melee = npc.melee
    melee.min = melee.min and min(melee.min, amount) or amount
    melee.max = melee.max and max(melee.max, amount) or amount
    melee.total = (melee.total or 0) + amount
    melee.count = (melee.count or 0) + 1
    melee.avg = floor((melee.total / melee.count) * 100 + 0.5) / 100
end

local function ensureSpell(npc, spellId, spellName, school)
    if not npc or not spellId then
        return nil
    end

    local key = tostring(spellId)
    npc.spells[key] = npc.spells[key] or {
        id = spellId,
        name = spellName,
        school = school,
        count = 0,
        casts = {},
        cooldown = ensureCooldownBucket(),
        damage = {
            min = nil,
            max = nil,
            total = 0,
            count = 0,
        },
        auraApplied = 0,
        auraRemoved = 0,
        uniqueSources = 0,
        byGuid = {},
    }

    local spell = npc.spells[key]
    spell.name = spell.name or spellName
    spell.school = spell.school or school
    spell.cooldown = ensureCooldownBucket(spell.cooldown)
    spell.byGuid = spell.byGuid or {}
    spell.uniqueSources = spell.uniqueSources or 0
    return spell
end

local function ensureGuidSpellState(npc, spell, sourceGUID)
    if not npc or not spell or not sourceGUID then
        return nil
    end

    sourceGUID = tostring(sourceGUID)
    npc.guidStats = npc.guidStats or {}
    npc.guidStats[sourceGUID] = npc.guidStats[sourceGUID] or {
        guid = sourceGUID,
        entry = npc.entry,
        name = npc.name,
        firstSeen = now(),
        lastSeen = now(),
        spellCasts = {},
    }

    local guidState = npc.guidStats[sourceGUID]
    guidState.lastSeen = now()
    guidState.spellCasts = guidState.spellCasts or {}

    local spellKey = tostring(spell.id)
    guidState.spellCasts[spellKey] = guidState.spellCasts[spellKey] or {
        id = spell.id,
        name = spell.name,
        school = spell.school,
        count = 0,
        casts = {},
        cooldown = ensureCooldownBucket(),
        lastCastAt = nil,
    }

    local guidSpell = guidState.spellCasts[spellKey]
    guidSpell.name = guidSpell.name or spell.name
    guidSpell.school = guidSpell.school or spell.school
    guidSpell.cooldown = ensureCooldownBucket(guidSpell.cooldown)

    spell.byGuid = spell.byGuid or {}
    spell.byGuid[sourceGUID] = spell.byGuid[sourceGUID] or {
        count = 0,
        casts = {},
        cooldown = ensureCooldownBucket(),
        lastCastAt = nil,
    }

    spell.uniqueSources = 0
    for _ in pairs(spell.byGuid) do
        spell.uniqueSources = spell.uniqueSources + 1
    end

    return guidSpell, spell.byGuid[sourceGUID]
end

local function recordSpellCast(npc, spell, sourceGUID, timestamp)
    if not spell or not npc then
        return
    end

    timestamp = timestamp or now()

    spell.count = (spell.count or 0) + 1
    tinsert(spell.casts, timestamp)
    trimList(spell.casts, MAX_CAST_TIMESTAMPS)

    local guidSpell, mergedGuidSpell = ensureGuidSpellState(npc, spell, sourceGUID)
    if not guidSpell or not mergedGuidSpell then
        return
    end

    guidSpell.count = (guidSpell.count or 0) + 1
    tinsert(guidSpell.casts, timestamp)
    trimList(guidSpell.casts, MAX_CAST_TIMESTAMPS_PER_GUID)

    mergedGuidSpell.count = (mergedGuidSpell.count or 0) + 1
    tinsert(mergedGuidSpell.casts, timestamp)
    trimList(mergedGuidSpell.casts, MAX_CAST_TIMESTAMPS_PER_GUID)

    if guidSpell.lastCastAt and timestamp > guidSpell.lastCastAt then
        local delta = floor((timestamp - guidSpell.lastCastAt) * 100 + 0.5) / 100
        guidSpell.cooldown = updateCooldownBucket(guidSpell.cooldown, delta)
        mergedGuidSpell.cooldown = updateCooldownBucket(mergedGuidSpell.cooldown, delta)
        spell.cooldown = updateCooldownBucket(spell.cooldown, delta)
    end

    guidSpell.lastCastAt = timestamp
    mergedGuidSpell.lastCastAt = timestamp
end

local function recordSpellDamage(spell, amount)
    if not spell or not amount then
        return
    end

    local damage = spell.damage
    damage.min = damage.min and min(damage.min, amount) or amount
    damage.max = damage.max and max(damage.max, amount) or amount
    damage.total = (damage.total or 0) + amount
    damage.count = (damage.count or 0) + 1
    damage.avg = floor((damage.total / damage.count) * 100 + 0.5) / 100
end

local function pushCombatEvent(eventRecord)
    local db = ensureDB()
    local session = currentSession()

    tinsert(session.combat, eventRecord)
    trimList(session.combat, db.maxCombatEvents)
    db.totals.combatEvents = (db.totals.combatEvents or 0) + 1
end

local function recordMonsterChat(eventType, ...)
    local db = ensureDB()
    if not db.enabled then
        return
    end

    local text, speaker = ...
    if text == nil and arg1 ~= nil then
        text = arg1
        speaker = arg2
    end

    local session = currentSession()
    session.chat = session.chat or {}
    tinsert(session.chat, {
        t = now(),
        type = eventType,
        text = text,
        speaker = speaker,
        zone = getZoneInfo(),
        label = db.currentLabel,
    })
    trimList(session.chat, 1000)
end

local function recordCombatLog(...)
    local db = ensureDB()
    if not db.enabled then
        return
    end

    local timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags = ...
    if not eventType then
        timestamp = arg1
        eventType = arg2
        sourceGUID = arg3
        sourceName = arg4
        sourceFlags = arg5
        destGUID = arg6
        destName = arg7
        destFlags = arg8
    end

    if not eventType then
        return
    end

    local sourceEntry = parseNpcEntry(sourceGUID)
    local destEntry = parseNpcEntry(destGUID)
    local spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, missType

    if strfind(eventType, "SPELL_") == 1 or strfind(eventType, "RANGE_") == 1 then
        spellId = select(9, ...)
        spellName = select(10, ...)
        spellSchool = select(11, ...)
        if spellId == nil and arg9 ~= nil then
            spellId = arg9
            spellName = arg10
            spellSchool = arg11
        end
    end

    if eventType == "SWING_DAMAGE" then
        amount = select(9, ...)
        overkill = select(10, ...)
        school = select(11, ...)
        resisted = select(12, ...)
        blocked = select(13, ...)
        absorbed = select(14, ...)
        critical = select(15, ...)
        glancing = select(16, ...)
        crushing = select(17, ...)
        if amount == nil and arg9 ~= nil then
            amount = arg9
            overkill = arg10
            school = arg11
            resisted = arg12
            blocked = arg13
            absorbed = arg14
            critical = arg15
            glancing = arg16
            crushing = arg17
        end
    elseif eventType == "SPELL_DAMAGE" or eventType == "SPELL_PERIODIC_DAMAGE" or eventType == "RANGE_DAMAGE" then
        amount = select(12, ...)
        overkill = select(13, ...)
        school = select(14, ...)
        resisted = select(15, ...)
        blocked = select(16, ...)
        absorbed = select(17, ...)
        critical = select(18, ...)
        glancing = select(19, ...)
        crushing = select(20, ...)
        if amount == nil and arg12 ~= nil then
            amount = arg12
            overkill = arg13
            school = arg14
            resisted = arg15
            blocked = arg16
            absorbed = arg17
            critical = arg18
            glancing = arg19
            crushing = arg20
        end
    elseif eventType == "SWING_MISSED" then
        missType = select(9, ...)
        if missType == nil and arg9 ~= nil then
            missType = arg9
        end
    elseif eventType == "SPELL_MISSED" or eventType == "SPELL_PERIODIC_MISSED" or eventType == "RANGE_MISSED" then
        missType = select(12, ...)
        if missType == nil and arg12 ~= nil then
            missType = arg12
        end
    end

    local eventRecord = {
        t = timestamp or now(),
        type = eventType,
        sourceGUID = sourceGUID,
        sourceName = sourceName,
        sourceEntry = sourceEntry,
        sourceFlags = sourceFlags,
        destGUID = destGUID,
        destName = destName,
        destEntry = destEntry,
        destFlags = destFlags,
        spellId = spellId,
        spellName = spellName,
        spellSchool = spellSchool,
        amount = amount,
        overkill = overkill,
        resisted = resisted,
        blocked = blocked,
        absorbed = absorbed,
        critical = critical,
        glancing = glancing,
        crushing = crushing,
        missType = missType,
        label = db.currentLabel,
    }

    pushCombatEvent(eventRecord)

    if sourceEntry or isNpcGuid(sourceGUID) then
        local npc = ensureNpcRecord(sourceEntry, sourceGUID, sourceName)
        if eventType == "SWING_DAMAGE" then
            recordDamage(npc, amount)
        elseif spellId then
            local spell = ensureSpell(npc, spellId, spellName, spellSchool)
            if eventType == "SPELL_CAST_START" or eventType == "SPELL_CAST_SUCCESS" then
                recordSpellCast(npc, spell, sourceGUID, timestamp)
            elseif eventType == "SPELL_DAMAGE" or eventType == "SPELL_PERIODIC_DAMAGE" or eventType == "RANGE_DAMAGE" then
                spell.count = (spell.count or 0) + 1
                recordSpellDamage(spell, amount)
            elseif eventType == "SPELL_AURA_APPLIED" then
                spell.auraApplied = (spell.auraApplied or 0) + 1
            elseif eventType == "SPELL_AURA_REMOVED" then
                spell.auraRemoved = (spell.auraRemoved or 0) + 1
            end
        end
    end
end

local function scanUnits()
    observeUnit("target", "scan")
    observeUnit("mouseover", "scan")

    if UnitExists and UnitExists("focus") then
        observeUnit("focus", "scan")
    end

    for i = 1, 4 do
        observeUnit("party" .. i .. "target", "scan")
    end

    for i = 1, 40 do
        observeUnit("raid" .. i .. "target", "scan")
    end
end

local function printUnit(unit)
    if not UnitExists or not UnitExists(unit) then
        msg("no " .. unit .. " unit.")
        return
    end

    local guid = UnitGUID and UnitGUID(unit) or nil
    local entry = parseNpcEntry(guid)
    local name = UnitName(unit) or "Unknown"
    local hp = UnitHealth and UnitHealth(unit) or "?"
    local maxHp = UnitHealthMax and UnitHealthMax(unit) or "?"
    local classification = UnitClassification and UnitClassification(unit) or "?"
    local level = UnitLevel and UnitLevel(unit) or "?"

    msg(name .. " entry=" .. tostringOrNil(entry) .. " guid=" .. tostringOrNil(guid) .. " hp=" .. tostring(hp) .. "/" .. tostring(maxHp) .. " class=" .. tostring(classification) .. " level=" .. tostring(level))
end

local function status()
    local db = ensureDB()
    local session = db.sessions[getn(db.sessions)]
    local combatCount = session and session.combat and getn(session.combat) or 0
    local observationCount = db.totals and db.totals.observations or 0
    local npcCount = 0

    for _ in pairs(db.npcs or {}) do
        npcCount = npcCount + 1
    end

    msg("enabled=" .. tostring(db.enabled) .. " label=\"" .. tostring(db.currentLabel or "") .. "\" npcs=" .. npcCount .. " observations=" .. observationCount .. " sessionCombat=" .. combatCount)
end

local function help()
    msg("/sfs start - enable recording")
    msg("/sfs stop - disable recording")
    msg("/sfs status - show counts and label")
    msg("/sfs mark <label> - set current run label")
    msg("/sfs target - record current target")
    msg("/sfs mouseover - record current mouseover")
    msg("/sfs scan - record target, mouseover, focus, party targets, raid targets")
    msg("/sfs print - print current target parse and HP")
    msg("/sfs wipe - arm wipe; run again within 10 seconds to clear data")
    msg("/sfs help - show commands")
end

local function wipeDB()
    local t = now()
    if not wipeArmedAt or (t - wipeArmedAt) > 10 then
        wipeArmedAt = t
        msg("wipe armed. Run /sfs wipe again within 10 seconds to clear saved data.")
        return
    end

    BeefSnapshotDB = nil
    ensureDB()
    wipeArmedAt = nil
    msg("saved data cleared.")
end

local function slashHandler(input)
    input = input or ""
    local _, _, command, rest = strfind(input, "^%s*(%S*)%s*(.-)%s*$")
    command = strlower(command or "")
    rest = rest or ""

    if command == "start" then
        startRecording()
    elseif command == "stop" then
        stopRecording()
    elseif command == "status" then
        status()
    elseif command == "mark" then
        local db = ensureDB()
        db.currentLabel = rest
        local session = db.sessions[getn(db.sessions)]
        if session and not session.closedAt then
            session.label = rest
        end
        msg("label set to \"" .. rest .. "\".")
    elseif command == "target" then
        local obs = observeUnit("target", "manual-target")
        if obs then
            msg("recorded target " .. tostring(obs.name) .. " entry=" .. tostringOrNil(obs.entry) .. " hp=" .. tostring(obs.hp) .. "/" .. tostring(obs.maxHp))
        else
            msg("no target recorded.")
        end
    elseif command == "mouseover" then
        local obs = observeUnit("mouseover", "manual-mouseover")
        if obs then
            msg("recorded mouseover " .. tostring(obs.name) .. " entry=" .. tostringOrNil(obs.entry) .. " hp=" .. tostring(obs.hp) .. "/" .. tostring(obs.maxHp))
        else
            msg("no mouseover recorded.")
        end
    elseif command == "scan" then
        scanUnits()
        msg("scan complete.")
    elseif command == "print" then
        printUnit("target")
    elseif command == "wipe" then
        wipeDB()
    else
        help()
    end
end

local function onEvent(self, event, ...)
    event = event or _G.event

    if event == "PLAYER_LOGIN" then
        ensureDB()
        SLASH_STORMFORGESNAPSHOT1 = "/sfs"
        SlashCmdList["STORMFORGESNAPSHOT"] = slashHandler
        msg("loaded. Use /sfs help.")
    elseif event == "PLAYER_LOGOUT" then
        local db = ensureDB()
        if db.enabled then
            local session = db.sessions[getn(db.sessions)]
            if session and not session.closedAt then
                session.closedAt = now()
            end
        end
    elseif event == "PLAYER_TARGET_CHANGED" then
        if ensureDB().enabled then
            observeUnit("target", "target-changed")
        end
    elseif event == "UPDATE_MOUSEOVER_UNIT" then
        if ensureDB().enabled then
            observeUnit("mouseover", "mouseover")
        end
    elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then
        recordCombatLog(...)
    elseif event == "CHAT_MSG_MONSTER_YELL" or event == "CHAT_MSG_MONSTER_EMOTE" or event == "CHAT_MSG_RAID_BOSS_EMOTE" then
        recordMonsterChat(event, ...)
    elseif event == "ZONE_CHANGED" or event == "ZONE_CHANGED_NEW_AREA" then
        if ensureDB().enabled then
            currentSession().lastZoneChangedAt = now()
        end
    end
end

local function onUpdate(self, elapsed)
    if not ensureDB().enabled then
        return
    end

    scanElapsed = scanElapsed + elapsed
    if scanElapsed >= 2 then
        scanElapsed = 0
        scanUnits()
    end
end

frame:SetScript("OnEvent", onEvent)
frame:SetScript("OnUpdate", onUpdate)
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_LOGOUT")
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
frame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
frame:RegisterEvent("CHAT_MSG_MONSTER_YELL")
frame:RegisterEvent("CHAT_MSG_MONSTER_EMOTE")
frame:RegisterEvent("CHAT_MSG_RAID_BOSS_EMOTE")
frame:RegisterEvent("ZONE_CHANGED")
frame:RegisterEvent("ZONE_CHANGED_NEW_AREA")

SFS.ParseNpcEntry = parseNpcEntry
SFS.ObserveUnit = observeUnit
BeefSnapshot = SFS
