------------------------------------------------
--                CT_BuffMod                  --
--                                            --
-- Mod that allows you to heavily customize   --
-- the display of buffs to your liking.       --
-- Please do not modify or otherwise          --
-- redistribute this without the consent of   --
-- the CTMod Team. Thank you.                 --
------------------------------------------------

--------------------------------------------
-- Initialization

local module = { };
local _G = getfenv(0);

local MODULE_NAME = "CT_BuffMod";
local MODULE_VERSION = strmatch(GetAddOnMetadata(MODULE_NAME, "version"), "^([%d.]+)");

module.name = MODULE_NAME;
module.version = MODULE_VERSION;

_G[MODULE_NAME] = module;
CT_Library:registerModule(module);

--------------------------------------------
-- Variables & Options

-- Variables
local newObjectDummy = true
local buffModFrame;
local maxBuffAmount;

-- Options
local showBuffTimers = true;
local expandBuffs = true;
local frameWidth, frameHeight = 275, 390;
local sortType = 2;
local subSortType = 1;
local buffSize, buffSpacing, effectiveBuffSize = 20, 0, 20;
local expirationWarningTime1, expirationWarningTime2,
	expirationWarningTime3 = 15, 60, 180;

--------------------------------------------
-- Local copies

local ceil = ceil;
local sort = sort;
local format = format;
local type = type;

--------------------------------------------
-- BuffObject handling

local buffObject = { };

-- Metatable for the buff object
local buffObjectMeta = { __index = buffObject };

-- List of all buff objects...
local buffObjectList = { };
module.buffList = buffObjectList;

-- and also a way to execute a given method on all objects
local lastMethod;
local function doMethod(...)
	local method = buffObject[lastMethod];
	if ( method ) then
		for key, value in ipairs(buffObjectList) do
			method(value, select(2, ...));
		end
	end
end
setmetatable(buffObjectList, { __index = function(key, value)
	local obj = buffObject[value];
	if ( type(obj) == "function" ) then
		lastMethod = value; return doMethod;
	else return obj; end end });

-- Get a new buff object
buffObjectPool = { };
function module:getBuffObject()
	local obj = tremove(buffObjectPool) or { };
	setmetatable(obj, buffObjectMeta);
	return obj;
end

--------------------------------------------
-- BuffObject

--------------------
-- Frame skeleton

local function buttonOnLoad(self)
	self.texture:SetTexCoord(0.1, 0.9, 0.1, 0.9);
	self:RegisterForClicks("LeftButtonUp", "RightButtonUp");
end
local function buttonOnEnter(self)
	local parent = self.parent;
	GameTooltip:SetOwner(self, "ANCHOR_CURSOR");
	if ( parent.buffObject.type == "ITEM" ) then
		GameTooltip:SetInventoryItem("player", parent:GetID());
	else
		GameTooltip:SetPlayerBuff(parent:GetID());
	end
end
local function buttonOnClick(self, button)
	local parent = self.parent;
	local buffObject = parent.buffObject;
	
	if ( buffObject.type ~= "ITEM" ) then
		if ( button == "RightButton" ) then
			CancelPlayerBuff(parent:GetID());
		elseif ( button == "LeftButton" and IsControlKeyDown() ) then
			CastSpellByName(buffObject.name, "player");
		end
	end
end

local buffObjectData;
local function buffObjectSkeleton()
	if ( not buffObjectData ) then
		buffObjectData = {
			["button#i:icon#l"] = {
				"texture#i:texture#all",
				["onload"] = buttonOnLoad,
				["onclick"] = buttonOnClick,
				["onenter"] = buttonOnEnter,
				["onleave"] = module.hideTooltip
			}
		};
	end
	
	return "frame#s:275:30", buffObjectData;
end

--------------------
-- Helper functions

local function updateKeyBind()
	if ( buffModFrame ) then
		local bindKey = GetBindingKey("CT_BUFFMOD_RECASTBUFFS");
		if ( bindKey ) then
			SetOverrideBindingClick(buffModFrame, false, bindKey, "CT_BUFFMOD_RECASTBUFFFRAME");
		else
			ClearOverrideBindings(buffModFrame);
		end
	end
end

local function getFrame()
	local frame = module:getFrame(buffObjectSkeleton, buffModFrame);
	frame:SetWidth(frameWidth);
	return frame;
end

local function computeFrameHeight()
	local height = 10 + #buffObjectList * effectiveBuffSize - buffSpacing;
	if ( frameHeight > height ) then
		height = frameHeight;
	end
	return height;
end

local function updatePositions(minPosition)
	local obj;
	for i = minPosition, #buffObjectList, 1 do
		obj = buffObjectList[i];
		obj.index = i;
		obj:position();
	end
end

local function updateIndices(minIndex)
	local index;
	for key, object in ipairs(buffObjectList) do
		if ( object.type ~= "ITEM" ) then
			index = object.auraIndex;
			if ( index > minIndex ) then
				index = index - 1;
				object.auraIndex = index;
				object.frame:SetID(index);
			end
		end
	end
end

local flashModelPool = { };
local function dropFlash(self)
	tinsert(flashModelPool, self);
end

local function getFlash()
	local model = tremove(flashModelPool);
	if ( not model ) then
		model = CreateFrame("Cooldown", nil, frame, "CooldownFrameTemplate");
	end
	return model;
end

local function updateDisplayStatus(isRemoving)
	local newHeight, numBuffs = computeFrameHeight(expandBuffs);
	local height = buffModFrame:GetHeight();

	if ( not expandBuffs ) then
		local numBuffs = floor ( ( frameHeight - 10 + buffSpacing ) / effectiveBuffSize * 1000 + 0.5) / 1000;
		for key, value in ipairs(buffObjectList) do
			if ( key > numBuffs ) then
				value.frame:Hide();
			else
				value.frame:Show();
			end
		end
		
		buffModFrame:SetHeight(frameHeight);
		return;
	end
	
	for key, value in ipairs(buffObjectList) do
		value.frame:Show();
	end
	
	if ( isRemoving ) then
		if ( height > newHeight ) then
			buffModFrame:SetHeight(newHeight);
		end
	else
		if ( newHeight > height ) then
			buffModFrame:SetHeight(newHeight);
		end
	end
end

-- Update 50 times/sec
local function update50(self, elapsed)
	local update = self.update - elapsed;
	if ( update <= 0 ) then
		local buffObject = self.buffObject;
		local newTime = buffObject.timeleft - 0.02 + update;
		buffObject.timeleft = newTime;
		buffObject:updateTimer(newTime);
		self.update = 0.02;
	else
		self.update = update;
	end
end

-- Update 20 times/sec
local function update20(self, elapsed)
	local update = self.update - elapsed;
	if ( update <= 0 ) then
		local buffObject = self.buffObject;
		local newTime = buffObject.timeleft - 0.05 + update;
		buffObject.timeleft = newTime;
		buffObject:updateTimer(newTime);
		self.update = 0.05;
	else
		self.update = update;
	end
end

-- Update 2 times/sec
local function update2(self, elapsed)
	local update = self.update - elapsed;
	if ( update <= 0 ) then
		local buffObject = self.buffObject;
		local newTime = buffObject.timeleft - 0.5 + update;
		buffObject.timeleft = newTime;
		buffObject:updateTimer(newTime);
		self.update = 0.5;
	else
		self.update = update;
	end
end

-- Update 1 time/sec
local function update1(self, elapsed)
	local update = self.update - elapsed;
	if ( update <= 0 ) then
		local buffObject = self.buffObject;
		local newTime = buffObject.timeleft - 1 + update;
		buffObject.timeleft = newTime;
		buffObject:updateTimer(newTime);
		self.update = 1;
	else
		self.update = update;
	end
end

-- Update expiring buff
local function updateExpire(self, elapsed)
	local update = self.update - elapsed;
	if ( update <= 0 ) then
		local buffObject = self.buffObject;
		local newTime = buffObject.timeleft - 0.02 + update;
		buffObject.timeleft = newTime;
		buffObject:updateTimer(newTime);
		
		newTime = newTime % 2;
		
		if ( newTime > 1 ) then
			self.icon:SetAlpha( 2 - newTime);
		else
			self.icon:SetAlpha( newTime );
		end
		self.update = 0.02;
	else
		self.update = update;
	end
end

local function setUpdater(frame, timeleft)
	
	if ( timeleft <= 60 ) then
		frame:SetScript("OnUpdate", update50);
	elseif ( timeleft <= 240 ) then
		frame:SetScript("OnUpdate", update20);
	elseif ( timeleft <= 540 ) then
		frame:SetScript("OnUpdate", update2);
	else
		frame:SetScript("OnUpdate", update1);
	end
end

local function getUpdateInterval(timeleft)
	if ( timeleft <= 60 ) then
		return 0.02;
	elseif ( timeleft <= 240 ) then
		return 0.05;
	elseif ( timeleft <= 540 ) then
		return 0.5;
	else
		return 1;
	end
end

local function dummyUpdate(self, elapsed)
	if ( not newObjectDummy ) then
		setUpdater(self, self.buffObject.timeleft);
	end
end

-- Sort method 1 (type)
local buffSubSort;
local buffSortTypes = { AURA = 1, BUFF = 2, ITEM = 3, DEBUFF = 4 };
local function buffSort1(t1, t2)
	local t1type, t2type = t1.type, t2.type;
	
	if ( t1type == t2type ) then
		return buffSubSort(t1, t2);
	else
		return buffSortTypes[t1type] < buffSortTypes[t2type];
	end
end

-- Sort method 2 (time)
local function buffSort2(t1, t2)
	local t1time, t2time = t1.timeleft, t2.timeleft;
	if ( t1time == 0 ) then
		return ( t2time > 0 or t1.name < t2.name );
	elseif ( t2time == 0 ) then
		return false;
	else
		return t1time > t2time;
	end
end

-- Sort method 3 (order)
local function buffSort3(t1, t2)
	return t1.num < t2.num;
end

-- Sort method 4 (name)
local function buffSort4(t1, t2)
	return t1.name < t2.name;
end

-- 1 hour / 35 minutes
local function timeFormat1(timeLeft)
	timeLeft = ceil(timeLeft);
	if ( timeLeft > 3540 ) then
		-- Hours
		local hours = ceil(timeLeft/3600);
		if ( hours ~= 1 ) then
			return format("%d hours", hours);
		else
			return "1 hour";
		end
	elseif ( timeLeft > 60 ) then
		-- Minutes
		local minutes = ceil(timeLeft/60);
		if ( minutes ~= 1 ) then
			return format("%d minutes", minutes);
		else
			return "1 minute";
		end
	else
		-- Seconds
		if ( timeLeft ~= 1 ) then
			return format("%d seconds", timeLeft);
		else
			return "1 second";
		end
	end
end

-- 1 hour / 35 min
local function timeFormat2(timeLeft)
	timeLeft = ceil(timeLeft);
	if ( timeLeft > 3540 ) then
		-- Hours
		local hours = ceil(timeLeft/3600);
		if ( hours ~= 1 ) then
			return format("%d hours", hours);
		else
			return "1 hour";
		end
	elseif ( timeLeft > 60 ) then
		-- Minutes
		return format("%d min", ceil(timeLeft/60));
	else
		-- Seconds
		return format("%d sec", timeLeft);
	end
end

-- 1h / 35m
local function timeFormat3(timeLeft)
	timeLeft = ceil(timeLeft);
	if ( timeLeft > 3540 ) then
		-- Hours
		return format("%dh", ceil(timeLeft/3600));
	elseif ( timeLeft > 60 ) then
		-- Minutes
		return format("%dm", ceil(timeLeft/60));
	else
		-- Seconds
		return format("%ds", timeLeft);
	end
end

-- 1h35m / 35m30s
local function timeFormat4(timeLeft)
	timeLeft = ceil(timeLeft);
	if ( timeLeft > 3540 ) then
		-- Hours & Minutes
		local hours = ceil(timeLeft/3600);
		return format("%dh %dm", hours, floor((timeLeft-hours*3600)/60));
	elseif ( timeLeft > 60 ) then
		-- Minutes & Seconds
		return format("%dm %.2ds", floor(timeLeft/60), timeLeft%60);
	else
		-- Seconds
		return format("%ds", timeLeft);
	end
end

-- 1:35h / 1:35 / 0:35
local function timeFormat5(timeLeft)
	timeLeft = ceil(timeLeft);
	if ( timeLeft > 3540 ) then
		-- Hours & Minutes
		local hours = ceil(timeLeft/3600);
		return format("%d:%.2dh", hours, floor((timeLeft-hours*3600)/60));
	else
		-- Minutes & Seconds
		return format("%.2d:%.2d", floor(timeLeft/60), timeLeft%60);
	end
end

getTimeFormat = timeFormat1;
module.humanizeTime = timeFormat1;

-- Recasting buffs
local buffQueue;
local buffButton = CreateFrame("Button", "CT_BUFFMOD_RECASTBUFFFRAME", nil, "SecureActionButtonTemplate");
buffButton:SetAttribute("unit", "player");
buffButton:SetAttribute("type", "spell");
buffButton:SetScript("PreClick", function(self)
	if ( buffQueue and not self:GetAttribute("spell") ) then
		self:SetAttribute("spell", buffQueue[#buffQueue]);
	end
end);
buffButton:SetScript("PostClick", function(self)
	local spell = self:GetAttribute("spell");
	self:SetAttribute("spell", nil);
	if ( buffQueue and spell ) then
		for i = #buffQueue, 1, -1 do
			if ( buffQueue[i] == spell ) then
				tremove(buffQueue, i);
				return;
			end
		end
	end
end);

local function setRecastSpell(spellName)
	buffButton:SetAttribute("spell", spellName);
end

local function queueBuffRecast(buffName)
	if ( not buffQueue ) then
		buffQueue = { };
	end

	setRecastSpell(buffName);

	-- Make sure it's not in here already
	for key, value in ipairs(buffQueue) do
		if ( value == buffName ) then
			return;
		end
	end

	tinsert(buffQueue, buffName);
	return true;
end

local function removeBuffRecast(buffName)
	if ( buffQueue ) then
		local num = #buffQueue;
		for key, value in ipairs(buffQueue) do
			if ( value == buffName ) then
				if ( key == num ) then
					setRecastSpell(buffQueue[num-1]);
				end
				tremove(buffQueue, key);
				return;
			end
		end
	end
end

module:regEvent("UPDATE_BINDINGS", updateKeyBind);
	

--------------------
-- Methods

local num = 0;

function buffObject:flash()
	--[[
	-- Commented out because there is no good way to catch when the animation is finished.
	
	local model = getFlash();
	local frame = self.frame;
	
	model:SetParent(frame);
	model:SetAllPoints(frame.icon);
	model:Show();
	model:SetCooldown(0, 1.5);
	--]]
end

function buffObject:getIndex()
	if ( sortType == 1 ) then
		-- Type
		local type = self.type;
		local typeIndex = buffSortTypes[type];
		local obj, objType;
		
		for i = #buffObjectList, 1, -1 do
			obj = buffObjectList[i];
			if ( obj ~= self ) then
				objType = obj.type;
				if ( objType == type ) then
					if ( not buffSubSort(self, obj) ) then
						return i+1;
					end
				elseif ( typeIndex > buffSortTypes[objType] ) then
					return i+1;
				end
			end
		end
		
		return 1;

	elseif ( sortType == 2 ) then
		-- Time
		local timeleft, initialTime, name = self.timeleft, self.initialTime, self.name;
		local obj, objTimeleft, objInitialTime;
		for i = #buffObjectList, 1, -1 do
			obj = buffObjectList[i];
			if ( obj ~= self ) then
				objTimeleft, objInitialTime = obj.timeleft, obj.initialTime;
				if ( initialTime == 0 ) then
					if ( objInitialTime == 0 and name > buffObjectList[i].name ) then
						return i+1;
					end
				elseif ( objInitialTime == 0 ) then
					return i+1;
				elseif ( timeleft < objTimeleft ) then
					return i+1;
				end
			end
		end
		
		return 1;
		
	elseif ( sortType == 3 ) then
		-- Order
		local num, index = self.num, self.index;
		for i = #buffObjectList, 1, -1 do
			if ( i ~= index ) then
				if ( num > buffObjectList[i].num ) then
					return i+1;
				end
			end
		end
		return 1;
		
	elseif ( sortType == 4 ) then
		-- Name
		local name, index, objName = self.name, self.index;
		
		for i = #buffObjectList, 1, -1 do
			if ( i ~= index ) then
				objName = buffObjectList[i].name;
				if ( name > objName ) then
					return i+1;
				end
			end
		end
		
		return 1;
	end
end

function buffObject:sort()
	-- Remove ourselves from the last position
	local oldIndex = self.index;
	if ( oldIndex ) then
		tremove(buffObjectList, oldIndex);
	end
	
	-- Update to our new index
	local index = self:getIndex();
	tinsert(buffObjectList, index, self);
	
	-- Update all frames below this one
	if ( oldIndex and oldIndex < index ) then
		updatePositions(oldIndex);
	else
		updatePositions(index);
	end
end

function buffObject:position()
	local frame = self.frame;
	frame:ClearAllPoints();
	if ( module.expandUpwards ) then
		frame:SetPoint("BOTTOMLEFT", buffModFrame, 5, (5-effectiveBuffSize) + self.index * effectiveBuffSize);
	else
		frame:SetPoint("TOPLEFT", buffModFrame, 5, (effectiveBuffSize-5) - self.index * effectiveBuffSize);
	end
end

function buffObject:updateDimension()
	local frame = self.frame;
	local icon = frame.icon;
	local spark = frame.spark;
	
	if ( spark ) then
		spark:SetWidth(min(buffSize * 2/3, 25));
		spark:SetHeight(buffSize * 1.9);
	end
	
	icon:SetHeight(buffSize);
	icon:SetWidth(buffSize);
	frame:SetHeight(buffSize);
	self:updateTimer();
end

function buffObject:display()
	local frame = self.frame;
	local type = self.type;
	local name = frame.name;
	local timeleft = frame.timeleft;
	local background = frame.background;
	local bgtimer = frame.bgtimer;
	local icon = frame.icon;
	local spark = frame.spark;
	local durationBelow;

	-- Set up the frame
	if ( module.colorBuffs ) then
		if ( not background ) then
			background = frame:CreateTexture(nil, "BACKGROUND");
			background:SetTexture("Interface\\AddOns\\CT_BuffMod\\Images\\barSmooth");
			frame.background = background;
		end
		if ( showBuffTimers ) then
			if ( not spark ) then
				spark = frame:CreateTexture(nil, "BORDER");
				spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark");
				spark:SetBlendMode("ADD");
				frame.spark = spark;
			end
			if ( module.showTimerBackground ) then
				if ( not bgtimer ) then
					bgtimer = frame:CreateTexture(nil, "BACKGROUND");
					bgtimer:SetTexture("Interface\\AddOns\\CT_BuffMod\\Images\\barSmooth");
					frame.bgtimer = bgtimer;
				end
			elseif ( bgtimer ) then
				bgtimer:Hide();
				bgtimer = nil;
			end
		elseif ( spark ) then
			spark:Hide();
			spark = nil;
		end
	elseif ( background ) then
		spark:Hide();
		spark = nil;
		background:Hide();
		background = nil;
		if ( bgtimer ) then
			bgtimer:Hide();
			bgtimer = nil;
		end
	end
	
	if ( module.showTimers ) then
		if ( not timeleft ) then
			timeleft = frame:CreateFontString(nil, "ARTWORK", "ChatFontNormal");
			frame.timeleft = timeleft;
		end
	elseif ( timeleft ) then
		timeleft:Hide();
		timeleft = nil;
	end
	
	if ( module.showNames ) then
		if ( not name ) then
			name = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal");
			frame.name = name;
		end
	elseif ( name ) then
		name:Hide();
		name = nil;
	end
	
	if ( name and timeleft and type ~= "AURA" and buffSize >= 23 and module.durationBelow ) then
		durationBelow = true;
	end
	
	-- Position the elements
	if ( module.rightAlign ) then
		icon:ClearAllPoints();
		icon:SetPoint("RIGHT", frame, -10, 0);
		
		if ( name ) then
			name:ClearAllPoints();
			name:SetJustifyH("RIGHT");
			name:Show();
			
			if ( timeleft and type ~= "AURA" ) then
				if ( durationBelow ) then
					name:SetPoint("BOTTOMRIGHT", icon, "LEFT", -5, 2);
					name:SetPoint("LEFT", frame);
				else
					name:SetPoint("RIGHT", icon, "LEFT", -5, 0);
					name:SetPoint("LEFT", timeleft, "RIGHT");
				end
			else
				name:SetPoint("RIGHT", icon, "LEFT", -5, 0);
				name:SetPoint("LEFT", frame);
			end
		end
		
		if ( timeleft ) then
			timeleft:ClearAllPoints();
			timeleft:Show();
			
			if ( name ) then
				if ( durationBelow ) then
					timeleft:SetPoint("TOPRIGHT", icon, "LEFT", -5, 3);
				else
					timeleft:SetPoint("LEFT", frame, 0, 0);
				end
			else
				timeleft:SetPoint("RIGHT", icon, "LEFT", -5, 0);
			end
		end
		
		if ( background ) then
			background:ClearAllPoints();
			background:SetPoint("TOPRIGHT", icon, "TOPLEFT");
			background:SetPoint("BOTTOM", frame);
			if ( spark ) then
				spark:ClearAllPoints();
				spark:SetPoint("CENTER", background, "LEFT");
			end
			
			if ( bgtimer ) then
				bgtimer:ClearAllPoints();
				bgtimer:SetPoint("TOPRIGHT", background, "TOPLEFT");
				bgtimer:SetPoint("BOTTOMLEFT", frame);
				bgtimer:Show();
			end
		end
	else
		icon:ClearAllPoints();
		icon:SetPoint("LEFT", frame);
		
		if ( name ) then
			name:ClearAllPoints();
			name:SetJustifyH("LEFT");
			name:Show();
			
			if ( timeleft and type ~= "AURA" ) then
				if ( durationBelow ) then
					name:SetPoint("BOTTOMLEFT", icon, "RIGHT", 5, 2);
					name:SetPoint("RIGHT", frame);
				else
					name:SetPoint("LEFT", icon, "RIGHT", 5, 0);
					name:SetPoint("RIGHT", timeleft, "LEFT");
				end
			else
				name:SetPoint("LEFT", icon, "RIGHT", 5, 0);
				name:SetPoint("RIGHT", frame, -10, 0);
			end
		end
		
		if ( timeleft ) then
			timeleft:ClearAllPoints();
			timeleft:Show();
			
			if ( name ) then
				if ( durationBelow ) then
					timeleft:SetPoint("TOPLEFT", icon, "RIGHT", 5, 3);
				else
					timeleft:SetPoint("RIGHT", frame, -10, 0);
				end
			else
				timeleft:SetPoint("LEFT", icon, "RIGHT", 5, 0);
			end
		end
		
		if ( background ) then
			background:ClearAllPoints();
			background:SetPoint("TOPLEFT", icon, "TOPRIGHT");
			background:SetPoint("BOTTOM", frame);
			if ( spark ) then
				spark:ClearAllPoints();
				spark:SetPoint("CENTER", background, "RIGHT");
			end
			
			if ( bgtimer ) then
				bgtimer:ClearAllPoints();
				bgtimer:SetPoint("TOPLEFT", background, "TOPRIGHT");
				bgtimer:SetPoint("BOTTOMRIGHT", frame, -10, 0);
				bgtimer:Show();
			end
		end
	end
	
	-- Set icon, frame & name settings
	icon.texture:SetTexture(self.texture);
	self:updateDimension();
	if ( name ) then
		name:SetText(self.name);
	end

	-- Set flash & timer
	if ( background ) then
		background:Show();
		
		-- Set our color
		local initialTime = self.initialTime;
		
		if ( name ) then
			name:SetTextColor(1, 0.82, 0);
		end
		
		if ( type == "AURA" ) then
			background:SetVertexColor(0.35, 0.8, 0.15, 0.5);
		elseif ( type == "BUFF" ) then
			background:SetVertexColor(0.1, 0.4, 0.85, 0.5);
		elseif ( type == "ITEM" ) then
			background:SetVertexColor(0.75, 0.25, 1, 0.75);
		elseif ( type == "DEBUFF" ) then
			background:SetVertexColor(1, 0, 0, 0.85);
		end
		
		-- Set background & spark
		if ( not showBuffTimers or type == "AURA" ) then
			background:SetWidth(frameWidth - 10 - buffSize);
			if ( spark ) then
				spark:Hide();
			end
		else
			if ( bgtimer ) then
				local r, g, b, a = background:GetVertexColor();
				bgtimer:SetVertexColor(r/1.35, g/1.35, b/1.35, a/2);
			end
			self:updateTimer(nil, initialTime);
		end
	end
	
	if ( type == "DEBUFF" and name ) then
		if ( module.colorCodeDebuffs ) then
			local dispelType = GetPlayerBuffDispelType(self.auraIndex);
			if ( dispelType ) then
				local color = DebuffTypeColor[dispelType];
				name:SetTextColor(color.r, color.g, color.b);
			else
				name:SetTextColor(1, 0.82, 0);
			end
		elseif ( background ) then
			name:SetTextColor(1, 0.82, 0);
		else
			name:SetTextColor(1, 0, 0);
		end
	end
end

function buffObject:checkExpiration(timeLeft)
	timeLeft = timeLeft or self.timeleft;
	
	if ( timeLeft == 0 ) then
		return;
	end
	
	-- Flash icons?
	if ( timeLeft <= 15 and module.flashIcons and not self.isFlashing ) then
		self.frame:SetScript("OnUpdate", updateExpire);
		self.isFlashing = true;
	end
	
	-- Expiration warning
	local initialTime = self.initialTime;
	if ( initialTime >= 115 and
	     not self.displayedExpirationWarning and
	     module.enableExpiration and
	     self.type ~= "DEBUFF" and
	     ( ( initialTime >= 1805 and timeLeft <= expirationWarningTime3 )
	       or ( initialTime >= 605 and timeLeft <= expirationWarningTime2 )
	       or ( timeLeft <= expirationWarningTime1 ) )
	    ) then
	     
	     	-- Check options
	     	local canRecastKeyBind;
	     	local name = self.name;
	     	if ( not module:getSpell(name) ) then
	     		if ( module.expirationCastOnly ) then
	     			return;
	     		end
	     	else
	     		-- Add to recast queue
	     		canRecastKeyBind = queueBuffRecast(name) and GetBindingKey("CT_BUFFMOD_RECASTBUFFS");
	     	end
	     	
		-- Display the expiration message
		self.displayedExpirationWarning = true;
		if ( canRecastKeyBind ) then
			module:printformat(module:getText("PRE_EXPIRATION_WARNING_KEYBINDING"),
				name, timeFormat1(timeLeft), canRecastKeyBind);
		else
			module:printformat(module:getText("PRE_EXPIRATION_WARNING"),
				name, timeFormat1(timeLeft));
		end
		
		-- Play sound
		if ( module.expirationSound ) then
			PlaySoundFile("Sound\\Spells\\misdirection_impact_head.wav");
		end
	end
end

function buffObject:add(auraIndex, charges)
	-- Create our object & frame
	local frame = getFrame();
	local timeleft = self.timeleft;
	local updateInterval = getUpdateInterval(timeleft);
	
	frame.buffObject = self;
	frame:SetID(auraIndex);
	
	-- Set our OnUpdate script
	if ( timeleft > 0 ) then
		frame.update = updateInterval;
		if ( newObjectDummy ) then
			frame:SetScript("OnUpdate", dummyUpdate);
		else
			setUpdater(frame, timeleft);
		end
	else
		frame.update = 0;
	end
	
	-- Set up our object
	self.auraIndex = auraIndex;
	self.frame = frame;
	self.initialTime = timeleft;
	self.updateInterval = updateInterval;
	self:display();
	self:renew(timeleft, charges);
end

function buffObject:renew(timeleft, charges)
	local frame = self.frame;
	if ( ( sortType == 3 and not module.keepRecastPosition ) or not self.num ) then
		num = num + 1;
		self.num = num;
	end
	
	self.initialTime = timeleft;
	
	-- Remove this from buff recasting
	removeBuffRecast(self.name);
	
	if ( self.isFlashing ) then
		-- If we were previously fading in/out, restore to the normal OnUpdate
		setUpdater(frame, timeleft);
	end
	self.displayedExpirationWarning = nil;
	self.isFlashing = nil;
	
	frame:Show();
	frame.icon:SetAlpha(1);
	
	-- Update position & time
	self:sort();
	self:updateTimeDisplay(timeleft, charges);
	
	-- Add option to toggle this behavior
	self:flash();
	
	-- Update display
	updateDisplayStatus();
end

function buffObject:drop()
	
	-- Clear things up
	local frame = self.frame;
	tremove(buffObjectList, self.index);
	updatePositions(self.index);
	if ( self.type ~= "ITEM" ) then
		updateIndices(self.auraIndex);
	end
	
	updateDisplayStatus(true);
	frame:Hide();
	frame:SetScript("OnUpdate", nil);
	self.index = nil;
	self.charges = nil;
	self.type = nil;
	self.num = nil;
	
	tinsert(buffObjectPool, self);
end

function buffObject:updateCharges(itemBuffCharges)
	local charges = itemBuffCharges or GetPlayerBuffApplications(self.auraIndex);
	
	local icon = self.frame.icon;
	local iconText = icon.text;
	
	if ( charges and charges > 0 ) then
		if ( not iconText ) then
			iconText = icon:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall");
			iconText:SetPoint("BOTTOMRIGHT", icon, 5, 0);
			iconText:SetFont("ARIALN.TTF",12,"MONOCHROME");
		end
		iconText:SetText(charges or "");
		icon.text = iconText;
	elseif ( iconText ) then
		iconText:SetText("");
	end
	self.charges = charges;
end

function buffObject:updateTimer(timeLeft, initialTime)
	local frame = self.frame;
	local background = frame.background;
	if ( not background ) then
		return;
	end
	local spark = frame.spark;
	
	initialTime = ( initialTime or self.initialTime );
	
	if ( not showBuffTimers or initialTime == 0 ) then
		if ( spark ) then
			spark:Hide();
		end
		background:SetWidth(frameWidth-buffSize-10);
	else
		if ( spark ) then
			spark:Show();
		end
		background:SetWidth(max((frameWidth-buffSize-10) * 
			min( ( timeLeft or self.timeleft ) / initialTime, 1), 0.01));
	end
end

function buffObject:updateTimeDisplay(timeLeft, itemBuffCharges)
	local frame = self.frame;
	if ( itemBuffCharges ) then
		-- We have an item buff; update texture all the time
		frame.icon.texture:SetTexture(self.texture);
	end
	if ( self.initialTime > 0 ) then
		timeLeft = timeLeft or self.timeleft;
		local timeleft = frame.timeleft;
		if ( timeleft ) then
			timeleft:SetText(getTimeFormat(timeLeft));
		end
		self.frame.update = self.updateInterval;
		
		-- Check expiration warning
		self:checkExpiration(timeLeft);
	end
	
	-- Update charges
	self:updateCharges(itemBuffCharges);
end

--------------------------------------------
-- Main BuffMod Frame

local function updateDimensions(width, height)
	frameWidth, frameHeight = width, height;
	maxBuffAmount = floor((height-10) / buffSize);
	
	for key, value in ipairs(buffObjectList) do
		value.frame:SetWidth(width);
		value:updateTimer();
	end
	updateDisplayStatus();
end

local function dragUpdate(self, elapsed)
	local x, y = GetCursorPosition();
	local width = ( x / self.scale ) - self.center + self.left;
	local height = max(self.top - ( y / self.scale ), 25);
	local minHeight = computeFrameHeight();
	
	width = min(max(width, 40),500);
	self.parent:SetWidth(width);

	if ( not expandBuffs or height > minHeight ) then
		self.parent:SetHeight(height);
	else
		self.parent:SetHeight(minHeight);
	end
	
	self.time = ( self.time or 0 ) - elapsed;
	if ( self.time <= 0 ) then
		updateDimensions(width, height);
		self.time = 0.1;
	end
end

local function startDragging(self)
	local x, y = GetCursorPosition();
	local scale = UIParent:GetScale();
	
	self.left = self.parent:GetWidth() / 2;
	self.center = self.parent:GetCenter() - ( self.parent:GetRight() - x/scale );
	self.top = self.parent:GetTop() + ( y/scale - self.parent:GetBottom() );
	self.scale = scale;
	self:SetScript("OnUpdate", dragUpdate);
	self.background:SetVertexColor(1, 1, 1);
	
	GameTooltip:Hide();
end

local function stopDragging(self)
	local height = self.parent:GetHeight();
	local width = self.parent:GetWidth();

	module:setOption("frameWidth", width, true);
	module:setOption("frameHeight", height, true);
	updateDimensions(width, height);
	
	self.center = nil;
	self.scale = nil;
	self:SetScript("OnUpdate", nil);
	
	if ( MouseIsOver(self) ) then
		self:GetScript("OnEnter")(self);
	else
		self:GetScript("OnLeave")(self);
	end
end


local function buffFrameSkeleton()
	return "frame#r:0:75", {
		"font#b:t#i:title#CT BuffMod",
		"backdrop#tooltip",
		
		["button#s:16:16#i:resize#br"] = {
			"texture#s:12:12#br:-4:4#i:background#Interface\\AddOns\\CT_BuffMod\\Images\\resize",
			["onenter"] = function(self)
				self.background:SetVertexColor(1, 1, 1);
				if ( self.scale ) then return; end
				module:displayPredefinedTooltip(self, "RESIZE");
			end,
			["onleave"] = function(self)
				module:hideTooltip();
				if ( self.scale ) then return; end
				self.background:SetVertexColor(1, 0.82, 0);
			end,
			["onload"] = function(self)
				self:SetFrameLevel(self:GetFrameLevel() + 2);
				self.background:SetVertexColor(1, 0.82, 0);
			end,
			["onmousedown"] = startDragging,
			["onmouseup"] = stopDragging
		},
		["onenter"] = function(self)
			if ( self.resize.scale ) then return; end
			module:displayPredefinedTooltip(self, "DRAG");
		end,
		["onleave"] = module.hideTooltip,
		["onmousedown"] = function(self, button)
			if ( button == "LeftButton" ) then
				module:moveMovable("BUFFMOD");
			end
		end,
		["onmouseup"] = function(self, button)
			if ( button == "LeftButton" ) then
				module:stopMovable("BUFFMOD");
			elseif ( button == "RightButton" ) then
				module:resetMovable("BUFFMOD");
				self:ClearAllPoints();
				self:SetPoint("TOPLEFT", UIParent, "CENTER", -frameWidth/2, frameHeight/2);
			end
		end
	}
end

buffModFrame = module:getFrame(buffFrameSkeleton);
module.buffFrame = buffModFrame;

--------------------
-- Initialization & Options

module.setBuffSize = function(self, value)
	if ( value ) then
		local oldBuffSize = buffSize;
		buffSize = value;
		effectiveBuffSize = buffSize + buffSpacing;
		for key, value in ipairs(buffObjectList) do
			value:display();
			value:position();
		end
		updateDisplayStatus(oldBuffSize > buffSize);
	end
end

module.expandBuffs = function(self, enable)
	expandBuffs = enable;
	updateDisplayStatus(not enable);
end

module.showBuffTimers = function(self, enable)
	showBuffTimers = enable;
	buffObjectList:display();
end

module.setSortType = function(self, newSort)
	sortType = newSort or sortType;
	
	-- Re-sort our table
	if ( sortType == 1 ) then
		sort(buffObjectList, buffSort1);
	elseif ( sortType == 2 ) then
		sort(buffObjectList, buffSort2);
	elseif ( sortType == 3 ) then
		sort(buffObjectList, buffSort3);
	elseif ( sortType == 4 ) then
		sort(buffObjectList, buffSort4);
	end
	
	updatePositions(1);
end

module.setSubSortType = function(self, newSort)
	subSortType = newSort or subSortType;
	
	if ( subSortType == 1 ) then
		buffSubSort = buffSort2;
	elseif ( subSortType == 2 ) then
		buffSubSort = buffSort3;
	elseif ( subSortType == 3 ) then
		buffSubSort = buffSort4;
	end
	
	if ( sortType == 1 ) then
		sort(buffObjectList, buffSort1);
		updatePositions(1);
	end
end

module.setExpiration = function(self, id, value)
	if ( id == 1 ) then
		expirationWarningTime1 = value;
	elseif ( id == 2 ) then
		expirationWarningTime2 = value;
	elseif ( id == 3 ) then
		expirationWarningTime3 = value;
	end
	buffObjectList:checkExpiration();
end

module.setTimeFormat = function(self, format)
	if ( format == 1 ) then
		getTimeFormat = timeFormat1;
	elseif ( format == 2 ) then
		getTimeFormat = timeFormat2;
	elseif ( format == 3 ) then
		getTimeFormat = timeFormat3;
	elseif ( format == 4 ) then
		getTimeFormat = timeFormat4;
	elseif ( format == 5 ) then
		getTimeFormat = timeFormat5;
	end
	buffObjectList:updateTimeDisplay();
end

module.setSpacing = function(self, spacing)
	buffSpacing = spacing;
	effectiveBuffSize = buffSize + spacing;
	updateDisplayStatus();
end

module.mainupdate = function(self, type, value)
	if ( type == "init" ) then
		self:registerMovable("BUFFMOD", buffModFrame, true);
		frameWidth = self:getOption("frameWidth") or frameWidth;
		frameHeight = self:getOption("frameHeight") or frameHeight;
		
		TemporaryEnchantFrame:Hide();
		BuffFrame:Hide();
		buffModFrame:SetWidth(frameWidth);
		buffModFrame:SetHeight(frameHeight);
		updateDimensions(frameWidth, frameHeight);
	end
end
module:schedule(1, function() newObjectDummy = false; end);