--[[---------------------------------------------------------------------------

Copyright (c) 2008 by K. Scott Piel 
All Rights Reserved

E-mail: < kscottpiel@gmail.com >
Web:    < http://www.scottpiel.com >

This file is part of nUI.

    nUI is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    nUI is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with nUI.  If not, see <http://www.gnu.org/licenses/>.
	
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

--]]---------------------------------------------------------------------------

-------------------------------------------------------------------------------
-- local cache

if not nUI then nUI = {}; end
if not nUI.SpellStatus then nUI.SpellStatus = {}; end
if not nUI.Cooldowns then nUI.Cooldowns = {}; end
if not nUI_ButtonColors then nUI_ButtonColors = {}; end
if not nUI_DefaultConfig then nUI_DefaultConfig = {}; end

local ActionHasRange     = ActionHasRange;
local CreateFrame        = CreateFrame;
local GetBindingKey      = GetBindingKey;
local GetActionInfo      = GetActionInfo;
local IsActionInRange    = IsActionInRange;
local IsUsableAction     = IsUsableAction;
local IsConsumableAction = IsConsumableAction;
local GetActionCount     = GetActionCount;
local UnitExists         = UnitExists;
local UnitIsUnit         = UnitIsUnit;
local GetTime            = GetTime;
local GetActionInfo      = GetActionInfo;
local UIFrameFlash       = UIFrameFlash;
local floor              = math.floor;
local nUI                = nUI;
local SpellStatus        = nUI.SpellStatus;
local Cooldowns          = nUI.Cooldowns;

-------------------------------------------------------------------------------

nUI_DefaultConfig.ButtonColors =
{
	["OOR"]  = { r = 1, g = 0, b = 0 },
	["OOM"]  = { r = 0, g = 0, b = 1 },
	["CDC1"] = { r = 1, g = 1, b = 0 },
	["CDC2"] = { r = 1, g = 0.6, b = 0.6 },
}

-------------------------------------------------------------------------------
-- spells we know to be valid for checking the global cooldown (GCD)

local nUI_GCDSpells = {};
local nUI_CandidateGCD = {};

local frame = CreateFrame( "Frame", "nUI_ButtonEvents", WorldFrame );

-------------------------------------------------------------------------------

local function onFeedbackEvent()
	
	if event == "VARIABLES_LOADED" then
		
		nUI:patchConfig();
		nUI_Unit:configButtons();
		
	end		
end

frame:SetScript( "OnEvent", onButtonEvent );
frame:RegisterEvent( "VARIABLES_LOADED" );

-------------------------------------------------------------------------------
-- initialize configuration for the action button color indicators
-- 
-- this method is called when the mod's saved variables have been loaded by Bliz and
-- may be called again whenever the button color configuration has been changed
-- by the player or programmatically. Passing true or a non-nil value for "use_default"
-- will cause the player's current feedback color configuration to be replaced with
-- the default settings defined at the top of this file (which cannot be undone!)

function nUI:configButtons( use_default )
	
	if not nUI_ButtonColors then nUI_ButtonColors = {}; end
	
	for color in pairs( nUI_DefaultConfig.ButtonColors ) do
		nUI:configButtonColor( color, use_default );
	end
end

function nUI:configButtonColor( feedback, use_default )
	
	local config  = nUI_ButtonColors[color] or {};
	local default = nUI_DefaultConfig.ButtonColors[color] or {};
	
	if use_default then
			
		config.r = default.r;
		config.g = default.g;
		config.b = default.b;

	else
			
		config.r = tonumber( config.r or default.r );
		config.g = tonumber( config.g or default.g );
		config.b = tonumber( config.b or default.b );

	end
	
	nUI_ButtonColors[color] = config;
	
end

-------------------------------------------------------------------------------
-- support method to convert time remaining into a countdown text string

function nUI_DurationText( remain )

	local text = "";
	
	if remain > 3600 then -- over an hour remaining
		
		local hours = remain / 3600;
		text = ("%0.0f"):format( hours ).."h";
		
	elseif remain > 60 then -- over one minute remaining
	
		local minutes = remain / 60;
		text = ("%0.0f"):format( minutes ).."m";
		
	elseif remain > 1 then -- more than a second to go
	
		local seconds = remain;
		text = ("%0.0f"):format( seconds ).."s";
		
	else -- less than two seconds
	
		text = ("%0.1f"):format( remain );
		
	end
	
	return text;
	
end

-------------------------------------------------------------------------------
-- hook action button updates

hooksecurefunc( "ActionButton_OnUpdate",

	function( elapsed )

		if this and this.onUpdate then
			if this:onUpdate( elapsed ) then
				this:updateUsable();
			end
		end	
	end	
);

-------------------------------------------------------------------------------
-- hook cooldown timer setups

hooksecurefunc( "CooldownFrame_SetTimer",

	function( cooldown, start, duration, enable )

		if not this then 
			this = cooldown; 
		end
		
		if this and this.startCooldown then 
			this:startCooldown( start, duration, enable ); 
		end
	end
	
);

-------------------------------------------------------------------------------
-- hook hotkey updates

hooksecurefunc( "ActionButton_UpdateHotkeys",

	function()

		if this and this.updateHotkeys then 
			this:updateHotkeys(); 
		end
	end
	
);

-------------------------------------------------------------------------------
-- hook usability updates

hooksecurefunc( "ActionButton_UpdateUsable",

	function()

		if this and this.updateUsable then 
			this:updateUsable(); 
		end
	end
	
);

-------------------------------------------------------------------------------
-- setup hooks and support methods in the SecureActionButton class to manage
-- nUI features

function nUI:initActionButton( button )

	-- initialization when button is first created
	
	if not button.nUI_init then
		
		local btn_name = button:GetName();

		button.nUI_init          = true;
		button.elapsed           = 0;
		button.layers            = {};	
		button.Timers            = {};
		button.Timers.range      = 0;
		button.Timers.cdc        = 0;
		button.Timers.cdc_expire = nil;
		button.Timers.visibility = 0;	
		button.Timers.scale      = 0;
		
		button.is_usable         = true;
		button.is_oom            = false;
		button.is_oor            = false;
		button.is_cdc            = false;
	
		-- cache references to all of the relevant frames
	
		button.layers.flash     = _G[btn_name.."Flash"];
		button.layers.count     = _G[btn_name.."Count"];
		button.layers.name      = _G[btn_name.."Name"];
		button.layers.border    = _G[btn_name.."Border"];
		button.layers.cooldown  = _G[btn_name.."Cooldown"];
		button.layers.normal    = _G[btn_name.."NormalTexture"];
		button.layers.normal2   = _G[btn_name.."NormalTexture2"];
		button.layers.castable  = _G[btn_name.."AutoCastable"];
		button.layers.autocast  = _G[btn_name.."AutoCast"];
		button.layers.icon      = _G[btn_name.."Icon"];
		button.layers.hotkey    = _G[btn_name.."HotKey"];
		button.layers.pushed    = button:GetPushedTexture();
		button.layers.highlight = button:GetHighlightTexture();
	
		---------------------------------------------------------------------------
		-- dispose of the Blizz background textures
		
		button:SetNormalTexture( "" ); 
		button.SetNormalTexture = function() end;
	
		---------------------------------------------------------------------------
		-- create a frame to hold the cooldown counter text
	
		button.layers.cdcFrame = CreateFrame( "Frame", btn_name.."_CDC", button );
		button.layers.cdc      = button.layers.cdcFrame:CreateFontString( "$parentLabel" );

		button.layers.cdcFrame:SetAllPoints( button );
		
		button.layers.cdc:SetAllPoints( button );
		button.layers.cdc:SetJustifyH( "CENTER" );
		button.layers.cdc:SetJustifyV( "MIDDLE" );
		button.layers.cdc:SetTextColor( 1, 1, 0, 1 );
			
		---------------------------------------------------------------------------
		-- create a grid to display when dragging actions
	
		button.layers.grid    = CreateFrame( "Frame", btn_name.."_Grid", button.parent );
		
		button.layers.grid:SetBackdrop(
			{
				bgFile   = "Interface\\AddOns\\nUI\\Art\\nUI_BevelboxBg.blp", 
				edgeFile = "Interface\\AddOns\\nUI\\Art\\nUI_BevelboxBorder.blp", 
				tile     = true, 
				tileSize = 1, 
				edgeSize = 5, 
				insets   = {left = 0, right = 0, top = 0, bottom = 0},
			}
		);
	
		button.layers.grid:SetScript( "OnEvent",
		
			function()
				
				if event == "ACTIONBAR_SHOWGRID" then button.layers.grid:SetAlpha( 1 );
				elseif event == "ACTIONBAR_HIDEGRID" then button.layers.grid:SetAlpha( 0 );
				elseif event == "PLAYER_TARGET_CHANGED" then button:updateUsable();
				elseif event == "UPDATE_BINDINGS" then button:updateHotkeys();
				end
				
			end
		);
		
		button.layers.grid:SetAlpha( 0 );
		button.layers.grid:SetAllPoints( button );
		button.layers.grid:SetFrameStrata( button:GetFrameStrata() );
		button.layers.grid:SetFrameLevel( button:GetFrameLevel() );
		button.layers.grid:SetBackdropColor( 0, 0, 0, 0.25 );
		button.layers.grid:RegisterEvent( "ACTIONBAR_SHOWGRID" );
		button.layers.grid:RegisterEvent( "ACTIONBAR_HIDEGRID" );
		button.layers.grid:RegisterEvent( "PLAYER_TARGET_CHANGED" );
		button.layers.grid:RegisterEvent( "UPDATE_BINDINGS" );
		
		---------------------------------------------------------------------------
		-- tweak the macro name label 
		
		if button.layers.name then
			button.layers.name:ClearAllPoints();
			button.layers.name:SetPoint( "TOP", button, "TOP", 1, 0 );
			button.layers.name:SetPoint( "BOTTOM", button, "BOTTOM", 1, 1 );
			button.layers.name:SetPoint( "LEFT", button, "LEFT", 1, 0 );
			button.layers.name:SetPoint( "RIGHT", button, "RIGHT", 1, 0 );
			button.layers.name:SetJustifyH( "CENTER" );
			button.layers.name:SetJustifyV( "BOTTOM" );
			button.layers.name:SetTextColor( 1, 1, 1, 1 );
		end

		---------------------------------------------------------------------------
		-- tone down the autocast animation if present
		
		if button.layers.autocast then		
			button.layers.autocast:SetAlpha( 0.05 );		
		end

		---------------------------------------------------------------------------
		-- extra border on pet frame buttons
		
		if button.layers.normal2 then 
			button.layers.normal2:SetAlpha( 0 ); 
		end			

		---------------------------------------------------------------------------
		-- Blizz stomps on the hotkey text mercilessly, so take control of it
		
		if button.layers.hotkey then 
			button.layers.hotkey:SetAlpha( 0 );
			button.layers.hotkey = button.layers.hotkey:GetParent():CreateFontString( "$parent_nUIHotkey" );
		else
			button.layers.hotkeyFrame = CreateFrame( "Frame", "$parent_nUIHotkeyFrame", button );
			button.layers.hotkey = button.layers.hotkeyFrame:CreateFontString( "$parentLabel" );				
		end
		
		button.layers.hotkey:ClearAllPoints();
		button.layers.hotkey:SetPoint( "TOP", button, "TOP", 1, 0 );
		button.layers.hotkey:SetPoint( "BOTTOM", button, "BOTTOM", 1, 1 );
		button.layers.hotkey:SetPoint( "LEFT", button, "LEFT", 1, 0 );
		button.layers.hotkey:SetPoint( "RIGHT", button, "RIGHT", 1, 0 );			
		button.layers.hotkey:SetJustifyH( "LEFT" );
		button.layers.hotkey:SetJustifyV( "TOP" );
		button.layers.hotkey:SetTextColor( 1, 1, 1, 1 );
				
		---------------------------------------------------------------------------
		-- just to manage position and font size
		
		if button.layers.count then
			button.layers.count:ClearAllPoints();
			button.layers.count:SetPoint( "TOP", button, "TOP", 0, 0 );
			button.layers.count:SetPoint( "BOTTOM", button, "BOTTOM", 0, 1 );
			button.layers.count:SetPoint( "LEFT", button, "LEFT", 1, 0 );
			button.layers.count:SetPoint( "RIGHT", button, "RIGHT", 1, 0 );
			button.layers.count:SetJustifyH( "RIGHT" );
			button.layers.count:SetJustifyV( "BOTTOM" );
			button.layers.count:SetTextColor( 1, 1, 1, 1 );
		end
		
		---------------------------------------------------------------------------
		-- start a new cooldown count
		
		button.startCooldown = function( self, start, duration, enable )
		
			-- start a counter
			
			if start > 0 and duration > 0 and enable > 0 then
				
				button.cdc_start    = start;
				button.cdc_duration = duration;
				
				button:updateCooldown();
				
			-- otherwise, if we're not enabling and we have an active cooldown, cancel it
			
			elseif button.is_cdc then
				
				button.cdc_start    = GetTime();
				button.cdc_duration = -1;
				
				button:updateCooldown();
			
			end
		end	

		-------------------------------------------------------------------------------
		-- set the key binding text
		
		button.updateHotkeys = function()
						
			if button.actionType then 
			
				local actionName = button.actionType..(button.actionID or "");
				
				local key1, key2 = GetBindingKey( actionName ) or
								   GetBindingKey( "CLICK "..btn_name..":LeftButton");

				local text = (key1 or (key2 or "" ));
				
				if ( text == "" ) then
					button.layers.hotkey:SetText( RANGE_INDICATOR );
					button.layers.hotkey:SetAlpha( 0 );
				else
			
					text = strupper( text );
					
					text = text:gsub( "NUMPAD", "|c00FF9966np|r" );
					text = text:gsub( "NUMLOCK", "|c00FF9966nl|r" );
					text = text:gsub( "MOUSEBUTTON ", "|c0000FFFFM|r" );
					text = text:gsub( "MIDDLEMOUSE", "|c0000FFFFMM|r" );
					text = text:gsub( "MOUSEWHEELUP", "|c0000FFFFWU|r" );
					text = text:gsub( "MOUSEWHEELDOWN", "|c0000FFFFWD|r" );
					text = text:gsub( "BACKSPACE", "|c00FF9966Bs|r" );
					text = text:gsub( "SPACEBAR", "|c00FF9966Sp|r" );
					text = text:gsub( "ESCAPE", "|c00FF9966Esc|r" );
					text = text:gsub( "DELETE", "|c00FF9966De|r" );
					text = text:gsub( "HOME", "|c00FF9966Ho|r" );
					text = text:gsub( "CAPSLOCK", "|c00FF9966Caps|r" );
					text = text:gsub( "END", "|c00FF9966En|r" );
					text = text:gsub( "TAB", "|c00FF9966Tab|r" );
					text = text:gsub( "ENTER", "|c00FF9966Ent|r" );
					text = text:gsub( "INSERT", "|c00FF9966Ins|r" );
					text = text:gsub( "PAGEUP", "|c00FF9966Pu|r" );
					text = text:gsub( "PAGEDOWN", "|c00FF9966Pd|r" );
					text = text:gsub( "DOWN", "|c00FF9966D|r" );
					text = text:gsub( "UP", "|c00FF9966U|r" );
					text = text:gsub( "LEFT", "|c00FF9966L|r" );
					text = text:gsub( "RIGHT", "|c00FF9966R|r" );
					text = text:gsub( "DIVIDE", "/" );
					text = text:gsub( "MINUS", "-" );
					text = text:gsub( "PLUS", "+" );
					text = text:gsub( "MULTIPLY", "*" );

					text = text:gsub( "CTRL--", "|c00FF9966c|r")
					text = text:gsub( "ALT--", "|c00FF9966a|r")
					text = text:gsub( "SHIFT--", "|c00FF9966s|r")
					text = text:gsub( "STRG--", "|c00FF9966s|r" );
					text = text:gsub( "MAJ--", "|c00FF9966s|r" );
				
					button.layers.hotkey:SetText( text );
					button.layers.hotkey:SetAlpha( 1 );
				end
			end
		end
		
		-------------------------------------------------------------------------------
		-- called when ActionButton_UpdateUsable is called on a button we have hooked
		
		button.updateUsable = function()
			
			-- there's nothing to do if there's no visible icon
			
			button.layers.icon = _G[btn_name.."Icon"];
		
			if button.layers.icon and button.layers.icon:IsVisible() and button.layers.icon:GetAlpha() > 0 then
			
				local action = button.action;
		
				if action then
					
					local update_vertex = false;
					local is_usable     = false;
					local is_oor        = false;
					local is_oom        = false;
				
					local can_use, oom = IsUsableAction( action );
					
					-- if the spell is usable then we need to clear the oom flag
					
					if can_use == 1 then
			
						is_usable = true;
						
					-- if the spell is not usable because of oom, be sure flag is set
							
					elseif oom == 1 then 
						
						is_usable = true;
						is_oom    = true;
				
					end
					
					-- if this is a consumable action and there's none left to consume, it's unusable
					
					if is_usable and IsConsumableAction( action ) then
						
						local count = GetActionCount( action );
						
						if count <= 0 then 
							is_usable = false; 
						end
						
					end
					
					-- if the action has a range component, then we do oor checks on it
					
					if is_usable and ActionHasRange( action ) then
								
						local in_range = IsActionInRange( action );
						
						if in_range == 0 then
							
							is_oor = true;
						
						elseif in_range == nil and IsActionInRange( action, "player" ) == nil then
							
							is_usable = false;
							
						end
		
					end
					
					-- if we changed any of the three states, save the state change
					
					if is_usable ~= button.is_usable or is_oor ~= button.is_oor or is_oom ~= button.is_oom then
						
						button.is_usable = is_usable;
						button.is_oor    = is_oor;
						button.is_oom    = is_oom;
			
						-- there is no built in mechanism I am aware of to check and see if a spell ID
						-- is usable or not... that is, you can test an action to see if it is usable
						-- or oom and you can check both actions and spells for range, but you cannot
						-- check a spell for usable or oom without an attached action. This hack creates
						-- a cache of spell states by spell ID as we update them on the action buttons.
						-- It isn't perfect, but it's usable. One place this is used is in range 
						-- checking since IsSpellInRange() can return 1 if the spell is unusable but
						-- the target is valid (i.e... hunter Kill Command will return "in range" any
						-- time the command is unusable even if the mob is out of range at the time.
						
						local _,id,_ = GetActionInfo( action );
						
						if id ~= nil then SpellStatus[id] =
						{
							usable = is_usable;
							oor    = is_oor;
							oom    = is_oom;
						}; end
						
					end				
			
					-- color the icon based on the state we last calculated for it
					-- I'd like to only do this when the state changes, but WoW's code
					-- steps on our coloring if we don't do it every time
					
					if not is_usable then 
						button.layers.icon:SetDesaturated( nil );
						button.layers.icon:SetVertexColor( 0.35, 0.35, 0.35, 1 );
					elseif is_oor then
						local color = nUI_ButtonColors["OOR"] or nUI_DefaultConfig.ButtonColors["OOR"];
						button.layers.icon:SetDesaturated( nil );
						button.layers.icon:SetVertexColor( color.r, color.g, color.b, 1 );
					elseif is_oom then
						local color = nUI_ButtonColors["OOM"] or nUI_DefaultConfig.ButtonColors["OOM"];
						button.layers.icon:SetDesaturated( nil );
						button.layers.icon:SetVertexColor( color.r, color.g, color.b, 1 );
					else
						button.layers.icon:SetDesaturated( nil );
						button.layers.icon:SetVertexColor( 1, 1, 1, 1 );
					end
				end
			end	
		end
		
		-------------------------------------------------------------------------------
		-- keep a cooldown counter on the face of applicable buttons
		
		button.updateCooldown = function()
			
			local action      = button.action or ActionButton_CalculateAction( button );
			local type, id, _ = GetActionInfo( action );
			local cooldown    = id and Cooldowns[id] or nil;
			local cdc         = cooldown and cooldown.text or nil;
			local cdc2        = button.layers.cdc;
			local start       = button.cdc_start;
			local duration    = button.cdc_duration;
			local now         = GetTime();
		
			if not (start > 0) and not (duration > 0) and button.is_cdc then
					
				button.is_cdc            = false;
				button.is_flashing       = false;
				button.Timers.cdc_expire = nil;
		
				if cdc then cdc:SetText( "" ); end;
				
				cdc2:SetText( "" );
				
				if cooldown then 
					
					cooldown.done = true;
					if nUI.hud then nUI.hud:updateCooldowns(); end
				end
						
			-- we only process the cooldown after the current time has hit the start time
			
			elseif now < start then
				
				button.Timers.cdc_expire = start - now;
			
			-- otherwise we'll get to work
			
			elseif id then
		
				local remain     = duration - ( now - start );
				local text       = "";
		
				-- again... we only have work to do if there's still time remaining
				
				if remain <= 0 then
				
					-- if we have an active cooldown counter we need to expire it
					-- and we'll return to looking for cooldowns at 12.5fps
					
					if button.is_cdc then
					
						button.is_cdc            = false;
						button.is_flashing       = false;
						button.Timers.cdc_expire = nil;
		
						if cdc then cdc:SetText( "" ); end
		
						cdc2:SetText( "" );
						
						if cooldown then 
							cooldown.done = true;
							if nUI.hud then nUI.hud:updateCooldowns(); end
						end				
					end
					
				else
		
					if not button.is_cdc then
						
						local color = nUI_ButtonColors["CDC1"] or nUI_DefaultConfig.ButtonColors["CDC1"];
						
						if type == "spell" then
							
							local name,_ = GetSpellName( id, BOOKTYPE_SPELL );
			
							-- if this is a spell that we know to be on the global cooldown counter
							-- then we can set the current value of the GCD accordingly. Note that
							-- is a value that can potentially change over time w/spellhaste and 
							-- the like.
							
							if nUI_GCDSpells[name] then nUI.GCD = duration; end
							
						end
						
						-- do not show counters on buttons that are just spinning the global cooldown
						
						if duration <= (nUI.GCD or 2) then 
							if nUI.hud then nUI.hud:startGCD( duration ); end
							return; 
						end
		
						-- this duration is not GCD, so run it...
						
						button.is_cdc      = true;
						button.is_flashing = false;
		
						if not cooldown then
							
							Cooldowns[id] = {};
							
							cooldown = Cooldowns[id];
						end
						
						cooldown.id       = id;
						cooldown.action   = action;
						cooldown.start    = start;
						cooldown.duration = duration;
						cooldown.done     = false;
						cooldown.icon     = button.layers.icon;
		
						if nUI.hud then nUI.hud:updateCooldowns(); end
						
						cdc = cooldown.text;
						
						cdc2:SetTextColor( color.r, color.g, color.b, 1 );
						
					elseif not button.is_flashing and remain < 10 then
						
						local color        = nUI_ButtonColors["CDC2"] or nUI_DefaultConfig.ButtonColors["CDC2"];
						button.is_flashing = true;
						
						if cdc then
							cdc:SetTextColor( color.r, color.g, color.b, 1 );
						end
		
						cdc2:SetTextColor( color.r, color.g, color.b, 1 );
		
						-- flag the item as flashing in the cooldown table so the hud
						-- can modify the cooldown display
						
						if cooldown then 
							cooldown.flashing = true; 
							if nUI.hud then nUI.hud:updateCooldowns(); end
						end				
					end
					
					-- in the interest of efficiency, we set our time to expire for
					-- the next cooldown count check based on how much time is left.
		
					local text = nUI_DurationText( remain );
					
					-- over an hour remaining
					
					if remain > 3600 then 
						
						button.Timers.cdc_expire = remain % 3600;
						
					-- over one minute remaining
		
					elseif remain > 60 then 
					
						button.Timers.cdc_expire = remain % 60;
						
					-- more than a second to go
		
					elseif remain > 1 then 
					
						button.Timers.cdc_expire = remain % 1;
						
					-- less than a second
		
					else 
					
						button.Timers.cdc_expire = remain - math.floor( remain * 10 ) / 10;
						
					end
					
					-- if we have a text object, update it, otherwise we'll recheck at 12.5fps
					
					if cdc then cdc:SetText( text ); cdc2:SetText( "" );
					elseif not cooldown then button.Timers.cdc_expire = 0.08;
					else cdc2:SetText( text );
					end
				end
			end
		end

		-------------------------------------------------------------------------------
		-- called when ActionButton_OnUpdate is called on a button we have hooked
		
		button.onUpdate = function( self, elapsed )
		
			local do_action_update = false;
			local visible = true;
		
			-- check the button for visibility. If there's no icon for the button 
			-- or the icon is not shown, then we need to hide the cooldown count
			-- and hotkey. Likewise, there's no reason to do any other work if
			-- there's no visible buttons
			
			button.layers.icon = _G[btn_name.."Icon"];
			
			if not button.layers.icon or not button.layers.icon:IsVisible() then
				visible = false;
			end
		
			if not visible then
			
				-- if we're not visible, then hide our custom layers, too
				
				if not button.hidden then
					button.layers.hotkey:SetAlpha( 0 );
					button.layers.cdc:SetAlpha( 0 );
					button.hidden = true;
				end
				
			else
				
				-- otherwise, if the layers are hidden, show them
				
				if button.hidden then					
					button.layers.hotkey:SetAlpha( 1 );
					button.layers.cdc:SetAlpha( 1 );
					button.hidden = nil;					
				end
			
				-- check for updates to the out of range and out of mana status
				
				button.Timers.range = button.Timers.range + elapsed;
				
				if button.Timers.range >= 0.08 then -- update at 12.5fsp
				
					button.Timers.range = 0;
					do_action_update  = true;
								
				end
		
				-- check for update to cooldown counter
		
				if button.Timers.cdc_expire then
					
					button.Timers.cdc = button.Timers.cdc + elapsed;
					
					if button.Timers.cdc >= button.Timers.cdc_expire then -- rate varies by time remaining
						
						button.Timers.cdc = 0;
					
						button:updateCooldown();
						
					end
				end
			end
			
			return do_action_update;
		
		end
	end	
	
	-- these steps are performed when the button is first initialized and any 
	-- time the display size changes or anything may alter scaling of the button

	local scale  = button:GetEffectiveScale();
	local height = button:GetHeight();
	
	button.layers.cdcFrame:SetScale( 1.0 / scale );
	button.layers.cdc:SetFont( "Fonts/ARIALN.TTF", height * scale * 0.52, "OUTLINE" );
	button.layers.hotkey:SetFont( "Fonts/ARIALN.TTF", height / 2.5, "OUTLINE" );
	button.layers.count:SetFont( "Fonts/ARIALN.TTF", height / 2.5, "OUTLINE" );
	button.layers.name:SetFont( "Fonts/ARIALN.TTF", height / 3, "OUTLINE" );

	-- tweak some of the layer scales to make them "fit" better
	
	if button.layers.border then
		
		local layer = button.layers.border;
		local inset = height * nUI.scale * 0.95;
		
		layer:ClearAllPoints();
		layer:SetPoint( "TOPLEFT", button, "TOPLEFT", -inset, inset );
		layer:SetPoint( "TOPRIGHT", button, "TOPRIGHT", inset, inset );
		layer:SetPoint( "BOTTOMLEFT", button, "BOTTOMLEFT", -inset, -inset );
		layer:SetPoint( "BOTTOMRIGHT", button, "BOTTOMRIGHT", inset, -inset );
	end	
	
	-- initialize the button state
	
	button:updateHotkeys();
	button:updateUsable();
	
	if button.action then
		
		local start, duration, enable = GetActionCooldown( button.action );
		button:startCooldown( start, duration, enable );
		
	end	
end
