SA_LIST_BOXES = {};

-- TODO: incombat interval ja outofcombat interval! performance++ =)

SA_REFRESHRATE = 1.5;

SA_BASERATE = 0.6;
SA_INCREASE = 0.1; -- how much to decrease refresh rate per mob
SA_LASTREFRESH = SA_BASERATE;

SA_SEMIALERT = "|cffffa303";
SA_ALERTCOL = "|cffff0000";
SA_TANKCOL = "|cff00ff00";

-- title modes

MODE_OOC = 1;
MODE_NORMAL = 2;
MODE_FILTERED = 3;
MODE_PAUSED = 4;
MODE_NEAREST = 5;
MODE_OVERLOADED = 10;

-- contains all data from last update

SA_PREV = {
	["aggro_count"] = 0,
	["mob_count"] = 0,
	["wb_count"] = 0,
	["title_mode"] = -1,
	["dirty"] = false
};

-- class icon texture coordinates

SA_TEXTCOORDS={};
SA_TEXTCOORDS["PRIEST"]	 = { left=0.50, right=0.75, top=0.25, bottom=0.50 };
SA_TEXTCOORDS["MAGE"]    = { left=0.25, right=0.50, top=0.00, bottom=0.25 };
SA_TEXTCOORDS["WARLOCK"] = { left=0.75, right=1.00, top=0.25, bottom=0.50 };
SA_TEXTCOORDS["DRUID"] 	 = { left=0.75, right=1.00, top=0.00, bottom=0.25 };
SA_TEXTCOORDS["HUNTER"]  = { left=0.00, right=0.25, top=0.25, bottom=0.50 };
SA_TEXTCOORDS["ROGUE"] 	 = { left=0.50, right=0.75, top=0.00, bottom=0.25 };
SA_TEXTCOORDS["WARRIOR"] = { left=0.00, right=0.25, top=0.00, bottom=0.25 };
SA_TEXTCOORDS["SHAMAN"]	 = { left=0.25, right=0.50, top=0.25, bottom=0.50 };
SA_TEXTCOORDS["PALADIN"] = { left=0.00, right=0.25, top=0.50, bottom=0.75 };

-- predefined icon locations

SA_ICONPOS={n=8};
SA_ICONPOS[1] = { name="Disabled", point="TOPLEFT", relativePoint="", x=-26, y=-28, width=25, height=25 };
SA_ICONPOS[2] = { name="Left bottom", point="TOPLEFT", relativePoint="", x=-26, y=-28, width=25, height=25 };
SA_ICONPOS[3] = { name="Left top", point="TOPLEFT", relativePoint="", x=-26, y=-3, width=25, height=25};
SA_ICONPOS[4] = { name="Inside box", point="BOTTOMRIGHT", relativePoint="", x=0, y=0, width=25, height=25};
SA_ICONPOS[5] = { name="Right bottom", point="TOPRIGHT", relativePoint="", x=26, y=-28, width=25, height=25 };
SA_ICONPOS[6] = { name="Right top", point="TOPRIGHT", relativePoint="", x=26, y=-3, width=25, height=25};
SA_ICONPOS[7] = { name="Left big", point="TOPLEFT", relativePoint="", x=-51, y=-3, width=50, height=50};
SA_ICONPOS[8] = { name="Right big", point="TOPRIGHT", relativePoint="", x=51, y=-3, width=50, height=50};


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

function SA_InitBar(bar, text)
	bar:SetMinMaxValues(0, 100);
	bar:SetValue(100);
	text:SetHeight(12);
	text:SetPoint("CENTER", bar:GetName(), "CENTER", 0, 0);
	SetTextStatusBarText(bar, text);
	ShowTextStatusBarText(bar);
end

function SA_List_OnLoad()
	-- initialize boxes
	for i=1, 10 do
		local box = {};
		box["frame"] = getglobal("Target"..i);
		box["mobText"] = getglobal("Target"..i.."MobText");
		box["targetText"] = getglobal("Target"..i.."TargetText");
		box["targetOf"] = getglobal("Target"..i.."TargetOf");
		box["mobBar"] = getglobal("Target"..i.."MobBar");
		box["targetBar"] = getglobal("Target"..i.."TargetBar");
		box["classIcon"] = getglobal("Target"..i.."ClassIcon");
		box["targetIcon"] = getglobal("Target"..i.."TargetIcon");
		box["huntersMarkIcon"] = getglobal("Target"..i.."HuntersMarkIcon");
		SA_LIST_BOXES[i] = box;
	end
end

function SA_List_OnShow()
	-- TODO: this should be done only if options altered from xml defaults
	SA_List_UpdateAppearance();
	
	if (SA_OPTIONS.ListScale ~= 1.0) then
		SAListFrameScaler:SetScale(SA_OPTIONS.ListScale);
	end
	
	-- set title button to correct mode
	SA_List_SetTitleButton(MODE_OOC);
end

------------------------------------------------------------------------------
-- initialize list dropdown menu
------------------------------------------------------------------------------

function SA_List_FrameDropDown_OnLoad()
	UIDropDownMenu_Initialize(this, SA_List_FrameDropDown_Initialize, "MENU");
end

------------------------------------------------------------------------------
-- note: this is called always when dropdown is shown!
------------------------------------------------------------------------------

function SA_List_FrameDropDown_Initialize()
	if (not SA_OPTIONS) then 
		-- options not available yet (onload event) -> abort
		return;
	end

	item = {};
	item.text = "Filter targets";
	if (SA_OPTIONS.Filter) then
		item.checked = 1;
	end
	item.func = SA_List_Menu_Filter;
	UIDropDownMenu_AddButton(item);

	item = {};
	item.text = "Assist with pet";
	if (SA_OPTIONS.AutoPetAttack) then
		item.checked = 1;
	end
	item.keepShownOnClick = 1;
	item.func = SA_List_Menu_AssistWithPet;
	UIDropDownMenu_AddButton(item);
	
	item = {};
	item.text = "Assist only players nearby (28 yards)";
	if (SA_OPTIONS.AssistOnlyNearest) then
		item.checked = 1;
	end
	item.func = SA_List_Menu_AssistOnlyNearest;
	UIDropDownMenu_AddButton(item);

	item = {};
	item.text = "Display out of combat targets";
	item.checked = SA_OPTIONS.OutOfCombat;
	item.func = SA_List_Menu_ShowOOC;
	UIDropDownMenu_AddButton(item);

	item = {};
	item.text = "Lock list position";
	item.func = SA_List_Menu_Lock;
	item.checked = SA_OPTIONS.LockList;
	UIDropDownMenu_AddButton(item);
	
	item = {};
	item.text = "SmartAssist options";
	item.notCheckable = 1;
	item.func = SA_ShowOptions;
	UIDropDownMenu_AddButton(item);
end

function SA_List_Menu_Lock()
	SA_ToggleOption("LockList");
end

function SA_List_Menu_AssistWithPet()
	SA_ToggleOption("AutoPetAttack");
end

function SA_List_Menu_AssistOnlyNearest()
	SA_ToggleOption("AssistOnlyNearest");
	if (SA_OPTIONS.AssistOnlyNearest) then
		printInfo("Assisting only players nearby");
	end
end

function SA_List_Menu_ShowOOC()
	SA_ToggleOption("OutOfCombat");
end

function SA_List_Menu_Filter()
	if (SA_OPTIONS.Filter == nil) then
		SAFilterFrame:Show();
	else
		SA_OPTIONS.Filter = nil;
		printInfo("Filtering disabled");
		SA_List_SetTitleButton(MODE_OOC);
	end;
end

function SA_List_FilterButtonOK_OnClick()
	local text = SAFilterEditBox:GetText();
	if (string.len(text) > 0) then
		SA_OPTIONS.Filter = string.lower(text);
		printInfo("SmartAssist will now ignore targets which name doesn't contain text '"..text.."' until you disable filtering. Note that filtering applies only assisting, it doesn't include fallback to nearest.");
		SA_List_SetTitleButton(MODE_FILTERED);
	else
		SA_OPTIONS.Filter = nil;
		printInfo("Filtering disabled");
		SA_List_SetTitleButton(MODE_OOC);
	end
	SAFilterFrame:Hide();
end

------------------------------------------------------------------------------
-- Title button clicked (show menu) or drag
-- kudos goes for DamageMeters
------------------------------------------------------------------------------

function SA_List_TitleButton_OnClick()
	local button = arg1;
	if ( button  == "LeftButton" and not SA_OPTIONS.LockList) then
		-- drag frame
		if ( this:GetButtonState() == "PUSHED" ) then
			SAListFrame:StopMovingOrSizing();
		else
			SAListFrame:StartMoving();
		end
	elseif ( button == "RightButton" ) then
		-- show menu
		ToggleDropDownMenu(1, nil, SAListFrameDropDown, "SAListFrameDropDown", -33, 25);
	end
end

------------------------------------------------------------------------------
-- change outlook for the list depending options
------------------------------------------------------------------------------

function SA_List_HasSideIcons()
	if (SA_OPTIONS.ClassIconMode==2 or SA_OPTIONS.ClassIconMode==3 or
		SA_OPTIONS.TargetIconMode==2 or SA_OPTIONS.TargetIconMode==3 or
		SA_OPTIONS.HuntersMarkIconMode==2 or SA_OPTIONS.HuntersMarkIconMode==3) 
	then
		return true;
	end
end

function SA_List_UpdateAppearance()
	local width = SA_OPTIONS.ListWidth;
	
	local x = 0;
	local y = 0;
	if (SA_OPTIONS.ListHorizontal) then
		-- horizontal
		local gap = 10;
		if (SA_List_HasSideIcons()) then
			gap = gap - 30;
		end
		if (SA_OPTIONS.ListSpacing > 0) then
			x = width - gap + SA_OPTIONS.ListSpacing;
		else
			x = - (width - gap - SA_OPTIONS.ListSpacing);
		end
	else
		-- vertical
		local height = ceil(Target1:GetHeight()) - 8;
		if (SA_OPTIONS.ListSpacing > 0) then
			y = height + SA_OPTIONS.ListSpacing;
		else
			y = - (height-SA_OPTIONS.ListSpacing);
		end
	end
	
	SAListFrame:SetWidth(width);
	SAListTitleButton:SetWidth(width);
	
	local anchor="TOPLEFT";
	for i,box in SA_LIST_BOXES do
		if (i==1) then
			-- first box is ancored to title
			if (y<=0) then
				box.frame:SetPoint(anchor, "SAListFrame", anchor, 0, -17);
			else
				box.frame:SetPoint(anchor, "SAListFrame", anchor, 0, 54);
			end
		elseif (i==6 and SA_OPTIONS.ListTwoRow) then
			-- two rows
			if (SA_OPTIONS.ListHorizontal) then
				local gap = SA_OPTIONS.ListSpacing;
				if (gap>0) then
					gap = -gap;
				end
				gap = gap + 10;
				box.frame:SetPoint(anchor, "Target1", "BOTTOMLEFT", 0, gap);
			else
				local gap = SA_OPTIONS.ListSpacing;
				if (gap<0) then
					gap = -gap;
				end
				gap = gap - 8;
				-- if icons visible next to box
				if (SA_List_HasSideIcons()) then
					gap = gap + 30;
				end
				box.frame:ClearAllPoints();
				box.frame:SetPoint(anchor, "Target1", "TOPRIGHT", gap, 0);
			end
		else
			-- rest are anchored to previous box
			box.frame:ClearAllPoints();
			box.frame:SetPoint(anchor, "Target"..i-1, anchor, x, y);
		end
		
		box.frame:SetWidth(width);
		-- scale inside elements width
		box.mobBar:SetWidth(width-20);
		box.targetBar:SetWidth(width-20);
		box.mobText:SetWidth(width-30);
		box.targetText:SetWidth(width-30);

		-- update display targeted by stuff
		box.targetOf:SetWidth(width-20);
		
		-- set texts alpha
		box.mobText:SetAlpha(SA_OPTIONS.TextAlpha);
		box.targetText:SetAlpha(SA_OPTIONS.TextAlpha);
		
		-- set icon positions
		if (SA_OPTIONS.ClassIconMode>1) then
			local icon = SA_ICONPOS[SA_OPTIONS.ClassIconMode];
			SA_List_UpdateIconAppearance(box.classIcon, i, icon);
		else
			box.classIcon:Hide();
		end
		
		if (SA_OPTIONS.TargetIconMode>1) then
			local icon = SA_ICONPOS[SA_OPTIONS.TargetIconMode];
			SA_List_UpdateIconAppearance(box.targetIcon, i, icon);
		else
			box.targetIcon:Hide();
		end
		
		if (SA_OPTIONS.HuntersMarkIconMode>1) then
			local icon = SA_ICONPOS[SA_OPTIONS.HuntersMarkIconMode];
			SA_List_UpdateIconAppearance(box.huntersMarkIcon, i, icon);
		else
			box.huntersMarkIcon:Hide();
		end
		
	end
end

-- helper function, updates icon appearance
function SA_List_UpdateIconAppearance(frame, i, icon)
	frame:ClearAllPoints();
	frame:SetPoint(icon.point, "Target"..i, icon.point, icon.x, icon.y);
	frame:SetWidth(icon.width);
	frame:SetHeight(icon.height);
	frame:Show();
end

------------------------------------------------------------------------------
-- Refresh the list if enough time has passed since last refresh
------------------------------------------------------------------------------

function SA_List_OnUpdate(elapsed)
	SA_LASTREFRESH = SA_LASTREFRESH - elapsed;
	if (SA_LASTREFRESH > 0) then
		return;
	end
	SA_LASTREFRESH = SA_BASERATE + (SA_INCREASE * SA_PREV.mob_count);
	SA_List_Update();
end

local TARGET_CLICKED = time()
function SA_List_Target_OnClick(arg)
	SA_Debug("TargetClick");
	TARGET_CLICKED = time();
	local target = arg.obj;
	if (not target) then printInfo("SmartAssist: Unknown target?"); return; end;
    -- if ctrl+alt is down paste all targetters names (except tanks) to chat
    if (IsControlKeyDown() and IsAltKeyDown()) then
    	local names = "";
    	local i = 0;
    	for _,c in target.players do
    		if (not SA_IsTank(c.unitName)) then
    			names = names..c.unitName..", ";
    			i = i + 1;
    		end;
    	end
    	if (i>0) then 
    		names = string.sub(names, 0, -3);
	    	ChatFrameEditBox:SetText(names.." ");
			ChatFrameEditBox:Show();
    	end
    	return;
    end
	
	-- todo: problem is that target.players[1].unitName may have changed target between updating the list --> click
	-- however we could impove situation by assisting the player that has the MOST common target amon all targetters
	-- ie. iterate all targetters and check if UnitIsUnit(unit, others) the player who has most highest count is 
	-- most likelly the correct one! That would however do 40*39 (1590) checks at worst!
	-- additionally / alternatively we could check target name (if only one player for example) and disaply 
	-- error (+sound?) if it has been changed
	
	-- initial implementation (just verboses when debug on)
	--local counts = {};
	local hicount = 0;
	local unitId = "";
	for _,player in target.players do
		--counts[player.unitId] = SA_List_TargetShareCount(player, target.players);
		local count = SA_List_TargetShareCount(player, target.players);
		if (count > hicount) then
			hicount = count;
			unitId = player.unitId;
		end
	end
	--for unitId,count in counts do
		--SA_Debug(unitId.." shares target with "..tostring(count), 1);
	--end
	SA_Debug("Assisting "..unitId.." which has highest common count of "..hicount);
	AssistUnit(unitId);
	
	-- initiate assist
	--SA_Debug("assisting "..target.players[1].unitName);
	--AssistUnit(target.players[1].unitId);
	
	SA_PostAssist();
	-- update the list immediattely
	SA_PREV.title_mode = -1; -- resets title mode on next refresh
	SA_List_Refresh();
end

-- return number of players in list that have same target as player
function SA_List_TargetShareCount(player, list)
	local count = 0;
	for _,compare in list do
		if (UnitIsUnit(player.unitId.."target", compare.unitId.."target")) then
			count = count + 1;
		end
	end
	return count;
end

function SA_List_Target_OnEnter(arg)
	local text = SA_List_Target_GetTooltip(arg);
	GameTooltip:SetOwner(arg, "ANCHOR_LEFT");
	GameTooltip:SetText(text,1,1,1,1,1);
	
	-- prevent going to pausemode if target has just been clicked
	if (time() - TARGET_CLICKED > 3) then
		-- Add some "sleep" to refreshing the list. It's not good to update the list while user is trying to select something.
		SA_LASTREFRESH = SA_BASERATE * 3;
		SA_PREV.title_mode = -1; -- resets title mode on next refresh
		SA_List_SetTitleButton(MODE_PAUSED);
	end
end

function SA_List_Target_OnLeave(arg)
	GameTooltip:Hide();
end

function SA_List_Target_GetTooltip(arg)
	local target = arg.obj;
	if (not target) then return "Unknown?"; end;
	local text = target.fullName.."\nTargetted by:\n";
	for k,v in target.players do
		-- colorize text by class
		local cv = RAID_CLASS_COLORS[string.upper(v.class)];
		local color="";
		if (not cv) then
			color = "|cff888888";
		else
			color = SA_ToTextCol(cv.r, cv.g, cv.b);
		end
		text = text .. color..v.unitName .. "|r" .. "\n";
	end
	return text;
end

------------------------------------------------------------------------------
-- Request list to be refreshed immediattely
------------------------------------------------------------------------------

function SA_List_Refresh()
	--SA_Debug("Requesting refresh now, target="..tostring(UnitName("target")));
	SA_LASTREFRESH = 0;
end

------------------------------------------------------------------------------
-- get puller from candidate list
------------------------------------------------------------------------------

function SA_GetPuller(candidates)
	for _,v in candidates do
		if (v.unitName == SA_OPTIONS.puller) then
			return v;
		end
	end
end

------------------------------------------------------------------------------
-- check if candidates target is already targetted by someone
-- return the target if found and nil if not
------------------------------------------------------------------------------

function SA_GetExistingTarget(candidate, targets)
	for _,target in targets do
		if (UnitIsUnit(target.players[1].target, candidate.target)) then
			return target;
		end
	end
	return nil;
end

function SA_List_NameSplit(str)
  local t = {n=0}
  local function helper(word) table.insert(t, word) end
  if not string.find(string.gsub(str, "%w+", helper), "%S") then return t end
end

------------------------------------------------------------------------------
-- construct target from candidate
------------------------------------------------------------------------------

function SA_GetTarget(candidate, pullerId)
	target = {};
	target["players"] = { candidate };
	target["name"] = UnitName(candidate.target);
	target["fullName"] = target.name;
	-- intelligent name split
	if (SA_OPTIONS.IntelligentSplit and target.name) then
		local parts = SA_List_NameSplit(target.name);
		if (parts) then
			if (table.getn(parts)>2) then
				if (parts[2]~="of") then -- splitting "shade of naxxramas" -> "of naxxramas" is stupid, dont split if second word is "of"
					target.name = string.sub(target.name, string.len(parts[1])+1 );
				end
			end
		end
	end
	target["health"] = UnitHealth(candidate.target);
	target["targetName"] = UnitName(candidate.target.."target");
	-- test fix for "shang's problem"
	-- this seems to happend when unit is near edge of known area and it's target is outide of it. Hence my client doesn't know anything about this unit ..
	if (target.targetName=="Unknown Entity" or target.targetName=="Unknown") then
		target.targetName=nil;
	end
	-- target.self gives us a reference to mob, example: party<n>target
	target["self"] = candidate.target;
	target["targetHealth"] = ceil( UnitHealth( candidate.target.."target" ) / UnitHealthMax( candidate.target.."target" ) * 100 );
	local _,targetClass = UnitClass(target.self);
	target["targetClass"] = targetClass;
	
	target["playersCount"] = 1;
	if (SA_IsMarked(candidate.target)) then
		target["marked"] = true;
	else
		target["marked"] = false;
	end

	if (isUnitCC(candidate.target)) then
		target["cced"] = true;
	else
		target["cced"] = false;
	end
	
	-- todo: xxx, only if enabled
	target["icon"] = GetRaidTargetIndex(target.self);
	if (not target.icon) then
		target["icon"] = 0;
	end
	
	-- is world boss, elite etc
	target["classification"] = UnitClassification(candidate.target);
	
	-- set myTarget to true if this is my target
	target["myTarget"] = UnitIsUnit(target.self, "target");
	
	if (pullerId~="") then
		target["pullerTarget"] = UnitIsUnit(target.self, pullerId.."target");
	end
	-- is unit in combat, 20.6.2006 - if it has target, treat as in combat!
	target["inCombat"] = UnitAffectingCombat(target.self) or target.targetName;
	
	return target;
end

-- return true if this candidate target should be added to list
function SA_Add_To_List(candidate)
	if( UnitCanAssist("player", candidate.unitId) and 
	    UnitExists(candidate.target) and
	    ((UnitAffectingCombat(candidate.target) or SA_OPTIONS.OutOfCombat) and not UnitIsDead(candidate.target)) and 
	    UnitCanAttack("player", candidate.target)) 
	then
		return true;
	else
		return false;
	end
end

function SA_List_SortTarget(a,b)
	-- marked first
	if (a.marked and not b.marked) then return true; end;
	if (b.marked and not a.marked) then return false; end;

	-- puller target second
	--if (a.pullerTarget and not b.pullerTarget) then return true; end;
	--if (b.pullerTarget and not a.pullerTarget) then return false; end;

	-- ooc last
	if (a.inCombat and not b.inCombat) then return true; end;
	if (b.inCombat and not a.inCombat) then return false; end;
			
	-- cced second lastest
	if (a.cced and not b.cced) then return false; end;
	if (b.cced and not a.cced) then return true; end;
		
	-- if both unmarked, then sort by name
	if (a.name == b.name) then
		return a.playersCount > b.playersCount;
	else
		return a.name > b.name;
	end
end

------------------------------------------------------------------------------
-- updates the available assists list
------------------------------------------------------------------------------

-- this table contains all variables used in incoming detection & printing
local incoming = { prev_iter=0, currently=0, prev_time=0 };

function SA_List_Update()

	-- if disabled, stop immediattely
	if (not SA_OPTIONS.ShowAvailable) then return; end
	
	local candidates, members= SA_GetCandidates(false)
	table.sort(candidates, function(a,b) return SA_SortCandidate(a,b,members) end);
	
	-- filter out candidates out of range if assisting only members nearby
	-- TODO: might not be good since causes overheading AND we should still add those players outside range to target?
	if (SA_OPTIONS.AssistOnlyNearest) then
		candidates = SA_FilterCandidatesByDistance(candidates, false);
	end
	
	-- add player to candidates if enabled
	if (UnitExists("target") and SA_OPTIONS.AddMyTarget) then
		table.insert(candidates, SA_GetPlayerAsCandidate());
	end

	local aggro_count = 0;
	local wb_count = 0;
	
	local pullerId = "";
	if (SA_OPTIONS.puller) then
		local puller = SA_GetPuller(candidates);
		-- puller might be null if it doesn't exist in group
		if (puller) then
			pullerId = puller.unitId;
		end
	end
	
	-- construct list of available assists / targets
	local targets = {};
	for _,candidate in candidates do
		if (SA_Add_To_List(candidate)) then
			local target = SA_GetExistingTarget(candidate, targets);
			if (target) then
				-- append targetting candidate to target
				table.insert(target.players, candidate);
				target.playersCount = target.playersCount + 1;
			else
				-- this candidate target is not targetted by anyone else, construct new target to the list
				local target = SA_GetTarget(candidate, pullerId);
				
				-- filtering if enabled
				if (SA_OPTIONS.Filter) then
					if string.find(string.lower(target.fullName), SA_OPTIONS.Filter) then
						table.insert(targets, target);
					end
				else
					table.insert(targets, target);
				end
			end
		end
	end
	
	-- update non-aggro cache and mark cache, this is used when SORTING candidates (see SA_SortCandidate)
	SA_MARKEDCACHE = {};
	SA_PASSIVECACHE = {};
	local dirty = false;
	local i = 0;
	local mc = 0;
	local ooc_seen = false;
	for _,target in targets do
		i = i + 1;
		if (target.marked) then
			if (i ~= 1 and mc == 0) then
				-- marked target not in first place, list is tainted
				SA_Debug("mark dirty list "..target.name, 1);
				dirty = true;
			end
			mc = mc + 1;
			for _,c in target.players do
				SA_MARKEDCACHE[c.unitName] = true;
			end
		end
		if (not target.inCombat or target.cced) then
			ooc_seen = true;
			for _,c in target.players do
				SA_PASSIVECACHE[c.unitName] = true;
			end
		else
			-- incombat targets after ooc targets, list is tainted
			if (ooc_seen) then
				SA_Debug("ooc dirty list "..target.name, 1);
				dirty = true;
			end
		end
	end
	
	-- if list is dirty abort refresh and request new one with correct cache,
	-- update 22.4.2006   	cache is not correct on next refresh either, in fact it may pass long time it is okay. 
	-- 						hence added PREV_DIRTY flag until problem is investigated
	-- update 20.6.2006		sort does not seem to work correctly with buff (ie. hunters mark) priorization!
	-- update 09.7.2006		sorting seems to work fine now, cleanup this + futile flag?
	if (dirty and not SA_PREV.dirty) then
		-- try again soon
		SA_LASTREFRESH = 0.2;
		SA_PREV.dirty = true;
		return;
	end
	SA_PREV.dirty = false;
	
	-- sort targets depending options
	if (SA_OPTIONS.PreserveOrder) then
		table.sort(targets, function(a,b) return SA_List_SortTarget(a,b) end);
	end
	
	------------------------------------------------------------------------------
	
	local i = 0;
	local overloaded = false;
	
	for _,target in targets do
		i = i + 1;
		if (i>10) then 
			overloaded = true;
			break; 
		end
		
		local box = SA_LIST_BOXES[i];
		box.frame.obj = target; -- store the target data to box (used ie. in tooltips)
		box.frame:Show();
			
		-- set values & texts
		local targetText = target.players[1].unitName;
		if (not SA_OPTIONS.HideTBY) then
			targetText = "|cffffffffT.by |r"..targetText;
		end
		if (target.playersCount > 1) then
			targetText = targetText.." + "..tostring(target.playersCount-1); -- -1 for the one who targets (who + n)
		end
		-- append prefixes to mob name + for elite, WB+ for worldboss
		local mobText = target.name;
		if (target.classification == "elite") then
			mobText = SA_SEMIALERT.."+|r"..mobText;
		end
		if (target.classification == "worldboss") then
			mobText = SA_SEMIALERT.."WB+|r"..mobText;
			if (target.inCombat) then
				wb_count = wb_count + 1;
			end
		end
		box.mobBar:SetValue(target.health);
		box.mobText:SetText(mobText);
		
		-- colorize box
		if (not target.inCombat) then
			-- if this is out of combat and my target, apply only slight green efect
			if (target.myTarget) then
				box.frame:SetBackdropColor(0,0.65,0,0.65);
				box.frame:SetBackdropBorderColor(0,1,0,0.75);
			elseif (target.pullerTarget) then
				-- yellow background on puller target
				box.frame:SetBackdropColor(1,1,0,0.3);
				box.frame:SetBackdropBorderColor(0,0,0,0.4); -- regular grey
			else
				box.frame:SetBackdropColor(0,0,0,0.4);
				box.frame:SetBackdropBorderColor(0,0,0,0.4); -- regular grey
			end
		else
			local addinc = true;
			-- set border color
			if (target.marked) then
				box.frame:SetBackdropBorderColor(1,0,0,1);
			elseif (target.myTarget) then
				box.frame:SetBackdropBorderColor(0,1,0,1);
			elseif (target.cced) then
				box.frame:SetBackdropBorderColor(0,0,0,1);
				addinc = false;
			else
				box.frame:SetBackdropBorderColor(0.8,0.8,0.8,1);
			end
			-- add to incoming
			if (addinc) then
				incoming.currently = incoming.currently + 1;
			end
			
			-- set background color
			if (target.myTarget) then
				box.frame:SetBackdropColor(0,0.75,0,0.85);
			elseif (target.pullerTarget) then
				-- yellow background on puller target
				box.frame:SetBackdropColor(1,1,0,0.3);
			else
				box.frame:SetBackdropColor(0,0,0,0.5);
			end
		end
		
		-- colorize target target text in some situations
		if (target.targetName) then
			local col = "";
			local endcol = "";
			if (UnitName("player") == target.targetName) then
				-- targetting player
				aggro_count = aggro_count + 1;
				if (SA_OPTIONS.TankMode) then
					col = SA_SEMIALERT;
				else
					col = SA_ALERTCOL;
				end
			elseif (not SA_IsTank(target.targetName) and SA_OPTIONS.TankMode) then
				-- targeting non tank in tank mode
				col = SA_ALERTCOL;
			elseif (SA_IsTank(target.targetName)) then
				-- targeting tank always on tank color, regardless of tank mode
				col = SA_TANKCOL;
			end
			-- append end of color code marking
			if (col~="") then
				endcol = "|r";
			end
			box.targetBar:SetValue(target.targetHealth);
			box.targetText:SetText("T:"..col..target.targetName..endcol);
		else
			box.targetBar:SetValue(0);
			box.targetText:SetText("?");
		end
		
		-- colorize mob bar
		if (UnitIsTapped(target.self) and not UnitIsTappedByPlayer(target.self)) then
			-- target is "grey" to us
			box.mobBar:SetStatusBarColor(0.8, 0.8, 0.8);
		elseif (UnitPlayerControlled(target.self)) then
			-- pvp target
			box.mobBar:SetStatusBarColor(1, 0, 0);
		elseif (not target.inCombat) then
			-- out of combat target
			box.mobBar:SetStatusBarColor(0.38, 0.38, 0.38);
		else
			-- normal green
			box.mobBar:SetStatusBarColor(0, 1, 0);
		end
		
		-- make texts visible always (bugs on some systems)
		-- code peeked from TextStatusBar_UpdateTextString sources
		-- perhaps I should've called that method but it does some other unwanted things
		box.mobBar.TextString:Show();
		box.targetBar.TextString:Show();
		box.targetOf:SetText(targetText);
		
		-- set class icon (texture coordinates)
		local coords = SA_TEXTCOORDS[target.targetClass];
		box.classIcon:SetTexCoord(coords.left, coords.right, coords.top, coords.bottom);
		
		-- set target icon (texture coordinates)
		-- TODO: xxx, only if feature enabled
		if (target.icon>0) then
			local icon = UnitPopupButtons["RAID_TARGET_"..target.icon];
			box.targetIcon:SetTexCoord(icon.tCoordLeft, icon.tCoordRight, icon.tCoordTop, icon.tCoordBottom );
			box.targetIcon:Show();
		else
			box.targetIcon:Hide();
		end

		-- hunters mark icon
		if (target.marked and SA_OPTIONS.HuntersMarkIconMode>1) then
			box.huntersMarkIcon:Show();
		else
			box.huntersMarkIcon:Hide();
		end
		
	end
	
	------------------------------------------------------------------------------
	
	-- if count has been changed update visibility
	-- TODO: apparently there is no performance loss calling hide/show .. so this is partially futile now
	if (i ~= SA_PREV.mob_count) then
		for j = i + 1, SA_PREV.mob_count do
			if (j>10) then
				break;
			end
			SA_LIST_BOXES[j].frame:Hide();
		end
	end
	
	-- if player have a ACQUIRED aggro, play optional sound to alert player
	if (aggro_count > SA_PREV.aggro_count) then
		if (SA_OPTIONS.AudioWarning) then PlaySound(SA_OPTIONS.SoundGainAggro); end;
		if (SA_OPTIONS.VerboseAcquiredAggro) then
			SA_Verbose("Acquired aggro!", COLOR_ALERT); 
		end
	end
	-- if player have a LOST aggro, play optional sound to alert player
	if (aggro_count < SA_PREV.aggro_count and i >= SA_PREV.mob_count) then
		if (SA_OPTIONS.LostAudioWarning) then PlaySoundFile(SA_OPTIONS.SoundLoseAggro); end;
		if (SA_OPTIONS.VerboseLostAggro) then
			SA_Verbose("Lost aggro!", COLOR_ALERT);
		end
	end
	-- if incoming worldbosses, play optional sound to alert player, TODO: OPTION!
	if (wb_count > SA_PREV.wb_count) then
		SA_Verbose("Incoming worldboss!", COLOR_ALERT);
		PlaySoundFile(SA_OPTIONS.SoundIncomingWoldBoss);
	end
	
	-- if we have new mobs in combat
	-- TODO: on incoming worldboss level unit play alert sound!
	if (SA_OPTIONS.VerboseIncoming) then
		local now = time();
		if (incoming.currently > incoming.prev_iter and now - incoming.prev_time > 4) then
			
			local amount = incoming.currently - incoming.prev_iter;
			if (amount > 1) then
				SA_Verbose("Incoming x"..amount.."!", COLOR_ALERT); 
			else
				SA_Verbose("Incoming!", COLOR_ALERT); 
			end
			
			incoming.prev_time = now;
		end
		incoming.prev_iter = incoming.currently;
		incoming.currently = 0;
	end
	
	-- update title to correct mode
	if (i>0) then
		title_mode = MODE_NORMAL;
	else
		title_mode = MODE_OOC;
	end
	if (SA_OPTIONS.Filter) then
		title_mode = MODE_FILTERED;
	end
	if (SA_OPTIONS.AssistOnlyNearest) then
		title_mode = MODE_NEAREST;
	end
	if (overloaded) then
		title_mode = MODE_OVERLOADED;
	end
	
	-- update title button if mode has been changed
	if (title_mode ~= SA_PREV.title_mode) then
		SA_List_SetTitleButton(title_mode);
	end
	
	-- set prev data for next iteration
	SA_PREV.title_mode = title_mode;
	SA_PREV.mob_count = i;
	SA_PREV.wb_count = wb_count;
	SA_PREV.aggro_count = aggro_count;
end

------------------------------------------------------------------------------
-- Title button handling
------------------------------------------------------------------------------

function SA_List_SetTitleButton(mode)
	if (mode == MODE_NORMAL or mode == MODE_OOC) then
		if (mode == MODE_NORMAL) then
			SAListTitleButton:SetAlpha(1);
		else
			if (SA_OPTIONS.HideTitle) then
				SAListTitleButton:SetAlpha(0);
			else
				SAListTitleButton:SetAlpha(SA_OPTIONS.ListOOCAlpha);
			end
		end
		SAListTitleButton:SetBackdropColor(0, 0, 0, 0.3);
		SAListTitleButton:SetBackdropBorderColor(1,1,1,1)
		SAListTitle:SetText("Available assists");
	elseif (mode == MODE_FILTERED or mode == MODE_NEAREST) then
        SAListTitleButton:SetAlpha(1);
        SAListTitleButton:SetBackdropColor(0,0.6,0.8,1);
        SAListTitleButton:SetBackdropBorderColor(0,0.6,0.8,1);
        local text = "";
        if (mode == MODE_FILTERED) then
        	text = "Filtered";
        end
        if (mode == MODE_NEAREST) then
        	text = "Nearest";
        end
        SAListTitle:SetText(text);
    elseif (mode == MODE_PAUSED) then
        SAListTitleButton:SetAlpha(1);
        SAListTitleButton:SetBackdropColor(0.3,0.3,0.3,1);
        SAListTitleButton:SetBackdropBorderColor(0.6,0.6,0.6,1);
        SAListTitle:SetText("Paused");
	elseif (mode == MODE_OVERLOADED) then
		SAListTitleButton:SetAlpha(1);
		SAListTitleButton:SetBackdropColor(1,0,0,1);
		SAListTitleButton:SetBackdropBorderColor(1,0,0,1);
		SAListTitle:SetText("OVERLOADED");
	else
		printInfo("smartassist error: unknown title mode "..tostring(mode));
	end
end

SA_PREV_ALPHA = 1;
function SA_List_TitleButton_OnEnter()
	SA_PREV_ALPHA = SAListTitleButton:GetAlpha();
	SAListTitleButton:SetAlpha(1);
end

function SA_List_TitleButton_OnLeave()
	SAListTitleButton:SetAlpha(SA_PREV_ALPHA);
end