WoWInterface SVN NeedToKnow-Updated

[/] [trunk/] [NeedToKnow/] [NeedToKnow.lua] - Rev 129

Compare with Previous | Blame | View Log

-- ----------------------
-- NeedToKnow
-- by Kitjan, lieandswell
-- ----------------------

local trace = print
--function maybe_trace(...)
  --local so_far = ""
  --local p = _G
  --for idx = 1,40,1 do
      --local v = select(idx,...)
      --if not v then 
        --break 
      --end
      --p = p[v]
      --if not p then
        --if so_far == "" then
          --trace("global variable",v,"does not exist")
        --else
          --trace(so_far,"does not have member",v)
        --end
        --return;
      --end
      --so_far = so_far .. "." .. v
  --end
  --trace(so_far,"=",p)
--end
--
-- -------------
-- ADDON GLOBALS
-- -------------

NeedToKnow = {}
NeedToKnowLoader = {}

-- -------------
-- ADDON MEMBERS
-- -------------
local g_GetActiveTalentGroup = _G.GetActiveSpecGroup
local g_UnitAffectingCombat = UnitAffectingCombat
local g_UnitIsFriend = UnitIsFriend
local g_UnitGUID = UnitGUID
local g_GetTime = GetTime
local g_GetWeaponEnchantInfo = GetWeaponEnchantInfo

local m_last_guid, m_last_cast, m_last_sent, m_last_cast_head, m_last_cast_tail
local m_bInCombat, m_bCombatWithBoss

local mfn_Bar_AuraCheck
local mfn_EnergyBar_OnUpdate
local mfn_AuraCheck_Single
local mfn_AuraCheck_TOTEM
local mfn_AuraCheck_BUFFCD
local mfn_AuraCheck_USABLE
local mfn_AuraCheck_EQUIPSLOT
local mfn_AuraCheck_POWER
local mfn_AuraCheck_CASTCD
local mfn_AuraCheck_Weapon
local mfn_AuraCheck_AllStacks
local mfn_GetUnresolvedCooldown
local mfn_GetAutoShotCooldown
local mfn_GetSpellChargesCooldown
local mfn_GetSpellCooldown
local mfn_AddInstanceToStacks
local mfn_SetStatusBarValue
local mfn_ResetScratchStacks
local mfn_UpdateVCT

local m_scratch = {}
m_scratch.all_stacks = 
    {
        min = 
        {
            buffName = "", 
            duration = 0, 
            expirationTime = 0, 
            iconPath = "",
            caster = ""
        },
        max = 
        {
            duration = 0, 
            expirationTime = 0, 
        },
        total = 0,
        total_ttn = { 0, 0, 0 }
    }
m_scratch.buff_stacks = 
    {
        min = 
        {
            buffName = "", 
            duration = 0, 
            expirationTime = 0, 
            iconPath = "",
            caster = ""
        },
        max = 
        {
            duration = 0, 
            expirationTime = 0, 
        },
        total = 0,
        total_ttn = { 0, 0, 0 }
    }
m_scratch.bar_entry = 
    {
        idxName = 0,
        barSpell = "",
        isSpellID = false,
    }
-- NEEDTOKNOW = {} is defined in the localization file, which must be loaded before this file

NEEDTOKNOW.VERSION = "4.0.17"
local c_UPDATE_INTERVAL = 0.05
local c_MAXBARS = 20

-- Get the localized name of spell 75, which is "Auto Shot" in US English
local c_AUTO_SHOT_NAME = GetSpellInfo(75)


-- COMBAT_LOG_EVENT_UNFILTERED events where select(6,...) is the caster, 9 is the spellid, and 10 is the spell name
-- (used for Target-of-target monitoring)
local c_AURAEVENTS = {
    SPELL_AURA_APPLIED = true,
    SPELL_AURA_REMOVED = true,
    SPELL_AURA_APPLIED_DOSE = true,
    SPELL_AURA_REMOVED_DOSE = true,
    SPELL_AURA_REFRESH = true,
    SPELL_AURA_BROKEN = true,
    SPELL_AURA_BROKEN_SPELL = true
}
    
    
NEEDTOKNOW.BAR_DEFAULTS = {
    Enabled         = true,
    AuraName        = "",
    Unit            = "player",
    BuffOrDebuff    = "HELPFUL",
    OnlyMine        = true,
    BarColor        = { r=0.6, g=0.6, b=0.6, a=1.0 },
    MissingBlink    = { r=0.9, g=0.1, b=0.1, a=0.5 },
    TimeFormat      = "Fmt_SingleUnit",
    vct_enabled     = false,
    vct_color       = { r=0.6, g=0.6, b=0.0, a=0.3 },
    vct_spell       = "",
    vct_extra       = 0,
    bDetectExtends  = false,
    show_text       = true,
    show_count      = true,
    show_time       = true,
    show_spark      = true,
    show_icon       = false,
    show_mypip      = false,
    show_all_stacks = false,
    show_charges    = true,
    show_ttn1       = false,
    show_ttn2       = false,
    show_ttn3       = false,
    show_text_user  = "",
    blink_enabled   = false,
    blink_ooc       = true,
    blink_boss      = false,
    blink_label     = "",
    buffcd_duration = 0,
    buffcd_reset_spells = "",
    usable_duration = 0,
    append_cd       = true,
    append_usable   = false,
}
NEEDTOKNOW.GROUP_DEFAULTS = {
    Enabled          = true,
    NumberBars       = 3,
    Scale            = 1.0,
    Width            = 270,
    Bars             = { NEEDTOKNOW.BAR_DEFAULTS, NEEDTOKNOW.BAR_DEFAULTS, NEEDTOKNOW.BAR_DEFAULTS },
    Position         = { "TOPLEFT", "TOPLEFT", 100, -100 },
    FixedDuration    = 0, 
}
NEEDTOKNOW.DEFAULTS = {
    Version       = NEEDTOKNOW.VERSION,
    OldVersion  = NEEDTOKNOW.VERSION,
    Profiles    = {},
    Chars       = {},
}
NEEDTOKNOW.CHARACTER_DEFAULTS = {
    Specs       = {},
    Locked      = false,
    Profiles    = {},
}
NEEDTOKNOW.PROFILE_DEFAULTS = {
    name        = "Default",
    nGroups     = 1,
    Groups      = { NEEDTOKNOW.GROUP_DEFAULTS },
    BarTexture  = "BantoBar",
    BarFont     = "Fritz Quadrata TT",
    BkgdColor   = { 0, 0, 0, 0.8 },
    BarSpacing  = 3,
    BarPadding  = 3,
    FontSize    = 12,
    FontOutline = 0,
}

NEEDTOKNOW.SHORTENINGS= {
    Enabled         = "On",
    AuraName        = "Aura",
    --Unit            = "Unit",
    BuffOrDebuff    = "Typ",
    OnlyMine        = "Min",
    BarColor        = "Clr",
    MissingBlink    = "BCl",
    TimeFormat      = "TF",
    vct_enabled     = "VOn",
    vct_color       = "VCl",
    vct_spell       = "VSp",
    vct_extra       = "VEx",
    bDetectExtends  = "Ext",
    show_text       = "sTx",
    show_count      = "sCt",
    show_time       = "sTm",
    show_spark      = "sSp",
    show_icon       = "sIc",
    show_mypip      = "sPp",
    show_all_stacks = "All",
    show_charges    = "Chg",
    show_text_user  = "sUr",
    show_ttn1       = "sN1",
    show_ttn2       = "sN2",
    show_ttn3       = "sN3",
    blink_enabled   = "BOn",
    blink_ooc       = "BOC",
    blink_boss      = "BBs",
    blink_label     = "BTx",
    buffcd_duration = "cdd",
    buffcd_reset_spells = "cdr",
    usable_duration = "udr",
    append_cd       = "acd",
    append_usable   = "aus",
    --NumberBars       = "NmB",
    --Scale            = "Scl",
    --Width            = "Cx",
    --Bars             = "Brs",
    --Position         = "Pos",
    --FixedDuration    = "FxD", 
    --Version       = "Ver",
    --OldVersion  = "OVr",
    --Profiles    = "Pfl",
    --Chars       = "Chr",
    --Specs       = "Spc",
    --Locked      = "Lck",
    --name        = "nam",
    --nGroups     = "nGr",
    --Groups      = "Grp",
    --BarTexture  = "Tex",
    --BarFont     = "Fnt",
    --BkgdColor   = "BgC",
    --BarSpacing  = "BSp",
    --BarPadding  = "BPd",
    --FontSize    = "FSz",
    --FontOutline = "FOl",
}

NEEDTOKNOW.LENGTHENINGS= {
   On = "Enabled",
   Aura = "AuraName",
--   Unit = "Unit",
   Typ = "BuffOrDebuff",
   Min = "OnlyMine",
   Clr = "BarColor",
   BCl = "MissingBlink",
   TF  = "TimeFormat",
   VOn = "vct_enabled",
   VCl = "vct_color",
   VSp = "vct_spell",
   VEx = "vct_extra",
   Ext = "bDetectExtends",
   sTx = "show_text",
   sCt = "show_count",
   sN1 = "show_ttn1",
   sN2 = "show_ttn2",
   sN3 = "show_ttn3",
   sTm = "show_time",
   sSp = "show_spark",
   sIc = "show_icon",
   sPp = "show_mypip",
   All = "show_all_stacks",
   Chg = "show_charges",
   sUr = "show_text_user",
   BOn = "blink_enabled",
   BOC = "blink_ooc",
   BBs = "blink_boss",
   BTx = "blink_label",
   cdd = "buffcd_duration",
   cdr = "buffcd_reset_spells",
   udr = "usable_duration",
   acd = "append_cd",
   aus = "append_usable",
    --NumberBars       = "NmB",
    --Scale            = "Scl",
    --Width            = "Cx",
    --Bars             = "Brs",
    --Position         = "Pos",
    --FixedDuration    = "FxD", 
    --Version       = "Ver",
    --OldVersion  = "OVr",
    --Profiles    = "Pfl",
    --Chars       = "Chr",
    --Specs       = "Spc",
    --Locked      = "Lck",
    --name can't be compressed since it's used even when not the active profile
    --nGroups     = "nGr",
    --Groups      = "Grp",
    --BarTexture  = "Tex",
    --BarFont     = "Fnt",
    --BkgdColor   = "BgC",
    --BarSpacing  = "BSp",
    --BarPadding  = "BPd",
    --FontSize    = "FSz",
    --FontOutline = "FOl";
}

-- -------------------
-- SharedMedia Support
-- -------------------

    NeedToKnow.LSM = LibStub("LibSharedMedia-3.0", true)
    
    if not NeedToKnow.LSM:Fetch("statusbar", "Aluminum", true) then NeedToKnow.LSM:Register("statusbar", "Aluminum",           [[Interface\Addons\NeedToKnow\Textures\Aluminum.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Armory", true) then NeedToKnow.LSM:Register("statusbar", "Armory",             [[Interface\Addons\NeedToKnow\Textures\Armory.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "BantoBar", true) then NeedToKnow.LSM:Register("statusbar", "BantoBar",           [[Interface\Addons\NeedToKnow\Textures\BantoBar.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "DarkBottom", true) then NeedToKnow.LSM:Register("statusbar", "DarkBottom",         [[Interface\Addons\NeedToKnow\Textures\Darkbottom.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Default", true) then NeedToKnow.LSM:Register("statusbar", "Default",            [[Interface\Addons\NeedToKnow\Textures\Default.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Flat", true) then NeedToKnow.LSM:Register("statusbar", "Flat",               [[Interface\Addons\NeedToKnow\Textures\Flat.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Glaze", true) then NeedToKnow.LSM:Register("statusbar", "Glaze",              [[Interface\Addons\NeedToKnow\Textures\Glaze.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Gloss", true) then NeedToKnow.LSM:Register("statusbar", "Gloss",              [[Interface\Addons\NeedToKnow\Textures\Gloss.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Graphite", true) then NeedToKnow.LSM:Register("statusbar", "Graphite",           [[Interface\Addons\NeedToKnow\Textures\Graphite.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Minimalist", true) then NeedToKnow.LSM:Register("statusbar", "Minimalist",         [[Interface\Addons\NeedToKnow\Textures\Minimalist.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Otravi", true) then NeedToKnow.LSM:Register("statusbar", "Otravi",             [[Interface\Addons\NeedToKnow\Textures\Otravi.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Smooth", true) then NeedToKnow.LSM:Register("statusbar", "Smooth",             [[Interface\Addons\NeedToKnow\Textures\Smooth.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Smooth v2", true) then NeedToKnow.LSM:Register("statusbar", "Smooth v2",          [[Interface\Addons\NeedToKnow\Textures\Smoothv2.tga]]) end
    if not NeedToKnow.LSM:Fetch("statusbar", "Striped", true) then NeedToKnow.LSM:Register("statusbar", "Striped",            [[Interface\Addons\NeedToKnow\Textures\Striped.tga]]) end

-- ---------------
-- EXECUTIVE FRAME
-- ---------------

function NeedToKnow.ExecutiveFrame_OnEvent(self, event, ...)
    local fnName = "ExecutiveFrame_"..event
    local fn = NeedToKnow[fnName]
    if ( fn ) then
        fn(...)
    end
end

function NeedToKnow.ExecutiveFrame_UNIT_SPELLCAST_SENT(unit, spell, rank_str, tgt, serialno)
    if unit == "player" then
        -- TODO: I hate to pay this memory cost for every "spell" ever cast.
        --       Would be nice to at least garbage collect this data at some point, but that
        --       may add more overhead than just keeping track of 100 spells.
        if not m_last_sent then
            m_last_sent = {}
        end
        m_last_sent[spell] = g_GetTime()

        -- How expensive a second check do we need?
        if ( m_last_guid[spell] or NeedToKnow.BarsForPSS ) then
            local r = m_last_cast[m_last_cast_tail]
            if not r then
                r = { spell=spell, target=tgt, serial=serialno }
                m_last_cast[m_last_cast_tail] = r
            else
                r.spell = spell
                r.target = tgt
                r.serial = serialno
            end
            m_last_cast_tail = m_last_cast_tail + 1
            if ( m_last_cast_tail == 2 ) then
                m_last_cast_head = 1
                if ( m_last_guid[spell] ) then
                    NeedToKnow_ExecutiveFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
                    NeedToKnow_ExecutiveFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
                else
                    NeedToKnow_ExecutiveFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
                end
            end
        end
    end
end


function NeedToKnow.ExecutiveFrame_UNIT_SPELLCAST_SUCCEEDED(unit, spell, rank_str, serialno, spellid)
    if unit == "player" then
        local found
        local t = m_last_cast
        local last = m_last_cast_tail-1
        local i
        for i = last,m_last_cast_head,-1  do
            if t[i].spell == spell and t[i].serial == serialno then
                found = i
                break
            end
        end

        if found then
            if ( NeedToKnow.BarsForPSS ) then
                local bar,one
                for bar,one in pairs(NeedToKnow.BarsForPSS) do
                    local unitTarget = NeedToKnow.raid_members[t[found].target or ""]
                    NeedToKnow.Bar_OnEvent(bar, "PLAYER_SPELLCAST_SUCCEEDED", "player", spell, spellid, unitTarget);
                end
            end

            if ( found == last ) then
                m_last_cast_tail = 1
                m_last_cast_head = 1
                NeedToKnow_ExecutiveFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
            else
                m_last_cast_head = found+1
            end
        end
    end
end

function NeedToKnow.ExecutiveFrame_COMBAT_LOG_EVENT_UNFILTERED(tod, event, hideCaster, guidCaster, ...)
    -- the time that's passed in appears to be time of day, not game time like everything else.
    local time = g_GetTime() 
    -- TODO: Is checking r.state sufficient or must event be checked instead?
    if ( guidCaster == NeedToKnow.guidPlayer and event=="SPELL_CAST_SUCCESS") then
        local guidTarget, nameTarget, _, _, spellid, spell = select(4, ...) -- source_name, source_flags, source_flags2, 

        local found
        local t = m_last_cast
        local last = m_last_cast_tail-1
        local i
        for i = last,m_last_cast_head,-1  do
            if t[i].spell == spell then
                found = i
                break
            end
        end
        if found then
            if ( NeedToKnow.BarsForPSS ) then
                local bar,one
                for bar,one in pairs(NeedToKnow.BarsForPSS) do
                    local unitTarget = NeedToKnow.raid_members[t[found].target or ""]
                    NeedToKnow.Bar_OnEvent(bar, "PLAYER_SPELLCAST_SUCCEEDED", "player", spell, spellid, unitTarget);
                end
            end

            local rBySpell = m_last_guid[spell]
            if ( rBySpell ) then
                local rByGuid = rBySpell[guidTarget]
                if not rByGuid then
                    rByGuid = { time=time, dur=0, expiry=0 }
                    rBySpell[guidTarget] = rByGuid
                else
                    rByGuid.time= time
                    rByGuid.dur = 0
                    rByGuid.expiry = 0
                end
            end

            if ( found == last ) then
                m_last_cast_tail = 1
                m_last_cast_head = 1
                NeedToKnow_ExecutiveFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
            else
                m_last_cast_head = found+1
            end
        end
    end
end


function NeedToKnow.ExecutiveFrame_ADDON_LOADED(addon)
    if ( addon == "NeedToKnow") then
        if ( not NeedToKnow_Visible ) then
            NeedToKnow_Visible = true
        end
        
        m_last_cast = {} -- [n] = { spell, target, serial }
        m_last_cast_head = 1
        m_last_cast_tail = 1
        m_last_guid = {} -- [spell][guidTarget] = { time, dur, expiry }
        NeedToKnow.totem_drops = {} -- array 1-4 of precise times the totems appeared
        NeedToKnow.weapon_enchants = { mhand = {}, ohand = {} }
        
        SlashCmdList["NEEDTOKNOW"] = NeedToKnow.SlashCommand
        SLASH_NEEDTOKNOW1 = "/needtoknow"
        SLASH_NEEDTOKNOW2 = "/ntk"
    end
end


function NeedToKnow.ExecutiveFrame_PLAYER_LOGIN()
    NeedToKnowLoader.SafeUpgrade()
    NeedToKnow.ExecutiveFrame_PLAYER_TALENT_UPDATE()
    NeedToKnow.guidPlayer = UnitGUID("player")

    local _, player_CLASS = UnitClass("player")
    if player_CLASS == "DEATHKNIGHT" then
        NeedToKnow.is_DK = 1
    elseif player_CLASS == "DRUID" then
        NeedToKnow.is_Druid = 1
    end

    NeedToKnowLoader.SetPowerTypeList(player_CLASS)

    NeedToKnow_ExecutiveFrame:RegisterEvent("PLAYER_TALENT_UPDATE")
    NeedToKnow_ExecutiveFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
    NeedToKnow_ExecutiveFrame:RegisterEvent("UNIT_TARGET")
    NeedToKnow_ExecutiveFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
    NeedToKnow_ExecutiveFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    NeedToKnow_ExecutiveFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    
    if ( NeedToKnow.is_DK ) then
        NeedToKnow.RegisterSpellcastSent();
    end
    NeedToKnow.Update()
    
    NeedToKnow_ExecutiveFrame:UnregisterEvent("PLAYER_LOGIN")
    NeedToKnow_ExecutiveFrame:UnregisterEvent("ADDON_LOADED")
    NeedToKnow.ExecutiveFrame_ADDON_LOADED = nil
    NeedToKnow.ExecutiveFrame_PLAYER_LOGIN = nil
    NeedToKnowLoader = nil

    NeedToKnow.RefreshRaidMemberNames()
    NeedToKnow.UpdateWeaponEnchants()
end

function NeedToKnow.RegisterSpellcastSent()
    if ( NeedToKnow.nRegisteredSent ) then
        NeedToKnow.nRegisteredSent = NeedToKnow.nRegisteredSent + 1
    else
        NeedToKnow.nRegisteredSent = 1
        NeedToKnow_ExecutiveFrame:RegisterEvent("UNIT_SPELLCAST_SENT")
    end
end

function NeedToKnow.UnregisterSpellcastSent()
    if ( NeedToKnow.nRegisteredSent ) then
        NeedToKnow.nRegisteredSent = NeedToKnow.nRegisteredSent - 1
        if ( 0 == NeedToKnow.nRegisteredSent ) then
            NeedToKnow.nRegisteredSent = nil
            NeedToKnow_ExecutiveFrame:UnregisterEvent("UNIT_SPELLCAST_SENT")
            NeedToKnow_ExecutiveFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
            NeedToKnow_ExecutiveFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        end
    end
end

function NeedToKnow.ExecutiveFrame_ACTIVE_TALENT_GROUP_CHANGED()
    -- This is the only event we're guaranteed to get on a talent switch,
    -- so we have to listen for it.  However, the client may not yet have
    -- the spellbook updates, so trying to evaluate the cooldows may fail.
    -- This is one of the reasons the cooldown logic has to fail silently
    -- and try again later
    NeedToKnow.ExecutiveFrame_PLAYER_TALENT_UPDATE()
end


function NeedToKnow.ExecutiveFrame_PLAYER_TALENT_UPDATE()
    if NeedToKnow.CharSettings then
        local spec = g_GetActiveTalentGroup()

        local profile_key = NeedToKnow.CharSettings.Specs[spec]
        if not profile_key then
            print("NeedToKnow: Switching to spec",spec,"for the first time")
            profile_key = NeedToKnow.CreateProfile(CopyTable(NEEDTOKNOW.PROFILE_DEFAULTS), spec)
        end
    
        NeedToKnow.ChangeProfile(profile_key);
    end
end


function NeedToKnow.ExecutiveFrame_UNIT_TARGET(unitTargeting)
    if m_bInCombat and not m_bCombatWithBoss then
        if UnitLevel(unitTargeting .. 'target') == -1 then
            m_bCombatWithBoss = true
            if NeedToKnow.BossStateBars then
                for bar, unused in pairs(NeedToKnow.BossStateBars) do
                    mfn_Bar_AuraCheck(bar)
                end
            end
        end
    end
end

function NeedToKnow.GetNameAndServer(unit)
  local name, server = UnitName(unit)
  if name and server then 
    return name .. '-' .. server
  end
  return name
end

function NeedToKnow.RefreshRaidMemberNames()
    NeedToKnow.raid_members = {}

    -- Note, if I did want to handle raid pets as well, they do not get the 
    -- server name decoration in the combat log as of 5.0.4
    if IsInRaid() then
        for i = 1, 40 do
            local unit = "raid"..i
            local name = NeedToKnow.GetNameAndServer(unit)
            if ( name ) then NeedToKnow.raid_members[name] = unit end
        end
    elseif IsInGroup() then
        for i = 1, 5 do
            local unit = "party"..i
            local name = NeedToKnow.GetNameAndServer(unit)
            if ( name ) then NeedToKnow.raid_members[name] = unit end
        end
    end

    -- Also get the player and their pet in directly
    -- (don't need NameAndServer since the player will always have a nil server.)
    local unit = "player"
    local name = UnitName(unit)
    NeedToKnow.raid_members[name] = unit

    unit = "pet"
    name = UnitName(unit)
    if ( name ) then
        NeedToKnow.raid_members[name] = unit
    end
end


function NeedToKnow.ExecutiveFrame_GROUP_ROSTER_UPDATE()
    NeedToKnow.RefreshRaidMemberNames();
end


function NeedToKnow.ExecutiveFrame_PLAYER_REGEN_DISABLED(unitTargeting)
    m_bInCombat = true
    m_bCombatWithBoss = false
    if IsInRaid() then
        for i = 1, 40 do
            if UnitLevel("raid"..i.."target") == -1 then
                m_bCombatWithBoss = true;
                break;
            end
        end
    elseif IsInGroup() then
        for i = 1, 5 do
            if UnitLevel("party"..i.."target") == -1 then
                m_bCombatWithBoss = true;
                break;
            end
        end
    elseif UnitLevel("target") == -1 then
        m_bCombatWithBoss = true
    end
    if NeedToKnow.BossStateBars then
        for bar, unused in pairs(NeedToKnow.BossStateBars) do
            mfn_Bar_AuraCheck(bar)
        end
    end
end


function NeedToKnow.ExecutiveFrame_PLAYER_REGEN_ENABLED(unitTargeting)
    m_bInCombat = false
    m_bCombatWithBoss = false
    if NeedToKnow.BossStateBars then
        for bar, unused in pairs(NeedToKnow.BossStateBars) do
            mfn_Bar_AuraCheck(bar)
        end
    end
end


function NeedToKnow.RemoveDefaultValues(t, def, k)
  if not k then k = "" end
  if def == nil then
    -- Some obsolete setting, or perhaps bUncompressed
    return true
  end
  -- Never want to compress name since it's read from inactive profiles
  -- Note: k was just for debugging, so it's got a leading space as part
  -- of how the debugging string was built.  This mechanism should probably
  -- be revisited.
  if type(t) ~= "table" then
    return ((k~=" name") and (t == def))
  end
  
  if #t > 0 then
    -- An array, like Groups or Bars. Compare each element against def[1]
    for i,v in ipairs(t)do
      local rhs = def[i]
      if rhs == nil then rhs = def[1] end
      if NeedToKnow.RemoveDefaultValues(v, rhs, k .. " " .. i) then
        t[i] = nil
      end
    end
  else
    for kT, vT in pairs(t) do
      if NeedToKnow.RemoveDefaultValues(t[kT], def[kT], k .. " " .. kT) then
        t[kT] = nil
      end
    end
  end
  local fn = pairs(t)
  return fn(t) == nil
end

function NeedToKnow.CompressProfile(profileSettings)
    -- Remove unused bars/groups
    for iG,vG in ipairs(profileSettings["Groups"]) do
        if iG > profileSettings.nGroups then
            profileSettings["Groups"][iG] = nil
        elseif vG.NumberBars then
            for iB, vB in ipairs(vG["Bars"]) do
                if iB > vG.NumberBars then
                    vG["Bars"][iB] = nil
                end
            end
        end
    end
    NeedToKnow.RemoveDefaultValues(profileSettings, NEEDTOKNOW.PROFILE_DEFAULTS);
end

-- DEBUG: remove k, it's just for debugging
function NeedToKnow.AddDefaultsToTable(t, def, k)
    if type(t) ~= "table" then return end
        if def == nil then
            return
        end
    if not k then k = "" end
    local n = table.maxn(t)
    if n > 0 then
        for i=1,n do
            local rhs = def[i]
            if rhs == nil then rhs = def[1] end
            if t[i] == nil then
                t[i] = NeedToKnow.DeepCopy(rhs)
            else
                NeedToKnow.AddDefaultsToTable(t[i], rhs, k .. " " .. i)
            end
        end
        else
        for kD,vD in pairs(def) do
            if t[kD] == nil then
                if type(vD) == "table" then
                    t[kD] = NeedToKnow.DeepCopy(vD)
                else
                    t[kD] = vD
                end
            else
                NeedToKnow.AddDefaultsToTable(t[kD], vD, k .. " " .. kD)
            end
        end
    end
end

function NeedToKnow.UncompressProfile(profileSettings)
    -- Make sure the arrays have the right number of elements so that
    -- AddDefaultsToTable will find them and fill them in
    if profileSettings.nGroups then
        if not profileSettings.Groups then
            profileSettings.Groups = {}
        end
        if not profileSettings.Groups[profileSettings.nGroups] then
            profileSettings.Groups[profileSettings.nGroups] = {}
        end
    end
    if profileSettings.Groups then
        for i,g in ipairs(profileSettings.Groups) do
            if g.NumberBars then
                if not g.Bars then
                    g.Bars = {}
                end
                if not g.Bars[g.NumberBars] then
                    g.Bars[g.NumberBars] = {}
                end
            end
        end
    end
        
    NeedToKnow.AddDefaultsToTable(profileSettings, NEEDTOKNOW.PROFILE_DEFAULTS)
    
    profileSettings.bUncompressed = true
end


function NeedToKnow.ChangeProfile(profile_key)
    if NeedToKnow_Profiles[profile_key] and
       NeedToKnow.ProfileSettings ~= NeedToKnow_Profiles[profile_key] then
        -- Compress the old profile by removing defaults
        if NeedToKnow.ProfileSettings and NeedToKnow.ProfileSettings.bUncompressed then
            NeedToKnow.CompressProfile(NeedToKnow.ProfileSettings)
        end

        -- Switch to the new profile
        NeedToKnow.ProfileSettings = NeedToKnow_Profiles[profile_key]
        local spec = g_GetActiveTalentGroup()
        NeedToKnow.CharSettings.Specs[spec] = profile_key

        -- fill in any missing defaults
        NeedToKnow.UncompressProfile(NeedToKnow.ProfileSettings)
        -- FIXME: We currently display 4 groups in the options UI, not nGroups
        -- FIXME: We don't handle nGroups changing (showing/hiding groups based on nGroups changing)
        -- Forcing 4 groups for now
        NeedToKnow.ProfileSettings.nGroups = 4
        for groupID = 1,4 do
            if ( nil == NeedToKnow.ProfileSettings.Groups[groupID] ) then
                NeedToKnow.ProfileSettings.Groups[groupID] = CopyTable( NEEDTOKNOW.GROUP_DEFAULTS )
                local groupSettings = NeedToKnow.ProfileSettings.Groups[groupID]
                groupSettings.Enabled = false;
                groupSettings.Position[4] = -100 - (groupID-1) * 100
            end
        end

        -- Hide any groups not in use
        local iGroup = NeedToKnow.ProfileSettings.nGroups + 1
        while true do
            local group = _G["NeedToKnow_Group"..iGroup]
            if not group then
                break
            end
            group:Hide()
            iGroup = iGroup + 1
        end
        
        -- Update the bars and options panel (if it's open)
        NeedToKnow.Update()
        NeedToKnowOptions.UIPanel_Update()
    elseif not NeedToKnow_Profiles[profile_key] then
        print("NeedToKnow profile",profile_key,"does not exist!") -- LOCME!
    end
end


mfn_SetStatusBarValue = function (bar,texture,value,value0)
  local pct0 = 0
  if value0 then
    pct0 = value0 / bar.max_value
    if pct0 > 1 then pct0 = 1 end
  end
  
  -- This happened to me when there was lag right around the time
  -- a bar was ending
  if value < 0 then
    value = 0
  end

  local pct = value / bar.max_value
  texture.cur_value = value
  if pct > 1 then pct = 1 end
  local w = (pct-pct0) * bar:GetWidth()
  if w < 1 then 
      texture:Hide()
  else
      texture:SetWidth(w)
      texture:SetTexCoord(pct0,0, pct0,1, pct,0, pct,1)
      texture:Show()
  end
end


function NeedToKnowLoader.Reset(bResetCharacter)
    NeedToKnow_Globals = CopyTable( NEEDTOKNOW.DEFAULTS )
    
    if bResetCharacter == nil or bResetCharacter then
        NeedToKnow.ResetCharacter()
    end
end


function NeedToKnow.ResetCharacter(bCreateSpecProfile)
    local charKey = UnitName("player") .. ' - ' .. GetRealmName(); 
    NeedToKnow_CharSettings = CopyTable(NEEDTOKNOW.CHARACTER_DEFAULTS)
    NeedToKnow.CharSettings = NeedToKnow_CharSettings
    if bCreateSpecProfile == nil or bCreateSpecProfile then
        NeedToKnow.ExecutiveFrame_PLAYER_TALENT_UPDATE()    
    end
end


function NeedToKnow.AllocateProfileKey()
    local n=NeedToKnow_Globals.NextProfile or 1
    while NeedToKnow_Profiles["G"..n] do
        n = n+1
    end
    if ( NeedToKnow_Globals.NextProfile==null or n >= NeedToKnow_Globals.NextProfile ) then
        NeedToKnow_Globals.NextProfile = n+1
    end
    return "G"..n;
end

function NeedToKnow.FindUnusedNumericSuffix(prefix, defPrefix)
    local suffix = defPrefix
    if not suffix then suffix = 1 end

    local candidate = prefix .. suffix
    while ( NeedToKnow.FindProfileByName(candidate) ) do 
        suffix = suffix + 1
        candidate = prefix .. suffix
    end
    return candidate;
end

function NeedToKnow.CreateProfile(settings, idxSpec, nameProfile)
    if not nameProfile then
        local prefix = UnitName("player") .. "-"..GetRealmName() .. "." 
        nameProfile = NeedToKnow.FindUnusedNumericSuffix(prefix, idxSpec)
    end
    settings.name = nameProfile

    local keyProfile
    for k,t in pairs(NeedToKnow_Globals.Profiles) do
        if t.name == nameProfile then
            keyProfile = k
            break;
        end
    end

    if not keyProfile then
        keyProfile = NeedToKnow.AllocateProfileKey()
    end

    if NeedToKnow_CharSettings.Profiles[keyProfile] then
        print("NeedToKnow: Clearing profile ",nameProfile); -- FIXME - Localization
    else
        print("NeedToKnow: Adding profile",nameProfile) -- FIXME - Localization
    end

    if idxSpec then
        NeedToKnow.CharSettings.Specs[idxSpec] = keyProfile
    end
    NeedToKnow_CharSettings.Profiles[keyProfile] = settings
    NeedToKnow_Profiles[keyProfile] = settings
    return keyProfile
end


function NeedToKnowLoader.RoundSettings(t)
  for k,v in pairs(t) do
    local typ = type(v)
    if typ == "number" then
      t[k] = tonumber(string.format("%0.4f",v))
    elseif typ == "table" then
      NeedToKnowLoader.RoundSettings(v)
    end
  end    
end


function NeedToKnowLoader.MigrateSpec(specSettings, idxSpec)
    if not specSettings or not specSettings.Groups or not specSettings.Groups[1] or not 
       specSettings.Groups[2] or not specSettings.Groups[3] or not specSettings.Groups[4] then
        return false
    end
    
    -- Round floats to 0.00001, since old versions left really stange values of
    -- BarSpacing and BarPadding around
    NeedToKnowLoader.RoundSettings(specSettings)
    specSettings.Spec = nil
    specSettings.Locked = nil
    specSettings.nGroups = 4
    specSettings.BarFont = NeedToKnowLoader.FindFontName(specSettings.BarFont)
    NeedToKnow.CreateProfile(specSettings, idxSpec)
    return true
end


function NeedToKnowLoader.MigrateCharacterSettings()
    print("NeedToKnow: Migrating settings from", NeedToKnow_Settings["Version"]);
    local oldSettings = NeedToKnow_Settings
    NeedToKnow.ResetCharacter(false)
    if ( not oldSettings["Spec"] ) then 
        NeedToKnow_Settings = nil 
        return 
    end

    -- Blink was controlled purely by the alpha of MissingBlink for awhile,
    -- But then I introduced an explicit blink_enabled variable.  Fill that in
    -- if it's missing
    for kS,vS in pairs(oldSettings["Spec"]) do
      for kG,vG in pairs(vS["Groups"]) do
        for kB,vB in pairs(vG["Bars"]) do
            if nil == vB.blink_enabled and vB.MissingBlink then
                vB.blink_enabled = vB.MissingBlink.a > 0
            end
        end
      end
    end

    NeedToKnow.CharSettings["Locked"] = oldSettings["Locked"]

    local bOK
    if ( oldSettings["Spec"] ) then -- The Spec member existed from versions 2.4 to 3.1.7
        for idxSpec = 1,2 do
            local newprofile = oldSettings.Spec[idxSpec]
            for kD,_ in pairs(NEEDTOKNOW.PROFILE_DEFAULTS) do
              if oldSettings[kD] then
                newprofile[kD] = oldSettings[kD]
              end
            end
            bOK = NeedToKnowLoader.MigrateSpec(newprofile, idxSpec)
        end
    -- if before dual spec support, copy old settings to both specs    
    elseif oldSettings["Version"] >= "2.0" and oldSettings["Groups"] then    
        bOK = NeedToKnowLoader.MigrateSpec(oldSettings, 1) and 
              NeedToKnowLoader.MigrateSpec(CopyTable(oldSettings), 2)

        -- save group positions if upgrading from version that used layout-local.txt
        if ( bOK and NeedToKnow_Settings.Version < "2.1" ) then    
            for groupID = 1, 4 do -- Prior to 3.2, there were always 4 groups
                NeedToKnow.SavePosition(_G["NeedToKnow_Group"..groupID], groupID)
            end
        end        
    end
        
    if not bOK then
        print("Old NeedToKnow character settings corrupted or not compatible with current version... starting from scratch")
        NeedToKnow.ResetCharacter()
    end
    NeedToKnow_Settings = nil
end


function NeedToKnowLoader.FindFontName(fontPath)
    local fontList = NeedToKnow.LSM:List("font")
    for i=1,#fontList do
        local fontName = fontList[i]
        local iPath = NeedToKnow.LSM:Fetch("font", fontName)
        if iPath == fontPath then
            return fontName
        end
    end
    return NEEDTOKNOW.PROFILE_DEFAULTS.BarFont
end

function NeedToKnowLoader.SafeUpgrade()
    local defPath = GameFontHighlight:GetFont()
    NEEDTOKNOW.PROFILE_DEFAULTS.BarFont = NeedToKnowLoader.FindFontName(defPath)
    NeedToKnow_Profiles = {}

    -- If there had been an error during the previous upgrade, NeedToKnow_Settings 
    -- may be in an inconsistent, halfway state.  
    if not NeedToKnow_Globals then
        NeedToKnowLoader.Reset(false)
    end

    if NeedToKnow_Settings then -- prior to 4.0
        NeedToKnowLoader.MigrateCharacterSettings()
    end
    if not NeedToKnow_CharSettings then
        -- we'll call talent update right after this, so we pass false now
        NeedToKnow.ResetCharacter(false)
    end
    NeedToKnow.CharSettings = NeedToKnow_CharSettings

    -- 4.0 settings sanity check 
    if not NeedToKnow_Globals or
       not NeedToKnow_Globals["Version"] or
       not NeedToKnow_Globals.Profiles
    then
        print("NeedToKnow settings corrupted, resetting")
        NeedToKnowLoader.Reset()
    end

    local maxKey = 0
    local aByName = {}
    for iS,vS in pairs(NeedToKnow_Globals.Profiles) do
        if vS.bUncompressed then
            NeedToKnow.CompressProfile(vS)
        end
        -- Although name should never be compressed, it could have been prior to 4.0.16
        if not vS.name then vS.name = "Default" end
        local cur = tonumber(iS:sub(2))
        if ( cur > maxKey ) then maxKey = cur end
        NeedToKnow_Profiles[iS] = vS
        if aByName[ vS.name ] then
            local renamed = NeedToKnow.FindUnusedNumericSuffix(vS.name, 2)
            print("Error! the profile name " .. vS.name .. " has been reused!  Renaming one of them to " .. renamed)
            vS.name = renamed;
        end
        aByName[vS.name] = vS
    end

    local aFixups = {}
    if NeedToKnow_CharSettings.Profiles then
        for iS,vS in pairs(NeedToKnow_CharSettings.Profiles) do
            -- Check for collisions by name
            if aByName[ vS.name ] then
                local renamed = NeedToKnow.FindUnusedNumericSuffix(vS.name, 2)
                print("Error! the profile name " .. vS.name .. " has been reused!  Renaming one of them to " .. renamed)
                vS.name = renamed;
            end
            aByName[vS.name] = vS

            -- Check for collisions by key
            if ( NeedToKnow_Profiles[iS] ) then
                print("NeedToKnow error encountered, both", vS.name, "and", NeedToKnow_Profiles[iS].name, "collided as " .. iS .. ".  Some specs may be mapped to one that should have been mapped to the other.");
                local oS = iS;
                iS = NeedToKnow.AllocateProfileKey();
                aFixups[oS] = iS
            end

            -- Although name should never be compressed, it could have been prior to 4.0.16
            if not vS.name then vS.name = "Default" end
            local cur = tonumber(iS:sub(2))
            if ( cur > maxKey ) then maxKey = cur end
            NeedToKnow_Profiles[iS] = vS
            local k = NeedToKnow.FindProfileByName(vS.name);
        end
    end

    -- fixup character profile collisions by key
    for oS,iS in pairs(aFixups) do
      NeedToKnow_CharSettings.Profiles[iS] = NeedToKnow_CharSettings.Profiles[oS]; 
      NeedToKnow_CharSettings.Profiles[oS] = nil; 
    end

    if ( not NeedToKnow_Globals.NextProfile or maxKey > NeedToKnow_Globals.NextProfile ) then
        print("Warning, NeedToKnow forgot how many profiles it had allocated.  New account profiles may hiccup when switching characters.")
        NeedToKnow_Globals.NextProfile = maxKey + 1
    end

    local spec = g_GetActiveTalentGroup()
    local curKey = NeedToKnow.CharSettings.Specs[spec]
    if ( curKey and not NeedToKnow_Profiles[curKey] ) then
        print("Current profile (" .. curKey .. ") has been deleted!");
        curKey = NeedToKnow.CreateProfile(CopyTable(NEEDTOKNOW.PROFILE_DEFAULTS), spec)
        local curProf = NeedToKnow_Profiles[curKey]
        NeedToKnow.CharSettings.Specs[spec] = curKey
    end
    

     -- TODO: check the required members for existence and delete any corrupted profiles
end


function NeedToKnowLoader.SetPowerTypeList(player_CLASS)
    if player_CLASS == "DRUID" or 
        player_CLASS == "MONK" 
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = "4", MenuText = NEEDTOKNOW.POWER_TYPES[4] } ) 
    end

    -- -1 - Combo Points
    if player_CLASS == "DRUID" or
       player_CLASS == "ROGUE" 
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
        { Setting = "-1", MenuText = NEEDTOKNOW.COMBO_POINTS } )
    end

    -- 0 - Mana
    if player_CLASS == "DRUID" or 
        player_CLASS == "MAGE" or
        player_CLASS == "PALADIN" or
        player_CLASS == "PRIEST" or
        player_CLASS == "SHAMAN" or
        player_CLASS == "WARLOCK" 
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
        { Setting = tostring(SPELL_POWER_MANA), MenuText = MANA } )
    end

    -- 1 - Rage
    if player_CLASS == "DRUID" or 
        player_CLASS == "WARRIOR" 
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_RAGE), MenuText = RAGE } )
    end

    -- 2 - Focus
    if player_CLASS == "HUNTER" 
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_FOCUS), MenuText = FOCUS } )
    end

    -- 3 - Energy
    if player_CLASS == "DRUID"  or 
        player_CLASS == "MONK" or 
        player_CLASS == "ROGUE"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_ENERGY), MenuText = ENERGY } )
    end

    -- 4 - HAPPINESS no longer used

    -- 5 - Runes These don't make sense as a bar, and UnitPower returns 0 anyway
    -- if player_CLASS == "DEATHKNIGHT"
    -- then
    --  table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
    --    { Setting = tostring(SPELL_POWER_RUNES), MenuText = RUNES } )
    --end

    -- 6 - Runic Power
    if player_CLASS == "DEATHKNIGHT"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_RUNIC_POWER), MenuText = RUNIC_POWER } )
    end

    -- 7 - Soul Shards for affliction
    if player_CLASS == "WARLOCK"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_SOUL_SHARDS), MenuText = SOUL_SHARDS } )
    end

    -- 8 - Eclipse for balance druids
    if player_CLASS == "DRUID"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_ECLIPSE), MenuText = ECLIPSE } )
    end

    -- 9 - Holy Power
    if player_CLASS == "PALADIN"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_HOLY_POWER), MenuText = HOLY_POWER } )
    end

    -- 10 - "Alternate" power, for various boss fights, useful for everybody
    table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
        { Setting = "10", MenuText = NEEDTOKNOW.ALTERNATE_POWER } )

    -- 11 - Dark Force, currently unused

    -- 12 - Monk Chi
    if player_CLASS == "MONK"
    then
    table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
        { Setting = tostring(SPELL_POWER_CHI), MenuText = CHI } )
    end

    -- 13 - Shadow Orbs for shadow priest
    if player_CLASS == "PRIEST"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_SHADOW_ORBS), MenuText = SHADOW_ORBS } )
    end

    -- 14 - Burning Embers for Destruction warlocks
    if player_CLASS == "WARLOCK"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_BURNING_EMBERS), MenuText = BURNING_EMBERS } )
    end

    -- 15 - Demonic Fury for demonology
    if player_CLASS == "WARLOCK"
    then
        table.insert(NeedToKnowRMB.BarMenu_SubMenus.PowerTypeList,
            { Setting = tostring(SPELL_POWER_DEMONIC_FURY), MenuText = DEMONIC_FURY } )
    end
end


function NeedToKnow.DeepCopy(object)
    if type(object) ~= "table" then
        return object
    else
        local new_table = {}
        for k,v in pairs(object) do
            new_table[k] = NeedToKnow.DeepCopy(v)
        end
        return new_table
    end
end


---- Copies anything (int, table, whatever).  Unlike DeepCopy (and CopyTable), CopyRefGraph can 
---- recreate a recursive reference structure (CopyTable will stack overflow.)
---- Copied from http://lua-users.org/wiki/CopyTable
--function NeedToKnow.CopyRefGraph(object)
    --local lookup_table = {}
    --local function _copy(object)
        --if type(object) ~= "table" then
            --return object
        --elseif lookup_table[object] then
            --return lookup_table[object]
        --end
        --local new_table = {}
        --lookup_table[object] = new_table
        --for index, value in pairs(object) do
            --new_table[_copy(index)] = _copy(value)
        --end
        --return setmetatable(new_table, getmetatable(object))
    --end
    --return _copy(object)
--end

function NeedToKnow.RestoreTableFromCopy(dest, source)
    for key,value in pairs(source) do
        if type(value) == "table" then
           if dest[key] then
               NeedToKnow.RestoreTableFromCopy(dest[key], value)
           else
               dest[key] = value
           end
        else
            dest[key] = value
        end
    end
    for key,value in pairs(dest) do
        if source[key] == nil then
            dest[key] = nil
        end
    end
end

function NeedToKnow.Update()
    if UnitExists("player") and NeedToKnow.ProfileSettings then
        NeedToKnow.UpdateWeaponEnchants()
        for groupID = 1, NeedToKnow.ProfileSettings.nGroups do
            NeedToKnow.Group_Update(groupID)
        end
    end
end


function NeedToKnow.Show(bShow)
    NeedToKnow_Visible = bShow
    for groupID = 1, NeedToKnow.ProfileSettings.nGroups do
        local groupName = "NeedToKnow_Group"..groupID
        local group = _G[groupName]
        local groupSettings = NeedToKnow.ProfileSettings.Groups[groupID]
        
        if (NeedToKnow_Visible and groupSettings.Enabled) then
            group:Show()
        else
            group:Hide()
        end
    end
end

do
    local executiveFrame = CreateFrame("Frame", "NeedToKnow_ExecutiveFrame")
    executiveFrame:SetScript("OnEvent", NeedToKnow.ExecutiveFrame_OnEvent)
    executiveFrame:RegisterEvent("ADDON_LOADED")
    executiveFrame:RegisterEvent("PLAYER_LOGIN")
end



-- ------
-- GROUPS
-- ------

function NeedToKnow.Group_Update(groupID)
    local groupName = "NeedToKnow_Group"..groupID
    local group = _G[groupName]
    local groupSettings = NeedToKnow.ProfileSettings.Groups[groupID]

    local bar
    for barID = 1, groupSettings.NumberBars do
        local barName = groupName.."Bar"..barID
        bar = _G[barName] or CreateFrame("Frame", barName, group, "NeedToKnow_BarTemplate")
        bar:SetID(barID)

        if ( barID > 1 ) then
            bar:SetPoint("TOP", _G[groupName.."Bar"..(barID-1)], "BOTTOM", 0, -NeedToKnow.ProfileSettings.BarSpacing)
        else
            bar:SetPoint("TOPLEFT", group, "TOPLEFT")
        end

        NeedToKnow.Bar_Update(groupID, barID)

        if ( not groupSettings.Enabled ) then
            NeedToKnow.ClearScripts(bar)
        end
    end

    local resizeButton = _G[groupName.."ResizeButton"]
    resizeButton:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 8, -8)

    local barID = groupSettings.NumberBars+1
    while true do
        bar = _G[groupName.."Bar"..barID]
        if bar then
            bar:Hide()
            NeedToKnow.ClearScripts(bar)
            barID = barID + 1
        else
            break
        end
    end

    if ( NeedToKnow.CharSettings["Locked"] ) then
        resizeButton:Hide()
    else
        resizeButton:Show()
    end

    -- Early enough in the loading process (before PLAYER_LOGIN), we might not
    -- know the position yet
    if groupSettings.Position then
        group:ClearAllPoints()
        local point, relativePoint, xOfs, yOfs = unpack(groupSettings.Position)
        group:SetPoint(point, UIParent, relativePoint, xOfs, yOfs)
        group:SetScale(groupSettings.Scale)
    end
    
    if ( NeedToKnow_Visible and groupSettings.Enabled ) then
        group:Show()
    else
        group:Hide()
    end
end



-- ----
-- BARS
-- ----

-- Attempt to figure out if a name is an item or a spell, and if a spell
-- try to choose a spell with that name that has a cooldown
-- This may fail for valid names if the client doesn't have the data for
-- that spell yet (just logged in or changed talent specs), in which case 
-- we mark that spell to try again later
function NeedToKnow.SetupSpellCooldown(bar, entry)
    local id = entry.id
    local name = entry.name
    local idx = entry.idxName
    if not id then
        if ( name == "Auto Shot" or
             name == c_AUTO_SHOT_NAME ) 
        then
            bar.settings.bAutoShot = true
            bar.cd_functions[idx] = mfn_GetAutoShotCooldown
        else
            local item_id = NeedToKnow.GetItemIDString(name)
            if item_id then
                entry.id = item_id
                entry.name = nil
                bar.cd_functions[idx] = NeedToKnow.GetItemCooldown
            else
                local betterSpellID
                betterSpellID = NeedToKnow.TryToFindSpellWithCD(name)
                if nil ~= betterSpell then
                    entry.id = betterSpell
                    entry.name = nil
                    bar.cd_functions[idx] = mfn_GetSpellCooldown
                elseif not GetSpellCooldown(name) then
                    bar.cd_functions[idx] = mfn_GetUnresolvedCooldown
                else
                    bar.cd_functions[idx] = mfn_GetSpellCooldown
                end

                if ( bar.cd_functions[idx] == mfn_GetSpellCooldown ) then
                    local key = entry.id or entry.name
                    if ( bar.settings.show_charges and GetSpellCharges(key) ) then
                        bar.cd_functions[idx] = mfn_GetSpellChargesCooldown
                    end
                end
            end
        end
    end
end

-- Called when the configuration of the bar has changed, when the addon
-- is loaded or when ntk is locked and unlocked
function NeedToKnow.Bar_Update(groupID, barID)
    local groupSettings = NeedToKnow.ProfileSettings.Groups[groupID]

    local barName = "NeedToKnow_Group"..groupID.."Bar"..barID
    local bar = _G[barName]
    if not bar then
        -- New bar added in the UI; need to create it!
        local group = _G["NeedToKnow_Group"..groupID]
        bar = CreateFrame("Button", barName, group, "NeedToKnow_BarTemplate")
        if barID > 1 then
            bar:SetPoint("TOPLEFT", "NeedToKnow_Group"..groupID.."Bar"..(barID-1), "BOTTOMLEFT", 0, 0)
        else
            bar:SetPoint("TOPLEFT", "NeedToKnow_Group"..groupID, "TOPLEFT")
        end
        bar:SetPoint("RIGHT", group, "RIGHT", 0, 0)
        --trace("Creating bar for", groupID, barID)
    end

    local background = _G[barName.."Background"]
    bar.spark = _G[barName.."Spark"]
    bar.text = _G[barName.."Text"]
    bar.time = _G[barName.."Time"]
    bar.bar1 = _G[barName.."Texture"]

    local barSettings = groupSettings["Bars"][barID]
    if not barSettings then
        --trace("Adding bar settings for", groupID, barID)
        barSettings = CopyTable(NEEDTOKNOW.BAR_DEFAULTS)
        groupSettings.Bars[barID] = CopyTable(NEEDTOKNOW.BAR_DEFAULTS)
    end
    bar.auraName = barSettings.AuraName
    
    if ( barSettings.BuffOrDebuff == "BUFFCD" or
         barSettings.BuffOrDebuff == "TOTEM" or
         barSettings.BuffOrDebuff == "USABLE" or
         barSettings.BuffOrDebuff == "EQUIPSLOT" or
         barSettings.BuffOrDebuff == "CASTCD") 
    then
        barSettings.Unit = "player"
    end

    bar.settings = barSettings
    bar.unit = barSettings.Unit
    bar.nextUpdate = g_GetTime() + c_UPDATE_INTERVAL

    bar.fixedDuration = tonumber(groupSettings.FixedDuration)
    if ( not bar.fixedDuration or 0 >= bar.fixedDuration ) then
        bar.fixedDuration = nil
    end

    bar.max_value = 1
    mfn_SetStatusBarValue(bar,bar.bar1,1)
    bar.bar1:SetTexture(NeedToKnow.LSM:Fetch("statusbar", NeedToKnow.ProfileSettings["BarTexture"]))
    if ( bar.bar2 ) then
        bar.bar2:SetTexture(NeedToKnow.LSM:Fetch("statusbar", NeedToKnow.ProfileSettings["BarTexture"]))
    end
    local fontPath = NeedToKnow.LSM:Fetch("font", NeedToKnow.ProfileSettings["BarFont"])
    if ( fontPath ) then
        local ol = NeedToKnow.ProfileSettings["FontOutline"]
        if ( ol == 0 ) then
          ol = nil
        elseif (ol == 1) then
          ol = "OUTLINE"
        else
          ol = "THICKOUTLINE"
        end

        bar.text:SetFont(fontPath, NeedToKnow.ProfileSettings["FontSize"],ol)
        bar.time:SetFont(fontPath, NeedToKnow.ProfileSettings["FontSize"],ol)
    end
    
    bar:SetWidth(groupSettings.Width)
    bar.text:SetWidth(groupSettings.Width-60)
    NeedToKnow.SizeBackground(bar, barSettings.show_icon)

    background:SetHeight(bar:GetHeight() + 2*NeedToKnow.ProfileSettings["BarPadding"])
    background:SetVertexColor(unpack(NeedToKnow.ProfileSettings["BkgdColor"]))

    -- Set up the Visual Cast Time overlay.  It isn't a part of the template 
    -- because most bars won't use it and thus don't need to pay the cost of
    -- a hidden frame
    if ( barSettings.vct_enabled ) then
        if ( nil == bar.vct ) then
            bar.vct = bar:CreateTexture(barName.."VisualCast", "ARTWORK")
            bar.vct:SetPoint("TOPLEFT", bar, "TOPLEFT")
        end
        local argb = barSettings.vct_color
        bar.vct:SetTexture(argb.r, argb.g, argb.b, argb.a )
        bar.vct:SetBlendMode("ADD")
        bar.vct:SetHeight(bar:GetHeight())
    elseif (nil ~= bar.vct) then
        bar.vct:Hide()
    end
    
    if ( barSettings.show_icon ) then
        if ( not bar.icon ) then
            bar.icon = bar:CreateTexture(bar:GetName().."Icon", "ARTWORK")
        end
        local size = bar:GetHeight()
        bar.icon:SetWidth(size)
        bar.icon:SetHeight(size)
        bar.icon:ClearAllPoints()
        bar.icon:SetPoint("TOPRIGHT", bar, "TOPLEFT", -NeedToKnow.ProfileSettings["BarPadding"], 0)
        bar.icon:Show()
    elseif (bar.icon) then
        bar.icon:Hide()
    end

    if ( NeedToKnow.CharSettings["Locked"] ) then
        local enabled = groupSettings.Enabled and barSettings.Enabled
        if enabled then
            -- Set up the bar to be functional
            -- click through
            bar:EnableMouse(0)

            -- Split the spell names    
            bar.spells = {}
            bar.cd_functions = {}
            local iSpell = 0
            for barSpell in bar.auraName:gmatch("([^,]+)") do
                iSpell = iSpell+1
                barSpell = strtrim(barSpell)
                local _, nDigits = barSpell:find("^-?%d+")
                if ( nDigits == barSpell:len() ) then
                    table.insert(bar.spells, { idxName=iSpell, id=tonumber(barSpell) } )
                else
                    table.insert(bar.spells, { idxName=iSpell, name=barSpell } )
                end
            end

            -- split the user name overrides
            bar.spell_names = {}
            for un in barSettings.show_text_user:gmatch("([^,]+)") do
                un = strtrim(un)
                table.insert(bar.spell_names, un)
            end

            -- split the "reset" spells (for internal cooldowns which reset when the player gains an aura)
            if barSettings.buffcd_reset_spells and barSettings.buffcd_reset_spells ~= "" then
                bar.reset_spells = {}
                bar.reset_start = {}
                iSpell = 0
                for resetSpell in barSettings.buffcd_reset_spells:gmatch("([^,]+)") do
                    iSpell = iSpell+1
                    resetSpell = strtrim(resetSpell)
                    local _, nDigits = resetSpell:find("^%d+")
                    if ( nDigits == resetSpell:len() ) then
                        table.insert(bar.reset_spells, { idxName = iSpell, id=tonumber(resetSpell) } )
                    else
                        table.insert(bar.reset_spells, { idxName = iSpell, name=resetSpell} )
                    end
                    table.insert(bar.reset_start, 0)
                end
            else
                bar.reset_spells = nil
                bar.reset_start = nil
            end

            barSettings.bAutoShot = nil
            bar.is_counter = nil
            bar.ticker = NeedToKnow.Bar_OnUpdate
            
            -- Determine which helper functions to use
            if     "BUFFCD" == barSettings.BuffOrDebuff then
                bar.fnCheck = mfn_AuraCheck_BUFFCD
            elseif "TOTEM" == barSettings.BuffOrDebuff then
                bar.fnCheck = mfn_AuraCheck_TOTEM
            elseif "USABLE" == barSettings.BuffOrDebuff then
                bar.fnCheck = mfn_AuraCheck_USABLE
            elseif "EQUIPSLOT" == barSettings.BuffOrDebuff then
                bar.fnCheck = mfn_AuraCheck_EQUIPSLOT
            elseif "POWER" == barSettings.BuffOrDebuff then
                bar.fnCheck = mfn_AuraCheck_POWER
                bar.is_counter = true
                bar.ticker = nil
                bar.ticking = false
            elseif "CASTCD" == barSettings.BuffOrDebuff then
                bar.fnCheck = mfn_AuraCheck_CASTCD
                for idx, entry in ipairs(bar.spells) do
                    table.insert(bar.cd_functions, mfn_GetSpellCooldown)
                    NeedToKnow.SetupSpellCooldown(bar, entry)
                end
            elseif "mhand" == barSettings.Unit or "ohand" == barSettings.Unit then
                bar.fnCheck = mfn_AuraCheck_Weapon
            elseif barSettings.show_all_stacks then
                bar.fnCheck = mfn_AuraCheck_AllStacks
            else
                bar.fnCheck = mfn_AuraCheck_Single
            end
        
            if ( barSettings.BuffOrDebuff == "BUFFCD" ) then
                local dur = tonumber(barSettings.buffcd_duration)
                if (not dur or dur < 1) then
                    print("Internal cooldown bar watching",barSettings.AuraName,"did not set a cooldown duration.  Disabling the bar")
                    enabled = false
                end
            end
        
            NeedToKnow.SetScripts(bar)
            -- Events were cleared while unlocked, so need to check the bar again now
            mfn_Bar_AuraCheck(bar)
        else
            NeedToKnow.ClearScripts(bar)
            bar:Hide()
        end
    else
        NeedToKnow.ClearScripts(bar)
        -- Set up the bar to be configured
        bar:EnableMouse(1)

        bar.bar1:SetVertexColor(barSettings.BarColor.r, barSettings.BarColor.g, barSettings.BarColor.b)
        bar.bar1:SetAlpha(barSettings.BarColor.a)
        bar:Show()
        bar.spark:Hide()
        bar.time:Hide()
        if ( bar.icon ) then
            bar.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        end
        if ( bar.vct ) then
            bar.vct:SetWidth( bar:GetWidth() / 16)
            bar.vct:Show()
        end
        if ( bar.bar2 ) then
            bar.bar2:Hide()
        end
        
        local txt=""
        if ( barSettings.show_mypip ) then
            txt = txt.."* "
        end

        if ( barSettings.show_text ) then
            if "" ~= barSettings.show_text_user then
                txt = barSettings.show_text_user
            else
                txt = txt .. NeedToKnow.PrettyName(barSettings)
            end

            if ( barSettings.append_cd
                 and (barSettings.BuffOrDebuff == "CASTCD"
                   or barSettings.BuffOrDebuff == "BUFFCD"
                   or barSettings.BuffOrDebuff == "EQUIPSLOT" ) )
            then
                txt = txt .. " CD"
            elseif ( barSettings.append_usable
                 and barSettings.BuffOrDebuff == "USABLE" )
            then
                txt = txt .. " Usable"
            end
            if ( barSettings.bDetectExtends == true ) then
                txt = txt .. " + 3s"
            end
        end
        bar.text:SetText(txt)

        if ( barSettings.Enabled ) then
            bar:SetAlpha(1)
        else
            bar:SetAlpha(0.4)
        end
    end
end


function NeedToKnow.CheckCombatLogRegistration(bar, force)
    if UnitExists(bar.unit) then
        bar:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
    else
        bar:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
    end
end


function NeedToKnow.SetScripts(bar)
    bar:SetScript("OnEvent", NeedToKnow.Bar_OnEvent)
 
    if ( bar.ticker ) then
        bar:SetScript("OnUpdate", bar.ticker)
    end
    if ( "TOTEM" == bar.settings.BuffOrDebuff ) then
        bar:RegisterEvent("PLAYER_TOTEM_UPDATE")
    elseif ( "CASTCD" == bar.settings.BuffOrDebuff ) then
        if ( bar.settings.bAutoShot ) then
            bar:RegisterEvent("START_AUTOREPEAT_SPELL")
            bar:RegisterEvent("STOP_AUTOREPEAT_SPELL")
        end
        bar:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN")
        bar:RegisterEvent("SPELL_UPDATE_COOLDOWN")
    elseif ( "EQUIPSLOT" == bar.settings.BuffOrDebuff ) then
        bar:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN")
    elseif ( "POWER" == bar.settings.BuffOrDebuff ) then
        if bar.settings.AuraName == "-1" then
          bar:RegisterEvent("UNIT_COMBO_POINTS")
        else
          bar:RegisterEvent("UNIT_POWER")
          bar:RegisterEvent("UNIT_DISPLAYPOWER")
        end
    elseif ( "USABLE" == bar.settings.BuffOrDebuff ) then
        bar:RegisterEvent("SPELL_UPDATE_USABLE")
    elseif ( "mhand" == bar.settings.Unit or "ohand" == bar.settings.Unit ) then
        bar:RegisterEvent("UNIT_INVENTORY_CHANGED")
    elseif ( bar.settings.Unit == "targettarget" ) then
        -- WORKAROUND: PLAYER_TARGET_CHANGED happens immediately, UNIT_TARGET every couple seconds
        bar:RegisterEvent("PLAYER_TARGET_CHANGED")
        bar:RegisterEvent("UNIT_TARGET")
        -- WORKAROUND: Don't get UNIT_AURA for targettarget
        NeedToKnow.CheckCombatLogRegistration(bar)
    else
        bar:RegisterEvent("UNIT_AURA")
    end

    if ( bar.unit == "focus" ) then
        bar:RegisterEvent("PLAYER_FOCUS_CHANGED")
    elseif ( bar.unit == "target" ) then
        bar:RegisterEvent("PLAYER_TARGET_CHANGED")
    elseif ( bar.unit == "pet" ) then
        bar:RegisterEvent("UNIT_PET")
    elseif ( "lastraid" == bar.settings.Unit ) then
        if ( not NeedToKnow.BarsForPSS ) then
            NeedToKnow.BarsForPSS = {}
        end
        NeedToKnow.BarsForPSS[bar] = true
        NeedToKnow.RegisterSpellcastSent()
    end
    
    if bar.settings.bDetectExtends then
        local idx,entry
        for idx, entry in ipairs(bar.spells) do
            local spellName
            if ( entry.id ) then
                spellName = GetSpellInfo(entry.id)
            else
                spellName = entry.name
            end
            if spellName then
                local r = m_last_guid[spellName]
                if not r then
                    m_last_guid[spellName] = { time=0, dur=0, expiry=0 }
                end
            else
                print("Warning! NTK could not get name for ", entry.id)
            end
        end
        NeedToKnow.RegisterSpellcastSent()
    end
    if bar.settings.blink_enabled and bar.settings.blink_boss then
        if not NeedToKnow.BossStateBars then
            NeedToKnow.BossStateBars = {}
        end
        NeedToKnow.BossStateBars[bar] = 1;
    end
end

function NeedToKnow.ClearScripts(bar)
    bar:SetScript("OnEvent", nil)
    bar:SetScript("OnUpdate", nil)
    bar:UnregisterEvent("PLAYER_TARGET_CHANGED")
    bar:UnregisterEvent("PLAYER_FOCUS_CHANGED")
    bar:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
    bar:UnregisterEvent("PLAYER_TOTEM_UPDATE")
    bar:UnregisterEvent("UNIT_AURA")
    bar:UnregisterEvent("UNIT_COMBO_POINTS")
    bar:UnregisterEvent("UNIT_POWER")
    bar:UnregisterEvent("UNIT_DISPLAYPOWER")
    bar:UnregisterEvent("UNIT_INVENTORY_CHANGED")
    bar:UnregisterEvent("UNIT_TARGET")
    bar:UnregisterEvent("START_AUTOREPEAT_SPELL")
    bar:UnregisterEvent("STOP_AUTOREPEAT_SPELL")
    bar:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
    if NeedToKnow.BossStateBars then
        NeedToKnow.BossStateBars[bar] = nil;
    end

    if bar.settings.bDetectExtends then
        NeedToKnow.UnregisterSpellcastSent()
    end
    if NeedToKnow.BarsForPSS and NeedToKnow.BarsForPSS[bar] then
        NeedToKnow.BarsForPSS[bar] = nil
        if nil == next(NeedToKnow.BarsForPSS) then
            NeedToKnow.BarsForPSS = nil
            NeedToKnow.UnregisterSpellcastSent();
        end
    end
end

function NeedToKnow.Bar_OnMouseUp(self, button)
    if ( button == "RightButton" ) then
        PlaySound("UChatScrollButton");
        NeedToKnowRMB.ShowMenu(self);
     end
end

function NeedToKnow.Bar_OnSizeChanged(self)
    if (self.bar1.cur_value) then mfn_SetStatusBarValue(self, self.bar1, self.bar1.cur_value) end
    if (self.bar2 and self.bar2.cur_value) then mfn_SetStatusBarValue(self, self.bar2, self.bar2.cur_value, self.bar1.cur_value) end
end




-- AuraCheck calls on this to compute the "text" of the bar
-- It is separated out like this in part to be hooked by other addons
function NeedToKnow.ComputeBarText(buffName, count, extended, buff_stacks, bar)
    local text
    if ( count > 1 ) then
        text = buffName.."  ["..count.."]"
    else
        text = buffName
    end

    if ( bar.settings.show_ttn1 and buff_stacks.total_ttn[1] > 0 ) then
        text = text .. " ("..buff_stacks.total_ttn[1]..")"
    end
    if ( bar.settings.show_ttn2 and buff_stacks.total_ttn[2] > 0 ) then
        text = text .. " ("..buff_stacks.total_ttn[2]..")"
    end
    if ( bar.settings.show_ttn3 and buff_stacks.total_ttn[3] > 0 ) then
        text = text .. " ("..buff_stacks.total_ttn[3]..")"
    end
    if ( extended and extended > 1 ) then
        text = text .. string.format(" + %.0fs", extended)
    end
    return text
end

-- Called by mfn_UpdateVCT, which is called from AuraCheck and possibly 
-- by Bar_Update depending on vct_refresh. In addition to refactoring out some 
-- code from the long AuraCheck, this also provides a convenient hook for other addons
function NeedToKnow.ComputeVCTDuration(bar)
    local vct_duration = 0
    
    local spellToTime = bar.settings.vct_spell
    if ( nil == spellToTime or "" == spellToTime ) then
        spellToTime = bar.buffName
    end
    local _, _, _, _, _, _, castTime = GetSpellInfo(spellToTime)

    if ( castTime ) then
        vct_duration = castTime / 1000
        bar.vct_refresh = true
    else
        bar.vct_refresh = false
    end
    
    if ( bar.settings.vct_extra ) then
        vct_duration =  vct_duration + bar.settings.vct_extra
    end
    return vct_duration
end

mfn_UpdateVCT = function (bar)
    local vct_duration = NeedToKnow.ComputeVCTDuration(bar)

    local dur = bar.fixedDuration or bar.duration
    if ( dur ) then
        vct_width =  (vct_duration * bar:GetWidth()) / dur
        if (vct_width > bar:GetWidth()) then
            vct_width = bar:GetWidth() 
        end
    else
        vct_width = 0
    end

    if ( vct_width > 1 ) then
        bar.vct:SetWidth(vct_width)
        bar.vct:Show()
    else
        bar.vct:Hide()
    end
end

function NeedToKnow.SizeBackground(bar, i_show_icon)
    local background = _G[bar:GetName() .. "Background"]
    local bgWidth = bar:GetWidth() + 2*NeedToKnow.ProfileSettings["BarPadding"]
    local y = NeedToKnow.ProfileSettings["BarPadding"]
    local x = -y
    background:ClearAllPoints()

    if ( i_show_icon ) then
        local iconExtra = bar:GetHeight() + NeedToKnow.ProfileSettings["BarPadding"]
        bgWidth = bgWidth + iconExtra
        x = x - iconExtra
    end
    background:SetWidth(bgWidth)
    background:SetPoint("TOPLEFT", bar, "TOPLEFT", x, y)
end

function NeedToKnow.CreateBar2(bar)
    if ( not bar.bar2 ) then
        local n = bar:GetName() .. "Bar2"
        bar.bar2 = bar:CreateTexture(n, "BORDER")
        bar.bar2:SetPoint("TOPLEFT", bar.bar1, "TOPRIGHT")
        bar.bar2:SetPoint("BOTTOM", bar, "BOTTOM")
        bar.bar2:SetWidth(bar:GetWidth())
    end
end

function NeedToKnow.PrettyName(barSettings)
    if ( barSettings.BuffOrDebuff == "EQUIPSLOT" ) then
        local idx = tonumber(barSettings.AuraName)
        if idx then return NEEDTOKNOW.ITEM_NAMES[idx] end
        return ""
    elseif ( barSettings.BuffOrDebuff == "POWER" ) then
        local idx = tonumber(barSettings.AuraName)
        if idx then return NEEDTOKNOW.POWER_TYPES[idx] end
        return ""
    else
        return barSettings.AuraName
    end
end

function NeedToKnow.ConfigureVisibleBar(bar, count, extended, buff_stacks)
    local text = ""
    if ( bar.settings.show_icon and bar.iconPath and bar.icon ) then
        bar.icon:SetTexture(bar.iconPath)
        bar.icon:Show()
        NeedToKnow.SizeBackground(bar, true)
    elseif bar.icon then
        bar.icon:Hide()
        NeedToKnow.SizeBackground(bar, false)
    end

    bar.bar1:SetVertexColor(bar.settings.BarColor.r, bar.settings.BarColor.g, bar.settings.BarColor.b)
    bar.bar1:SetAlpha(bar.settings.BarColor.a)
    if ( bar.max_expirationTime and bar.max_expirationTime ~= bar.expirationTime ) then 
        NeedToKnow.CreateBar2(bar)
        bar.bar2:SetTexture(bar.bar1:GetTexture())
        bar.bar2:SetVertexColor(bar.settings.BarColor.r, bar.settings.BarColor.g, bar.settings.BarColor.b)
        bar.bar2:SetAlpha(bar.settings.BarColor.a * 0.5)
        bar.bar2:Show()
    elseif (bar.bar2) then
        bar.bar2:Hide()
    end
    
    local txt = ""
    if ( bar.settings.show_mypip ) then
        txt = txt .. "* "
    end

    local n = ""
    if ( bar.settings.show_text ) then
        n = bar.buffName
        if "" ~= bar.settings.show_text_user then
            local idx=bar.idxName
            if idx > #bar.spell_names then idx = #bar.spell_names end
            n = bar.spell_names[idx]
        end
    end

    local c = count
    if not bar.settings.show_count then
        c = 1
    end
    local to_append = NeedToKnow.ComputeBarText(n, c, extended, buff_stacks, bar)
    if to_append and to_append ~= "" then
        txt = txt .. to_append
    end

    if ( bar.settings.append_cd 
         and (bar.settings.BuffOrDebuff == "CASTCD" 
           or bar.settings.BuffOrDebuff == "BUFFCD"
           or bar.settings.BuffOrDebuff == "EQUIPSLOT" ) ) 
    then
        txt = txt .. " CD"
    elseif (bar.settings.append_usable and bar.settings.BuffOrDebuff == "USABLE" ) then
        txt = txt .. " Usable"
    end
    bar.text:SetText(txt)
        
    -- Is this an aura with a finite duration?
    local vct_width = 0
    if ( not bar.is_counter and bar.duration > 0 ) then
        -- Configure the main status bar
        local duration = bar.fixedDuration or bar.duration
        bar.max_value = duration

        -- Determine the size of the visual cast bar
        if ( bar.settings.vct_enabled ) then
            mfn_UpdateVCT(bar)
        end
        
        -- Force an update to get all the bars to the current position (sharing code)
        -- This will call UpdateVCT again, but that seems ok
        bar.nextUpdate = -c_UPDATE_INTERVAL
        if bar.expirationTime > g_GetTime() then
            NeedToKnow.Bar_OnUpdate(bar, 0)
        end

        bar.time:Show()
    elseif bar.is_counter then
        bar.max_value = 1
        local pct = buff_stacks.total_ttn[1] / buff_stacks.total_ttn[2]
        mfn_SetStatusBarValue(bar,bar.bar1,pct)
        if bar.bar2 then mfn_SetStatusBarValue(bar,bar.bar2,pct) end

        bar.time:Hide()
        bar.spark:Hide()

        if ( bar.vct ) then
            bar.vct:Hide()
        end
    else
        -- Hide the time text and spark for auras with "infinite" duration
        bar.max_value = 1

        mfn_SetStatusBarValue(bar,bar.bar1,1)
        if bar.bar2 then mfn_SetStatusBarValue(bar,bar.bar2,1) end

        bar.time:Hide()
        bar.spark:Hide()

        if ( bar.vct ) then
            bar.vct:Hide()
        end
    end
end

function NeedToKnow.ConfigureBlinkingBar(bar)
    local settings = bar.settings
    if ( not bar.blink ) then
        bar.blink=true
        bar.blink_phase=1
        bar.bar1:SetVertexColor(settings.MissingBlink.r, settings.MissingBlink.g, settings.MissingBlink.b)
        bar.bar1:SetAlpha(settings.MissingBlink.a)
    end
    bar.text:SetText(settings.blink_label)
    bar.time:Hide()
    bar.spark:Hide()
    bar.max_value = 1
    mfn_SetStatusBarValue(bar,bar.bar1,1)
    
    if ( bar.icon ) then
        bar.icon:Hide()
        NeedToKnow.SizeBackground(bar, false)
    end
    if ( bar.bar2 ) then
        bar.bar2:Hide()
    end
end

function NeedToKnow.GetUtilityTooltips()
    if ( not NeedToKnow_Tooltip1 ) then
        for idxTip = 1,2 do
            local ttname = "NeedToKnow_Tooltip"..idxTip
            local tt = CreateFrame("GameTooltip", ttname)
            tt:SetOwner(UIParent, "ANCHOR_NONE")
            tt.left = {}
            tt.right = {}
            -- Most of the tooltip lines share the same text widget,
            -- But we need to query the third one for cooldown info
            for i = 1, 30 do
                tt.left[i] = tt:CreateFontString()
                tt.left[i]:SetFontObject(GameFontNormal)
                if i < 5 then
                    tt.right[i] = tt:CreateFontString()
                    tt.right[i]:SetFontObject(GameFontNormal)
                    tt:AddFontStrings(tt.left[i], tt.right[i])
                else
                    tt:AddFontStrings(tt.left[i], tt.right[4])
                end
            end 
         end
    end
    local tt1,tt2 = NeedToKnow_Tooltip1, NeedToKnow_Tooltip2
    
    tt1:ClearLines()
    tt2:ClearLines()
    return tt1,tt2
end

function NeedToKnow.DetermineTempEnchantFromTooltip(i_invID)
    local tt1,tt2 = NeedToKnow.GetUtilityTooltips()
    
    tt1:SetInventoryItem("player", i_invID)
    local n,h = tt1:GetItem()

    tt2:SetHyperlink(h)
    
    -- Look for green lines present in tt1 that are missing from tt2
    local nLines1, nLines2 = tt1:NumLines(), tt2:NumLines()
    local i1, i2 = 1,1
    while ( i1 <= nLines1 ) do
        local txt1 = tt1.left[i1]
        if ( txt1:GetTextColor() ~= 0 ) then
            i1 = i1 + 1
        elseif ( i2 <= nLines2 ) then
            local txt2 = tt2.left[i2]
            if ( txt2:GetTextColor() ~= 0 ) then
                i2 = i2 + 1
            elseif (txt1:GetText() == txt2:GetText()) then
                i1 = i1 + 1
                i2 = i2 + 1
            else
                break
            end
        else
            break
        end
    end
    if ( i1 <= nLines1 ) then
        local line = tt1.left[i1]:GetText()
        local paren = line:find("[(]")
        if ( paren ) then
            line = line:sub(1,paren-2)
        end
        return line
    end    
end



-- Looks at the tooltip for the given spell to see if a cooldown 
-- is listed with a duration in seconds.  Longer cooldowns don't
-- need this logic, so we don't need to do unit conversion
function NeedToKnow.DetermineShortCooldownFromTooltip(spell)
    if not NeedToKnow.short_cds then
        NeedToKnow.short_cds = {}
    end
    if not NeedToKnow.short_cds[spell] then
        -- Figure out what a cooldown in seconds should look like
        local ref = SecondsToTime(10):lower()
        local unit_ref = ref:match("10 (.+)")

        -- Get the number and unit of the cooldown from the tooltip
        local tt1 = NeedToKnow.GetUtilityTooltips()
        local lnk = GetSpellLink(spell)
        local cd, n_cd, unit_cd
        if lnk and lnk ~= "" then
            tt1:SetHyperlink( lnk )
            
            for iTT=3,2,-1 do
                cd = tt1.right[iTT]:GetText()
                if cd then 
                    cd = cd:lower()
                    n_cd, unit_cd = cd:match("(%d+) (.+) ")
                end
                if n_cd then break end
            end
        end

        -- unit_ref will be "|4sec:sec;" in english, so do a find rather than a ==
        if not n_cd then 
            -- If we couldn't parse the tooltip, assume there's no cd
            NeedToKnow.short_cds[spell] = 0
        elseif unit_ref:find(unit_cd) then
            NeedToKnow.short_cds[spell] = tonumber(n_cd)
        else
            -- Not a short cooldown.  Record it as a minute
            NeedToKnow.short_cds[spell] = 60
        end
    end

    return NeedToKnow.short_cds[spell]
end


-- Search the player's spellbook for a spell that matches 
-- todo: cache this result?
function NeedToKnow.TryToFindSpellWithCD(barSpell)
    if NeedToKnow.DetermineShortCooldownFromTooltip(barSpell) > 0 then return barSpell end
    
    for iBook = 1, GetNumSpellTabs() do
        local sBook,_,iFirst,nSpells = GetSpellTabInfo(iBook)
        for iSpell=iFirst+1, iFirst+nSpells do
            local sName = GetSpellInfo(iSpell, sBook)
            if sName == barSpell then
                local sLink = GetSpellLink(iSpell, sBook)
                local sID = sLink:match("spell:(%d+)")
                local start = GetSpellCooldown(sID)
                if start then
                    local ttcd = NeedToKnow.DetermineShortCooldownFromTooltip(sID)
                    if ttcd and ttcd>0 then
                        return sID
                    end
                end
            end
        end
    end
end


function NeedToKnow.GetItemIDString(id_or_name)
    local _, link = GetItemInfo(id_or_name)
    if link then
        local idstring = link:match("item:(%d+):")
        if idstring then
            return idstring
        end
    end
end


-- Helper for mfn_AuraCheck_CASTCD which gets the autoshot cooldown
mfn_GetAutoShotCooldown = function(bar)
    local tNow = g_GetTime()
    if ( bar.tAutoShotStart and bar.tAutoShotStart + bar.tAutoShotCD > tNow ) then
        local n, icon = GetSpellInfo(75)
        return bar.tAutoShotStart, bar.tAutoShotCD, 1, c_AUTO_SHOT_NAME, icon
    else
        bar.tAutoShotStart = nil
    end
end


-- Helper for mfn_AuraCheck_CASTCD for names we haven't figured out yet
mfn_GetUnresolvedCooldown = function(bar, entry)
    NeedToKnow.SetupSpellCooldown(bar, entry)
    local fn = bar.cd_functions[entry.idxName]
    if mfn_GetUnresolvedCooldown ~= fn then
        return fn(bar, entry)
    end
end


-- Wrapper around GetSpellCooldown with extra sauce
-- Expected to return start, cd_len, enable, buffName, iconpath
mfn_GetSpellCooldown = function(bar, entry)
    local barSpell = entry.id or entry.name
    local start, cd_len, enable = GetSpellCooldown(barSpell)
    if start and start > 0 then
        local spellName, spellRank, spellIconPath, _, _, spellPower = GetSpellInfo(barSpell)
        if not spellName then 
            if not NeedToKnow.GSIBroken then 
                NeedToKnow.GSIBroken = {} 
            end
            if  not NeedToKnow.GSIBroken[barSpell] then
                print("NeedToKnow: Warning! Unable to get spell info for",barSpell,".  Try using Spell ID instead.")
                NeedToKnow.GSIBroken[barSpell] = true;
            end
            spellName = tostring(barSpell)
        end

        if 0 == enable then 
            -- Filter out conditions like Stealth while stealthed
            start = nil
        elseif spellPower == 5 then -- Rune
            -- Filter out rune cooldown artificially extending the cd
            if cd_len <= 10 then
                local tNow = g_GetTime()
                if bar.expirationTime and tNow < bar.expirationTime then
                    -- We've already seen the correct CD for this; keep using it
                    start = bar.expirationTime - bar.duration
                    cd_len = bar.duration
                elseif m_last_sent and m_last_sent[spellName] and m_last_sent[spellName] > (tNow - 1.5) then
                    -- We think the spell was just cast, and a CD just started but it's short.
                    -- Look at the tooltip to tell what the correct CD should be. If it's supposed
                    -- to be short (Ghoul Frenzy, Howling Blast), then start a CD bar
                    cd_len = NeedToKnow.DetermineShortCooldownFromTooltip(barSpell)
                    if cd_len == 0 or cd_len > 10 then
                        start = nil
                    end
                else
                    start = nil
                end
            end
        end
        
        if start then
            return start, cd_len, enable, spellName, spellIconPath
        end
    end
end



mfn_GetSpellChargesCooldown = function(bar, entry)
    local barSpell = entry.id or entry.name
    local cur, max, charge_start, recharge = GetSpellCharges(barSpell)
    if ( cur ~= max ) then
        local start, cd_len, enable, spellName, spellIconPath 
        if ( cur == 0 ) then
            start, cd_len, enable, spellName, spellIconPath = mfn_GetSpellCooldown(bar, entry)
            return start, cd_len, enable, spellName, spellIconPath, max, charge_start
        else
            local spellName, _, spellIconPath = GetSpellInfo(barSpell)
            if not spellName then spellName = barSpell end
            return charge_start, recharge, 1, spellName, spellIconPath, max-cur
        end
    end
end



-- Wrapper around GetItemCooldown
-- Expected to return start, cd_len, enable, buffName, iconpath
function NeedToKnow.GetItemCooldown(bar, entry)
    local start, cd_len, enable = GetItemCooldown(entry.id)
    if start then
        local name, _, _, _, _, _, _, _, _, icon = GetItemInfo(entry.id)
        return start, cd_len, enable, name, icon
    end
end


function NeedToKnow.UpdateWeaponEnchantData(data, slot)
    local oldname = data.name
    data.name = NeedToKnow.DetermineTempEnchantFromTooltip(slot)
    if not data.name then
        data.name = "Unknown"
        data.icon = nil
        --trace("Warning: NTK couldn't figure out what enchant is on weapon slot",slot)
    end
    data.expiration = g_GetTime() + data.expiration/1000
    if oldname ~= data.name then
        local _
        _,_,data.icon = GetSpellInfo(data.name)
        if nil == data.icon then
            _,_,data.icon = GetSpellInfo(data.name .. " Weapon")
        end
        if nil == data.icon then
            _,_,_,_,_,_,_,_,_,data.icon = GetItemInfo(data.name)
        end
        if nil == data.icon then
            data.icon=GetInventoryItemTexture("player",slot)
        end
    end
end

-- Scrapes the current tooltips for the player's weapons to tease out
-- the name of the current weapon imbue (and not just the name of the 
-- weapon, like you get from the Blizzard API.)  This info gets cached
-- on the bar so we don't have to compute it every Bar_AuraCheck
function NeedToKnow.UpdateWeaponEnchants()
    local mdata = NeedToKnow.weapon_enchants.mhand
    local odata = NeedToKnow.weapon_enchants.ohand

    mdata.present, mdata.expiration, mdata.charges, 
      odata.present, odata.expiration, odata.charges 
      = g_GetWeaponEnchantInfo()
      
    if ( mdata.present ) then
        NeedToKnow.UpdateWeaponEnchantData(mdata, 16)
    else
        mdata.name=nil
    end

    if ( odata.present ) then
        NeedToKnow.UpdateWeaponEnchantData(odata, 17)
    else
        odata.name = nil
    end
end


mfn_AddInstanceToStacks = function (all_stacks, bar_entry, duration, name, count, expirationTime, iconPath, caster, tt1, tt2, tt3)
    if duration then
        if (not count or count < 1) then count = 1 end
        if ( 0 == all_stacks.total or all_stacks.min.expirationTime > expirationTime ) then
            all_stacks.min.idxName = bar_entry.idxName
            all_stacks.min.buffName = name
            all_stacks.min.caster = caster
            all_stacks.min.duration = duration
            all_stacks.min.expirationTime = expirationTime
            all_stacks.min.iconPath = iconPath
        end
        if ( 0 == all_stacks.total or all_stacks.max.expirationTime < expirationTime ) then
            all_stacks.max.duration = duration
            all_stacks.max.expirationTime = expirationTime
        end 
        all_stacks.total = all_stacks.total + count
        if ( tt1 ) then
            all_stacks.total_ttn[1] = all_stacks.total_ttn[1] + tt1
            if ( tt2 ) then
                all_stacks.total_ttn[2] = all_stacks.total_ttn[2] + tt2
            end
            if ( tt3 ) then
                all_stacks.total_ttn[3] = all_stacks.total_ttn[3] + tt3
            end
        end
    end
end


-- Bar_AuraCheck helper for Totem bars, this returns data if
-- a totem matching bar_entry is currently out. 
mfn_AuraCheck_TOTEM = function(bar, bar_entry, all_stacks)
    local idxName = bar_entry.idxName
    local sComp = bar_entry.name or GetSpellInfo(bar_entry.id)
    for iSlot=1, 4 do
        local haveTotem, totemName, startTime, totemDuration, totemIcon = GetTotemInfo(iSlot)
        if ( totemName and totemName:find(sComp) ) then
            -- WORKAROUND: The startTime reported here is both cast to an int and off by 
            -- a latency meaning it can be significantly low.  So we cache the g_GetTime 
            -- that the totem actually appeared, so long as g_GetTime is reasonably close to 
            -- startTime (since the totems may have been out for awhile before this runs.)
            if ( not NeedToKnow.totem_drops[iSlot] or 
                 NeedToKnow.totem_drops[iSlot] < startTime ) 
            then
                local precise = g_GetTime()
                if ( precise - startTime > 1 ) then
                    precise = startTime + 1
                end
                NeedToKnow.totem_drops[iSlot] = precise
            end

            mfn_AddInstanceToStacks(all_stacks, bar_entry, 
                   totemDuration,                              -- duration
                   totemName,                                  -- name
                   1,                                          -- count
                   NeedToKnow.totem_drops[iSlot] + totemDuration, -- expiration time
                   totemIcon,                                  -- icon path
                   "player" )                                  -- caster
        end
    end
end




-- Bar_AuraCheck helper for tracking usable gear based on the slot its in
-- rather than the equipment name
mfn_AuraCheck_EQUIPSLOT = function (bar, bar_entry, all_stacks)
    local spellName, spellRank, spellIconPath
    if ( bar_entry.id ) then
        local id = GetInventoryItemID("player",bar_entry.id)
        if id then
            local item_entry = m_scratch.bar_entry
            item_entry.id = id
            local start, cd_len, enable, name, icon = NeedToKnow.GetItemCooldown(bar, item_entry)

            if ( start and start > 0 ) then
                mfn_AddInstanceToStacks(all_stacks, bar_entry, 
                       cd_len,                                     -- duration
                       name,                                       -- name
                       1,                                          -- count
                       start + cd_len,                             -- expiration time
                       icon,                                       -- icon path
                       "player" )                                  -- caster
            end
        end
    end
end



-- Bar_AuraCheck helper for power and combo points.  The current
-- amount is reported as the first tooltip number rather than 
-- stacks since 1 stack doesn't get displayed normally
mfn_AuraCheck_POWER = function (bar, bar_entry, all_stacks)
    local spellName, spellRank, spellIconPath
    local cpt = UnitPowerType(bar.unit)
    local pt = bar_entry.id

    if ( pt ) then
        if pt == 4 then pt = cpt end

        local curPower, maxPower;
        if ( pt == -1 ) then
            curPower = GetComboPoints("player", bar.unit)
            maxPower =  MAX_COMBO_POINTS
        else
            curPower = UnitPower(bar.unit, pt)
            maxPower = UnitPowerMax(bar.unit, pt)
        end

        if ( maxPower and maxPower > 0 and
             (not bar.settings.power_sole or pt == cpt) ) 
        then
            local bTick = false
            if pt == 3 then -- SPELL_POWER_ENERGY
                if (pt == cpt) then
                    bar.power_regen = GetPowerRegen()
                end
                if (bar.power_regen and bar.power_regen > 0) then
                    bTick = true
                end
            end
            if bTick then
                if not bar.ticking then
                    bar.ticker = mfn_EnergyBar_OnUpdate
                    bar:SetScript("OnUpdate", bar.ticker)
                    bar.ticking = true
                end
            elseif bar.ticking then
                bar:SetScript("OnUpdate", nil)
                bar.ticking = false
            end

            if bar.ticking then                
                local now = g_GetTime()
                if not bar.tPower or now - bar.tPower > 2 or bar.last_power ~= curPower then
                    bar.tPower = now
                    bar.last_power = curPower
                    bar.last_power_max = maxPower

                end
            end

            mfn_AddInstanceToStacks(all_stacks, bar_entry, 
                   0,                                          -- duration
                   NEEDTOKNOW.POWER_TYPES[pt],                 -- name
                   1,                                          -- count
                   0,                                          -- expiration time
                   nil,                                        -- icon path
                   bar.unit,                                   -- caster
                   curPower,                                   -- tooltip #1
                   maxPower,                                   -- tooltip #2
                   floor(curPower*1000/maxPower)/10 )          -- tooltip #3
        end
    end
end



-- Bar_AuraCheck helper that checks the bar.weapon_enchants 
-- (computed by UpdateWeaponEnchants) for the given spell.
-- FIXME: this is the only bar type that does not work with spell ids.
mfn_AuraCheck_Weapon = function (bar, bar_entry, all_stacks)
    local data = NeedToKnow.weapon_enchants[bar.settings.Unit]
    if ( data.present and data.name and data.name:find(bar_entry.name) ) then
        mfn_AddInstanceToStacks( all_stacks, bar_entry,
               1800,                                       -- duration TODO: Get real duration?
               data.name,                                  -- name
               data.charges,                               -- count
               data.expiration,                            -- expiration time
               data.icon,                                  -- icon path
               "player" )                                  -- caster
    end
end


-- Bar_AuraCheck helper that checks for spell/item use cooldowns
-- Relies on mfn_GetAutoShotCooldown, mfn_GetSpellCooldown 
-- and NeedToKnow.GetItemCooldown. Bar_Update will have already pre-processed 
-- this list so that bar.cd_functions[idxName] can do something with bar_entry
mfn_AuraCheck_CASTCD = function(bar, bar_entry, all_stacks)
    local idxName = bar_entry.idxName
    local func = bar.cd_functions[idxName]
    if ( not func ) then
        print("NTK ERROR setting up index",idxName,"on bar",bar:GetName(),bar.settings.AuraName);
        return;
    end
    local start, cd_len, should_cooldown, buffName, iconPath, stacks, start_2 = func(bar, bar_entry)

    -- filter out the GCD, we only care about actual spell CDs
    if start and cd_len <= 1.5 and func ~= mfn_GetAutoShotCooldown then
        if bar.expirationTime and bar.expirationTime <= (start + cd_len) then
            start = bar.expirationTime - bar.duration
            cd_len = bar.duration
        else
            start = nil
        end
    end

    if start and cd_len then
        local tNow = g_GetTime()
        local tEnd = start + cd_len
        if ( tEnd > tNow + 0.1 ) then
            if start_2 then
                mfn_AddInstanceToStacks( all_stacks, bar_entry,
                    cd_len,                                   -- duration
                    buffName,                                   -- name
                    1,                                          -- count
                    start_2+cd_len,                             -- expiration time
                    iconPath,                                   -- icon path
                    "player" )                                  -- caster
                stacks = stacks - 1
            else
                if not stacks then stacks = 1 end
            end
            mfn_AddInstanceToStacks( all_stacks, bar_entry,
                    cd_len,                                     -- duration
                    buffName,                                   -- name
                    stacks,                                     -- count
                    tEnd,                                       -- expiration time
                    iconPath,                                   -- icon path
                    "player" )                                  -- caster
        end
    end
end


-- Bar_AuraCheck helper for watching "Is Usable", which means that the action
-- bar button for the spell lights up.  This is mostly useful for Victory Rush
mfn_AuraCheck_USABLE = function (bar, bar_entry, all_stacks)
    local key = bar_entry.id or bar_entry.name
    local settings = bar.settings
    if ( not key ) then key = "" end
    local spellName, _, iconPath = GetSpellInfo(key)
    if ( spellName ) then
        local isUsable, notEnoughMana = IsUsableSpell(spellName)
        if (isUsable or notEnoughMana) then
            local duration = settings.usable_duration
            local expirationTime
            local tNow = g_GetTime()
            if ( not bar.expirationTime or 
                 (bar.expirationTime > 0 and bar.expirationTime < tNow - 0.01) ) 
            then
                duration = settings.usable_duration
                expirationTime = tNow + duration
            else
                duration = bar.duration
                expirationTime = bar.expirationTime
            end

            mfn_AddInstanceToStacks( all_stacks, bar_entry,
                   duration,                                   -- duration
                   spellName,                                  -- name
                   1,                                          -- count
                   expirationTime,                             -- expiration time
                   iconPath,                                   -- icon path
                   "player" )                                  -- caster
        end
    end
end


mfn_ResetScratchStacks = function (buff_stacks)
    buff_stacks.total = 0;
    buff_stacks.total_ttn[1] = 0;
    buff_stacks.total_ttn[2] = 0;
    buff_stacks.total_ttn[3] = 0;
end

-- Bar_AuraCheck helper for watching "internal cooldowns", which is like a spell
-- cooldown for spells cast automatically (procs).  The "reset on buff" logic
-- is still handled by 
mfn_AuraCheck_BUFFCD = function (bar, bar_entry, all_stacks)
    local buff_stacks = m_scratch.buff_stacks
    mfn_ResetScratchStacks(buff_stacks);
    mfn_AuraCheck_Single(bar, bar_entry, buff_stacks)
    local tNow = g_GetTime()
    if ( buff_stacks.total > 0 ) then
        if buff_stacks.max.expirationTime == 0 then
            -- TODO: This really doesn't work very well as a substitute for telling when the aura was applied
            if not bar.expirationTime then
                local nDur = tonumber(bar.settings.buffcd_duration)
                mfn_AddInstanceToStacks( all_stacks, bar_entry,
                    nDur, buff_stacks.min.buffName, 1, nDur+tNow, buff_stacks.min.iconPath, buff_stacks.min.caster )
            else
                mfn_AddInstanceToStacks( all_stacks, bar_entry,
                       bar.duration,                               -- duration
                       bar.buffName,                               -- name
                       1,                                          -- count
                       bar.expirationTime,                         -- expiration time
                       bar.iconPath,                               -- icon path
                       "player" )                                  -- caster
            end
            return
        end
        local tStart = buff_stacks.max.expirationTime - buff_stacks.max.duration
        local duration = tonumber(bar.settings.buffcd_duration)
        local expiration = tStart + duration
        if ( expiration > tNow ) then
            mfn_AddInstanceToStacks( all_stacks, bar_entry,
                   duration,                                   -- duration
                   buff_stacks.min.buffName,                                   -- name
                   -- Seeing the charges on the CD bar violated least surprise for me
                   1,                                          -- count
                   expiration,                                 -- expiration time
                   buff_stacks.min.iconPath,                   -- icon path
                   buff_stacks.min.caster )                    -- caster
        end
    elseif ( bar.expirationTime and bar.expirationTime > tNow + 0.1 ) then
        mfn_AddInstanceToStacks( all_stacks, bar_entry,
               bar.duration,                               -- duration
               bar.buffName,                               -- name
               1,                                          -- count
               bar.expirationTime,                         -- expiration time
               bar.iconPath,                               -- icon path
               "player" )                                  -- caster
    end
end

local function UnitAuraWrapper(a,b,c,d)
     local
        name,  
        _, -- rank,  
        icon,
        count,  
        _, -- type,
        dur,
        expiry,
        caster,
        _, -- uao.steal,
        _, -- uao.cons -- Should consolidate
        id,
        _, -- uao.canCast -- The player's class/spec can cast this spell
        _, -- A boss applied this
        _, -- cast by any player
        v1,
        v2,
        v3
    = UnitAura(a,b,c,d)

    if name then
        return name, icon, count, dur, expiry, caster, id, v1, v2, v3
    end
end

-- Bar_AuraCheck helper that looks for the first instance of a buff
-- Uses the UnitAura filters exclusively if it can
mfn_AuraCheck_Single = function(bar, bar_entry, all_stacks)
    local settings = bar.settings
    local filter = settings.BuffOrDebuff
    if settings.OnlyMine then
        filter = filter .. "|PLAYER"
    end

    if bar_entry.id then
        -- WORKAROUND: The second parameter to UnitAura can't be a spellid, so I have 
        --             to walk them all
        local barID = bar_entry.id
        local j = 1
        while true do
            local buffName, iconPath, count, duration, expirationTime, caster, spellID, tt1, tt2, tt3
              = UnitAuraWrapper(bar.unit, j, filter)
            if (not buffName) then
                break
            end

            if (spellID == barID) then 
                mfn_AddInstanceToStacks( all_stacks, bar_entry,
                       duration,                               -- duration
                       buffName,                               -- name
                       count,                                  -- count
                       expirationTime,                         -- expiration time
                       iconPath,                               -- icon path
                       caster,                                 -- caster
                       tt1, tt2, tt3 )                         -- extra status values, like vengeance armor or healing bo
                return;
            end
            j=j+1
        end
    else
        local buffName, iconPath, count, duration, expirationTime, caster, _, tt1, tt2, tt3 
          = UnitAuraWrapper(bar.unit, bar_entry.name, nil, filter)
          mfn_AddInstanceToStacks( all_stacks, bar_entry,
               duration,                               -- duration
               buffName,                               -- name
               count,                                  -- count
               expirationTime,                         -- expiration time
               iconPath,                               -- icon path
               caster,                                 -- caster
               tt1, tt2, tt3 )                         -- extra status values, like vengeance armor or healing bo
    end
end


-- Bar_AuraCheck helper that updates bar.all_stacks (but returns nil)
-- by scanning all the auras on the unit
mfn_AuraCheck_AllStacks = function (bar, bar_entry, all_stacks)
    local j = 1
    local settings = bar.settings
    local filter = settings.BuffOrDebuff
    
    while true do
        local buffName, iconPath, count, duration, expirationTime, caster, spellID, tt1, tt2, tt3
          = UnitAuraWrapper(bar.unit, j, filter)
        if (not buffName) then
            break
        end
        
        if (spellID == bar_entry.id) or (bar_entry.name == buffName) 
        then
            mfn_AddInstanceToStacks(all_stacks, bar_entry, 
                duration,
                buffName,
                count,
                expirationTime,
                iconPath,
                caster,
                tt1, tt2, tt3 )
        end

        j = j+1
    end
end


-- Called whenever the state of auras on the bar's unit may have changed
local g_UnitExists = UnitExists
mfn_Bar_AuraCheck = function (bar)
    local settings = bar.settings
    local bUnitExists, isWeapon
    if "mhand" == settings.Unit or
       "ohand" == settings.Unit then
        isWeapon = true
        bUnitExists = true
    elseif "player" == settings.Unit then
        bUnitExists = true
    elseif "lastraid" == settings.Unit then
        bUnitExists = bar.unit and UnitExists(bar.unit)
    else
        bUnitExists = g_UnitExists(settings.Unit)
    end
    
    -- Determine if the bar should be showing anything
    local all_stacks       
    local idxName, duration, buffName, count, expirationTime, iconPath, caster
    if ( bUnitExists ) then
        all_stacks = m_scratch.all_stacks
        mfn_ResetScratchStacks(all_stacks);

        -- Call the helper function for each of the spells in the list
        for idx, entry in ipairs(bar.spells) do
            bar.fnCheck(bar, entry, all_stacks);
            
            if all_stacks.total > 0 and not settings.show_all_stacks then
                idxName = idx
                break 
            end
        end
    end
    
    if ( all_stacks and all_stacks.total > 0 ) then
        idxName = all_stacks.min.idxName
        buffName = all_stacks.min.buffName
        caster = all_stacks.min.caster
        duration = all_stacks.max.duration
        expirationTime = all_stacks.min.expirationTime
        iconPath = all_stacks.min.iconPath
        count = all_stacks.total
    end

    -- Cancel the work done above if a reset spell is encountered
    -- (reset_spells will only be set for BUFFCD)
    if ( bar.reset_spells ) then
        local maxStart = 0
        local tNow = g_GetTime()
        local buff_stacks = m_scratch.buff_stacks
        mfn_ResetScratchStacks(buff_stacks);
        -- Keep track of when the reset auras were last applied to the player
        for idx, resetSpell in ipairs(bar.reset_spells) do
            -- Note this relies on BUFFCD setting the target to player, and that the onlyMine will work either way
            local resetDuration, _, _, resetExpiration
              = mfn_AuraCheck_Single(bar, resetSpell, buff_stacks)
            local tStart
            if buff_stacks.total > 0 then
               if 0 == buff_stacks.max.duration then 
                   tStart = bar.reset_start[idx]
                   if 0 == tStart then
                       tStart = tNow
                   end
               else
                   tStart = buff_stacks.max.expirationTime - buff_stacks.max.duration
               end
               bar.reset_start[idx] = tStart
               
               if tStart > maxStart then maxStart = tStart end
            else
               bar.reset_start[idx] = 0
            end
        end
        if duration and maxStart > expirationTime-duration then
            duration = nil
        end
    end
    
    -- There is an aura this bar is watching! Set it up
    if ( duration ) then
        duration = tonumber(duration)
        -- Handle duration increases
        local extended
        if (settings.bDetectExtends) then
            local curStart = expirationTime - duration
            local guidTarget = UnitGUID(bar.unit)
            local r = m_last_guid[buffName] 
            
            if ( not r[guidTarget] ) then -- Should only happen from /reload or /ntk while the aura is active
                -- This went off for me, but I don't know a repro yet.  I suspect it has to do with bear/cat switching
                --trace("WARNING! allocating guid slot for ", buffName, "on", guidTarget, "due to UNIT_AURA");
                r[guidTarget] = { time=curStart, dur=duration, expiry=expirationTime }
            else
                r = r[guidTarget]
                local oldExpiry = r.expiry
                -- This went off for me, but I don't know a repro yet.  I suspect it has to do with bear/cat switching
                --if ( oldExpiry > 0 and oldExpiry < curStart ) then
                    --trace("WARNING! stale entry for ",buffName,"on",guidTarget,curStart-r.time,curStart-oldExpiry)
                --end

                if ( oldExpiry < curStart ) then
                    r.time = curStart
                    r.dur = duration 
                    r.expiry= expirationTime
                else
                    r.expiry= expirationTime
                    extended =  expirationTime - (r.time + r.dur)
                    if ( extended > 1 ) then
                        duration = r.dur 
                    else
                        extended = nil
                    end
                end
            end
        end

        --bar.duration = tonumber(bar.fixedDuration) or duration
        bar.duration = duration

        bar.expirationTime = expirationTime
        bar.idxName = idxName
        bar.buffName = buffName
        bar.iconPath = iconPath
        if ( all_stacks and all_stacks.max.expirationTime ~= expirationTime ) then
            bar.max_expirationTime = all_stacks.max.expirationTime
        else
            bar.max_expirationTime = nil
        end

        -- Mark the bar as not blinking before calling ConfigureVisibleBar, 
        -- since it calls OnUpdate which checks bar.blink
        bar.blink=false
        NeedToKnow.ConfigureVisibleBar(bar, count, extended, all_stacks)
        bar:Show()
    else
        if (settings.bDetectExtends and bar.buffName) then
            local r = m_last_guid[bar.buffName]
            if ( r ) then
                local guidTarget = UnitGUID(bar.unit)
                if guidTarget then
                    r[guidTarget] = nil
                end
            end
        end
        bar.buffName = nil
        bar.duration = nil
        bar.expirationTime = nil
        
        local bBlink = false
        if settings.blink_enabled and settings.MissingBlink.a > 0 then
            if isWeapon then
                bBlink = true
            else
                bBlink = bUnitExists and not UnitIsDead(bar.unit)
            end
        end
        if ( bBlink and not settings.blink_ooc ) then
            if not g_UnitAffectingCombat("player") then
                bBlink = false
            end
        end
        if ( bBlink and settings.blink_boss ) then
            if g_UnitIsFriend(bar.unit, "player") then
                bBlink = m_bCombatWithBoss
            else
                bBlink = (UnitLevel(bar.unit) == -1)
            end
        end
        if ( bBlink ) then
            NeedToKnow.ConfigureBlinkingBar(bar)
            bar:Show()
        else    
            bar.blink=false
            bar:Hide()
        end
    end
end


function NeedToKnow.Fmt_SingleUnit(i_fSeconds)
    return string.format(SecondsToTimeAbbrev(i_fSeconds))
end


function NeedToKnow.Fmt_TwoUnits(i_fSeconds)
  if ( i_fSeconds < 6040 ) then
      local nMinutes, nSeconds
      nMinutes = floor(i_fSeconds / 60)
      nSeconds = floor(i_fSeconds - nMinutes*60)
      return string.format("%02d:%02d", nMinutes, nSeconds)
  else
      string.format(SecondsToTimeAbbrev(i_fSeconds))
  end
end

function NeedToKnow.Fmt_Float(i_fSeconds)
  return string.format("%0.1f", i_fSeconds)
end

function NeedToKnow.Bar_OnUpdate(self, elapsed)
    local now = g_GetTime()
    if ( now > self.nextUpdate ) then
        self.nextUpdate = now + c_UPDATE_INTERVAL

        if ( self.blink ) then
            self.blink_phase = self.blink_phase + c_UPDATE_INTERVAL
            if ( self.blink_phase >= 2 ) then
                self.blink_phase = 0
            end
            local a = self.blink_phase
            if ( a > 1 ) then
                a = 2 - a
            end

            self.bar1:SetVertexColor(self.settings.MissingBlink.r, self.settings.MissingBlink.g, self.settings.MissingBlink.b)
            self.bar1:SetAlpha(self.settings.MissingBlink.a * a)
            return
        end
        
        -- WORKAROUND: Although the existence of the enchant is correct at UNIT_INVENTORY_CHANGED
        -- the expiry time is not yet correct.  So we update the expiration every update :(
        local origUnit = self.settings.Unit
        if ( origUnit == "mhand" ) then
            -- The expiry time doesn't update right away, so we have to poll it
            local mhEnchant, mhExpire = g_GetWeaponEnchantInfo()
            if ( mhExpire ) then
                self.expirationTime = g_GetTime() + mhExpire/1000
            end
        elseif ( origUnit == "ohand" ) then
            local _, _, _, ohEnchant, ohExpire = g_GetWeaponEnchantInfo()
            if ( ohExpire ) then
                self.expirationTime = g_GetTime() + ohExpire/1000
            end
        end
        
        -- WORKAROUND: Some of these (like item cooldowns) don't fire an event when the CD expires.
        --   others fire the event too soon.  So we have to keep checking.
        if ( self.duration and self.duration > 0 ) then
            local duration = self.fixedDuration or self.duration
            local bar1_timeLeft = self.expirationTime - g_GetTime()
            if ( bar1_timeLeft < 0 ) then
                if ( self.settings.BuffOrDebuff == "CASTCD" or
                     self.settings.BuffOrDebuff == "BUFFCD" or
                     self.settings.BuffOrDebuff == "EQUIPSLOT" )
                then
                    mfn_Bar_AuraCheck(self)
                    return
                end
                bar1_timeLeft = 0
            end
            mfn_SetStatusBarValue(self, self.bar1, bar1_timeLeft);
            if ( self.settings.show_time ) then
                local fn = NeedToKnow[self.settings.TimeFormat]
                local oldText = self.time:GetText()
                local newText
                if ( fn ) then
                    newText = fn(bar1_timeLeft)
                else 
                    newText = string.format(SecondsToTimeAbbrev(bar1_timeLeft))
                end
                
                if ( newText ~= oldText ) then
                    self.time:SetText(newText)
                end
            else
                self.time:SetText("")
            end
            
            if ( self.settings.show_spark and bar1_timeLeft <= duration ) then
                self.spark:SetPoint("CENTER", self, "LEFT", self:GetWidth()*bar1_timeLeft/duration, 0)
                self.spark:Show()
            else
                self.spark:Hide()
            end
            
            if ( self.max_expirationTime ) then
                local bar2_timeLeft = self.max_expirationTime - g_GetTime()
                mfn_SetStatusBarValue(self, self.bar2, bar2_timeLeft, bar1_timeLeft)
            end
            
            if ( self.vct_refresh ) then
                mfn_UpdateVCT(self)
            end
        end
    end
end 


mfn_EnergyBar_OnUpdate = function(bar, elapsed)
    local now = g_GetTime()
    if ( now > bar.nextUpdate ) then
        bar.nextUpdate = now + c_UPDATE_INTERVAL
        local delta = now - bar.tPower
        local predicted = bar.last_power + bar.power_regen * delta
        local bCapped = false
        if predicted >= bar.last_power_max then
            predicted = bar.last_power_max
            bCapped = true
        elseif predicted <= 0 then
            predicted = 0
            bCapped = true
        end

        bar.max_value = bar.last_power_max
        mfn_SetStatusBarValue(bar, bar.bar1, predicted);

        if bCapped then
            bar.ticking = false
            bar:SetScript("OnUpdate", nil)
        end
    end
end




-- Define the event dispatching table.  Note, this comes last as the referenced 
-- functions must already be declared.  Avoiding the re-evaluation of all that
-- is one of the reasons this is an optimization!
local fnAuraCheckIfUnitMatches = function(self, unit)
    if ( unit == self.unit )  then
        mfn_Bar_AuraCheck(self)
    end
end

local fnAuraCheckIfUnitPlayer = function(self, unit)
    if ( unit == "player" ) then
        mfn_Bar_AuraCheck(self)
    end
end

local EDT = {}
EDT["COMBAT_LOG_EVENT_UNFILTERED"] = function(self, unit, ...)
    local combatEvent = select(1, ...)

    if ( c_AURAEVENTS[combatEvent] ) then
        local guidTarget = select(7, ...)
        if ( guidTarget == g_UnitGUID(self.unit) ) then
            local idSpell, nameSpell = select(11, ...)
            if (self.auraName:find(idSpell) or
                    self.auraName:find(nameSpell)) 
            then 
                mfn_Bar_AuraCheck(self)
            end
        end
    elseif ( combatEvent == "UNIT_DIED" ) then
        local guidDeceased = select(7, ...) 
        if ( guidDeceased == UnitGUID(self.unit) ) then
            mfn_Bar_AuraCheck(self)
        end
    end 
end
EDT["PLAYER_TOTEM_UPDATE"] = mfn_Bar_AuraCheck
EDT["ACTIONBAR_UPDATE_COOLDOWN"] = mfn_Bar_AuraCheck
EDT["SPELL_UPDATE_COOLDOWN"] = mfn_Bar_AuraCheck
EDT["SPELL_UPDATE_USABLE"] = mfn_Bar_AuraCheck
EDT["UNIT_AURA"] = fnAuraCheckIfUnitMatches
EDT["UNIT_POWER"] = fnAuraCheckIfUnitMatches
EDT["UNIT_DISPLAYPOWER"] = fnAuraCheckIfUnitMatches
EDT["UNIT_COMBO_POINTS"] = mfn_Bar_AuraCheck
EDT["UNIT_INVENTORY_CHANGED"] = function(self, unit)
  if unit == "player" then
    NeedToKnow.UpdateWeaponEnchants()
    mfn_Bar_AuraCheck(self)
  end
end
EDT["PLAYER_TARGET_CHANGED"] = function(self, unit)
    if self.unit == "targettarget" then
        NeedToKnow.CheckCombatLogRegistration(self)
    end
    mfn_Bar_AuraCheck(self)
end  
EDT["PLAYER_FOCUS_CHANGED"] = EDT["PLAYER_TARGET_CHANGED"]
EDT["UNIT_TARGET"] = function(self, unit)
    if unit == "target" and self.unit == "targettarget" then
        NeedToKnow.CheckCombatLogRegistration(self)
    end
    mfn_Bar_AuraCheck(self)
end  
EDT["UNIT_PET"] = fnAuraCheckIfUnitPlayer
EDT["PLAYER_SPELLCAST_SUCCEEDED"] = function(self, unit, ...)
    local spellName, spellID, tgt = select(1,...)
    local i,entry
    for i,entry in ipairs(self.spells) do
        if entry.id == spellID or entry.name == spellName then
            self.unit = tgt or "unknown"
            --trace("Updating",self:GetName(),"since it was recast on",self.unit)
            mfn_Bar_AuraCheck(self)
            break;
        end
    end
end
EDT["START_AUTOREPEAT_SPELL"] = function(self, unit, ...)
    self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
end
EDT["STOP_AUTOREPEAT_SPELL"] = function(self, unit, ...)
    self:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
end
EDT["UNIT_SPELLCAST_SUCCEEDED"] = function(self, unit, ...)
    local spell = select(1,...)
    if ( self.settings.bAutoShot and unit == "player" and spell == c_AUTO_SHOT_NAME ) then
        local interval = UnitRangedDamage("player")
        self.tAutoShotCD = interval
        self.tAutoShotStart = g_GetTime()
        mfn_Bar_AuraCheck(self)
    end
end

function NeedToKnow.Bar_OnEvent(self, event, unit, ...)
    local fn = EDT[event]
    if fn then 
        fn(self, unit, ...)
    end
end

Compare with Previous | Blame