-- AUTHOR: Paranoidi

-- $LastChangedDate: 2006-08-14 18:34:44 +0300 (Mon, 14 Aug 2006) $
-- $LastChangedBy: marko $
-- $Rev: 247 $

--[[

			local px,py = GetPlayerMapPosition("player");
			local tx,ty = GetPlayerMapPosition("raid"..idx);
			local xdist = tx-px;
			local ydist = ty-py;
			local dist = sqrt(math.pow(xdist,2)+math.pow(ydist,2));
			
			http://intern.darklegion.de/interface/profiler.jpg
			
			testaa profileria?
			
			BUG: jonku pelaajan targettaaminen ei nollaa jrjestyst (kuten tyhjn valinta) !
			
--]]


-- integration to myAddons
SmartAssistDetails = {
		name = "SmartAssist",
		description = "SmartAssist is an addon which improves default assisting system in groups.",
		version = "1.2.9",
		releaseDate = string.sub("$LastChangedDate: 2006-08-14 18:34:44 +0300 (Mon, 14 Aug 2006) $", 18, 28),
		author = "paranoidi",
		email = "paranoidi@gmx.net",
		category = MYADDONS_CATEGORY_COMBAT,
		frame = "SmartAssistFrame",
		optionsframe = "SAOptionsFrame"
};

-- options
SA_OPTIONS_WRAPPER = {};
SA_OPTIONS = nil;

SA_MARKEDCACHE = {};
SA_PASSIVECACHE = {};
ASSIST_EMOTES = {};
PREVIOUS_ASSISTS = {};
PREVIOUS_NEAREST = false;
PREVIOUS_FALLBACK = false;
PREVIOUS_ASSIST_TIME = 0;
PREVIOUS_NEAREST_TIME = 0;
TARGET_CHANGED = false;

SMARTASSIST_CORE_REV = tonumber(string.sub("$Rev: 247 $", 7, -3));
BINDING_HEADER_SMARTASSIST = "SmartAssist";
BINDING_NAME_SASET = "Set puller";
BINDING_NAME_SAASSIST = "Assist";

SA_RUNNING = false; 		-- used to disable sounds and detect target changes
PASSIVE_WARN_FLAG = false;	-- is passive warning visible

COLOR_DEFAULT = {r=0, g=0.8, b=1};
COLOR_ALERT = {r=0.9, g=0, b=0};

StaticPopupDialogs["SA_NO_BINDINGS"] = {
	text = TEXT("You must bind SmartAssist key before being able to use this addon!"),
	button1 = TEXT(ACCEPT),
	OnAccept = function()
		printInfo("Tip: Press ESC key and go to 'bind keys' options. SmartAssist keys should be there at somewhere near bottom of the list.");
	end,
	timeout = 0,
	whileDead = 1,
};

StaticPopupDialogs["SA_UPGRADE_RESETPOS"] = {
	text = TEXT("SmartAssist will need to reset frame positons due changes in the way scaling is used."),
	button1 = TEXT(ACCEPT),
	OnAccept = function()
		SA_ResetFramePos();		
	end,
	timeout = 0,
	whileDead = 1,
};

StaticPopupDialogs["SA_AUTOCONF"] = {
	text = TEXT("Auto configure Smart Actions?"),
	button1 = TEXT(ACCEPT),
	button2 = TEXT(CANCEL),
	OnAccept = function()
		SA_AutoConfigureSmartActions();
		printInfo("You should see SmartAssist options and verify detected settings. Use command /sa to access configuration.");
	end,
	timeout = 0,
	whileDead = 1,
};

StaticPopupDialogs["SA_DISABLE_ATTACK"] = {
	text = TEXT("You have 'attack on assist' enabled in interface options which will cause problems with SmartAssist.\n\nAllow SmartAssist to turn it off (recommended) ?"),
	button1 = TEXT(YES),
	button2 = TEXT(NO),
	OnAccept = function()
		SetCVar("assistAttack", "0");
	end,
	OnCancel = function()
		SA_OPTIONS.AutoAttackWhineIgnored = true;
	end,
	timeout = 0,
	whileDead = 1,
};

function SA_OnLoad()
	SLASH_SMARTASSIST1 = "/smartassist";
	SLASH_SMARTASSIST2 = "/sa";
	SlashCmdList["SMARTASSIST"] = function(msg)
		SA_Command(msg);
	end
end

function SA_InitOption(option, value)
	if (SA_OPTIONS[option]==nil) then SA_OPTIONS[option]=value; end;
end

------------------------------------------------------------------------------
-- event: variables loaded
------------------------------------------------------------------------------

function SA_VariablesLoaded()
	-- Register the addon in myAddOns
	if(myAddOnsFrame_Register) then
		myAddOnsFrame_Register(SmartAssistDetails);
	end
	printInfo("SmartAssist "..SmartAssistDetails.version.." loaded.")

	-- add item to every possible context menu	
	table.insert(UnitPopupMenus["PARTY"], "PULLER");
	table.insert(UnitPopupMenus["PLAYER"], "PULLER");
	table.insert(UnitPopupMenus["RAID"], "PULLER");
	
	UnitPopupButtons["PULLER"] = { text = TEXT("Set as puller"), dist = 0 };
		
	SA_Init();
end


function SA_Init() 

	-- load realm & char specific configs
	local id = SA_GetAccountID();
	if (SA_OPTIONS_WRAPPER[id]==nil) then
		printInfo("SmartAssist created new default settings for "..id);
		SA_OPTIONS_WRAPPER[id] = {};
	end
	SA_OPTIONS = SA_OPTIONS_WRAPPER[id];
		
	-- set default values to option variables if they do not exist

	-- before rev 66 we had no stored rev number so we need to make sure it's in such cases atleast zero
	SA_InitOption("Rev", 0);

	-- special cases where we need to reset variables when upgrading the addon from older version
	if (SA_OPTIONS.Rev<240 and SA_OPTIONS.Rev>0) then
		StaticPopup_Show("SA_UPGRADE_RESETPOS");	
	end
	
	-- basic options
	SA_InitOption("VisualWarning", true);
	SA_InitOption("AssistOnEmote", true);
	SA_InitOption("PriorizeHealth", true);
	SA_InitOption("PriorizeHealthValue", 35);
	SA_InitOption("FallbackTargetNearest", true);
	SA_InitOption("CheckNearest", false);
	SA_InitOption("NearestMustBePvP", false);
	SA_InitOption("NearestMustBeTargetting", true);
	SA_InitOption("AssistOnlyNearest", false);
	-- available assist list
	SA_InitOption("ShowAvailable", true);
	SA_InitOption("PreserveOrder", false);
	SA_InitOption("OutOfCombat", false);
	SA_InitOption("TankMode", false); -- todo: class based defaults
	SA_InitOption("ClassIconMode", 1);
	SA_InitOption("TargetIconMode", 3);
	SA_InitOption("HuntersMarkIconMode", 1);
	SA_InitOption("AddMyTarget", false);
	SA_InitOption("ListWidth", 150);
	SA_InitOption("ListScale", 1.0);
	SA_InitOption("ListOOCAlpha", 0.4);
	SA_InitOption("ListSpacing", -5);
	SA_InitOption("ListHorizontal", false);
	SA_InitOption("ListTwoRow", false);
	SA_InitOption("LockList", false);
	SA_InitOption("HideTBY", false);
	SA_InitOption("HideTitle", false);
	SA_InitOption("AudioWarning", true); -- todo: class based defaults
	SA_InitOption("LostAudioWarning", false); -- todo: class based defaults
	SA_InitOption("VerboseAcquiredAggro", true);
	SA_InitOption("VerboseLostAggro", false);
	SA_InitOption("TextAlpha", 0.9);
	SA_InitOption("IntelligentSplit", false);
	-- advanced
	SA_InitOption("DisableSliderValue", 15);
	SA_InitOption("DisableTargetNearest", true);
	SA_InitOption("DisablePriorityHealth", true);
	SA_InitOption("AssistKeyMode", 1);
	SA_InitOption("DisableAutoCastKeyMode", 2);
	SA_InitOption("VerboseAssist", true);
	SA_InitOption("VerboseIncoming", false);
	SA_InitOption("VerboseNearest", true);
	SA_InitOption("VerboseUnableToAssist", false);
	SA_InitOption("DisableAssistWithoutPuller", false);
	SA_InitOption("PauseResetsOrder", false);
	-- smart actions
	SA_InitOption("AssistSpells", {});
	SA_InitOption("TriggerAssist", true);
	-- geek (not in options)
	SA_InitOption("SoundLoseAggro", "Sound\\Spells\\EyeOfKilroggDeath.wav");
	SA_InitOption("SoundIncomingWoldBoss", "Sound\\Spells\\PVPFlagTaken.wav");
	SA_InitOption("SoundGainAggro", "AuctionWindowClose");
	-- development
	SA_InitOption("DebugLevel", 0);
	
	-- display auto configuration dialog on first run
	SA_InitOption("AutoConfiguredV2", false);
	if (not SA_OPTIONS.AutoConfiguredV2) then
		SA_OPTIONS.AutoConfiguredV2 = true;
		StaticPopup_Show("SA_AUTOCONF");
	end
		
	-- init auto cast on assist defaults for hunters
	if (SA_OPTIONS["AutoAssist"]==nil and UnitClass("player") == CLASSNAME_HUNTER) then
		SA_OPTIONS.AutoAssist = true;
		SA_OPTIONS.FallbackOnBusy = true;
		SA_OPTIONS.AutoAssistTexture = "Interface\\Icons\\INV_Weapon_Rifle_08";
		SA_OPTIONS.AutoAssistName = SPELL_AUTOSHOT;
	end
	SA_InitOption("AutoAssist", false);
	SA_InitOption("AutoPetAttack", false);
	SA_InitOption("AutoPetAttackBusy", false);

	-- check if should display 'attack on assist' disable dialog if not ignored
	SA_InitOption("AutoAttackWhineIgnored", false);
	SA_CheckForAssistAttack();
	
	-- prefered assitance order (just a guess, got better one?)
	SA_InitOption("ClassOrder",{	CLASSNAME_WARRIOR, CLASSNAME_HUNTER, 
									CLASSNAME_ROGUE, CLASSNAME_PALADIN, 
									CLASSNAME_DRUID, CLASSNAME_SHAMAN, CLASSNAME_MAGE, 
									CLASSNAME_WARLOCK, CLASSNAME_PRIEST } );
									
	-- store revision number to configuration, this can be used to detect some special cases where we need to reset
	-- exsisting options (ie. bug in assistance order list) in new releases
	SA_OPTIONS.Rev = SMARTASSIST_CORE_REV;
	
	ASSIST_EMOTES = { EMOTE_EVERYONE_ATTACK, EMOTE_ATTACK, EMOTE_HELP };
	
	if (SA_OPTIONS.ShowAvailable) then
		SAListFrame:Show();
	else
		SAListFrame:Hide();
	end
end

-- check if assist attack is enabled
function SA_CheckForAssistAttack()
	if (not SA_OPTIONS.AutoAttackWhineIgnored) then
		if (GetCVar("assistAttack")~="0" and SA_OPTIONS.AutoAssist and SA_OPTIONS.AutoAssistName) then
			StaticPopup_Show("SA_DISABLE_ATTACK");
		end
	end
end

-------------------------------------------------------------------------------
-- WARNING ICON EVENTS
-------------------------------------------------------------------------------

SA_WARN_SPEED = 0.07;
SA_WARN_REFRESH = SA_WARN_SPEED;
SA_WARN_ALPHA = 0.4;
SA_WARN_ALPHA_UP = true;

function SA_WarnOnUpdate(elapsed)
	if (not PASSIVE_WARN_FLAG) then return; end;
	SA_WARN_REFRESH = SA_WARN_REFRESH - elapsed;
	if (SA_WARN_REFRESH > 0) then
		return;
	end
	SA_WARN_REFRESH = SA_WARN_SPEED;
	
	-- if target target is != nil (attacking someone) then REMOVE the icon
	if (UnitName("targettarget")~=nil) then
		SA_Debug("target target != nil .. remove the warning", 1);
		SA_ResetWarning();
		SA_AggroedTargetMsg();
		return;
	end

	if (SA_WARN_ALPHA_UP) then
		SA_WARN_ALPHA = SA_WARN_ALPHA + 0.1;
	else
		SA_WARN_ALPHA = SA_WARN_ALPHA - 0.1;
	end
	if (SA_WARN_ALPHA<0.4) then
		SA_WARN_ALPHA_UP = true;
		SA_WARN_ALPHA = 0.5;
	end
	if (SA_WARN_ALPHA>1.0) then
		SA_WARN_ALPHA_UP = false;
		SA_WARN_ALPHA = 0.9;
	end
	SAWarningFrame:SetAlpha(SA_WARN_ALPHA);
end

-------------------------------------------------------------------------------
-- handles smart assist slash commands
-------------------------------------------------------------------------------

function SA_Command(msg)
	local _,_,value = string.find(msg, "debug (%d)");
	if (value) then
		SA_OPTIONS.DebugLevel = tonumber(value);
		printInfo("Setting debug level to "..tostring(SA_OPTIONS.DebugLevel));
	elseif (msg=="available") then
		SA_ToggleAvailable();
	elseif (msg=="assist") then
		DoSmartAssist();
	elseif (msg=="reset" or msg=="reset all") then
		-- resets frame positions
		SA_ResetFramePos();
				
		-- clears this character settings and initializes defaults
		if (msg=="reset all") then
			printInfo("Reseting SmartAssist settings for all characters");
			SA_OPTIONS_WRAPPER = {};
		else
			printInfo("Reseting SmartAssist settings for this character");
			local id = SA_GetAccountID();
			SA_OPTIONS_WRAPPER[id] = {};
		end
		SA_Init();
	elseif (msg=="rev" or msg=="ver") then
		if (SMARTASSIST_REV) then
			printInfo("SmartAssist revision "..SMARTASSIST_REV);
		else
			printInfo("Unofficial or broken build! Revision number missing! Core rev "..SA_OPTIONS.Rev);
		end
	elseif (msg=="dump") then
		if (not DevTools_Dump) then
			printInfo("Requires devtools");
		else
			printInfo("Dumping internal variables:");
			printInfo("PREVIOUS_ASSISTS: ");
			DevTools_Dump(PREVIOUS_ASSISTS);
			printInfo("PREVIOUS_NEAREST: "..tostring(PREVIOUS_NEAREST));
			printInfo("TARGET_CHANGED: "..tostring(TARGET_CHANGED));
			printInfo("SA_RUNNING: "..tostring(SA_RUNNING));
		end
	else
		printInfo("Available parameters:");
		printInfo("ver - Display addon version information.");
		printInfo("assist - Execute SmartAssist for macros. Or call DoSmartAssist()");
		printInfo("reset - Reset character settings and frame positions.");
		printInfo("reset all - Reset all characters settings and frame positions.");
		SA_ShowOptions();
	end
end

function SA_ResetFramePos()
	SAListFrame:ClearAllPoints();
	SAListFrame:SetPoint("TOP", "UIParent", "TOP", 0, -20);
	SAListFrame:StopMovingOrSizing();
	SAWarningFrame:ClearAllPoints();
	SAWarningFrame:SetPoint("TOP", "UIParent", "TOP", 5, -25);
	SAWarningFrame:StopMovingOrSizing();
	SAOptionsFrame:ClearAllPoints();
	SAOptionsFrame:SetPoint("CENTER", "UIParent", "CENTER", 0, 0);
	SAOptionsFrame:StopMovingOrSizing();
end

function SA_ShowOptions()
	-- open config, or warn about key bindings
	local key1, key2 = GetBindingKey("SAASSIST");
	if (key1==nil and key2==nil) then
		StaticPopup_Show("SA_NO_BINDINGS");
	else
		SAOptionsFrame:Show();
	end
end

-------------------------------------------------------------------------------
-- SMART ACTIONS
-------------------------------------------------------------------------------

local old_UseAction = UseAction;
function UseAction(m1, m2, m3)

	local name = SA_GetSlotName(m1);
	local assistAction = false;
	if (SA_TableIndex(SA_OPTIONS.AssistSpells, name) ~= -1) then
		assistAction = true;
	end

	-- no target selected and attack action
	-- treat targetting player as no target
	if (SA_OPTIONS.TriggerAssist and (UnitName("target")==nil or UnitIsUnit("target","player")) and assistAction) then
		SA_Debug("no target, initiating assist by action!", 1);
		FindTarget(false, false);
		-- target is not player selected so don't attack unless target already in combat
		if (not UnitAffectingCombat("target")) then
			SA_Debug("not in combat, aborting action", 1);
			SA_ShowWarning();
			return; 
		end
	end
	
	-- if trying to attack friendly unit, initiate smart action
	
	-- UnitIsFriend bugs when dueling people in your party
	-- Added UnitPlayerControlled because on pve server as non flagged attacking the opposite faction initiated assist!
	if (assistAction and not UnitCanAttack("target", "player") and not (UnitPlayerControlled("target") and UnitFactionGroup("player")~=UnitFactionGroup("target"))) then
		SA_Debug("friendly target, assistAction = "..tostring(assistAction), 1);
		if (UnitCanAttack("player", "targettarget")) then
			SA_Debug("friendly target, assisting", 1);
			AssistUnit("target");
		else
			-- supresses annoyance when attacking and ally has no target
			if (assistAction) then 
				SA_Debug("aborting action, supressing annoyance", 1);
				return; 
			end
		end
	end
	
	return old_UseAction(m1, m2, m3);
end

-------------------------------------------------------------------------------
-- implements shift click assist
-- should work with ANYWHERE where you can select unit
-------------------------------------------------------------------------------

local SA_PREV_ASSIST_TIME = 0;
function SA_PlayerTargetChanged()
	TARGET_CHANGED = true;

	-- target lost
	if (not UnitExists("target")) then
		SA_ResetWarning();
		if (not SA_RUNNING) then
			PREVIOUS_ASSISTS = {};
			PREVIOUS_NEAREST = false;
		end
		SA_List_Refresh();
		return; -- fixes issue when we have target already (fires two events, on lost and on gain)
	end	
	
	-- asisst by unit selection & modifier key
	if ( (IsShiftKeyDown() and SA_OPTIONS.AssistKeyMode==1) or 
	     (IsControlKeyDown() and SA_OPTIONS.AssistKeyMode==2) or
	     (IsAltKeyDown() and SA_OPTIONS.AssistKeyMode==3) ) 
	then
		-- this time check (hoax) removes looping, when selecting player that has friendly taget. Without this 
		-- it goes there and starts to assist him
		-- todo: this can be done better .. if we check what's the target!
		
		local now = time();
		local dif = now - SA_PREV_ASSIST_TIME;
		if (dif == 0) then return; end
		SA_PREV_ASSIST_TIME = now;

		if (UnitCanAssist("player", "target") and
			UnitCanAttack("player", "targettarget"))
		then
			SAMsgFrame:AddMessage("Assisting "..UnitName("target"), 0.0, 0.8, 0.0, 1, 2.5);
			AssistUnit("target");
			SA_PostAssist();
		else
			if (UnitExists("target") and not UnitIsDead("target")) then
				UIErrorsFrame:AddMessage("Unable to assist", 1.0, 0, 0, 1.0, 1.5);
			end
		end
	end
	
	SA_List_Refresh();
end

------------------------------------------------------------------------------
-- credits to popup assist author, refactored a bit tough :)
------------------------------------------------------------------------------

local old_UnitPopup_OnClick = UnitPopup_OnClick;
function UnitPopup_OnClick(index)
	local dropdownFrame = getglobal(UIDROPDOWNMENU_INIT_MENU);
	local unit = dropdownFrame.unit;
	if ( this.value == "PULLER" ) then
		SA_SetPuller(unit);
	end
	return old_UnitPopup_OnClick(index);
end

------------------------------------------------------------------------------
-- param: unit - ID to be set as puller
-- set unit as puller
------------------------------------------------------------------------------

function SA_SetPuller(unit)
	if (UnitIsFriend(unit, "player")) then
		local candidates,_ = SA_GetCandidates(true);
		-- check that current puller exists in party, if not clear
		if (not candidates[UnitName(unit)]) then
			SAMsgFrame:AddMessage("Puller must be in party / raid", 1.0, 1.0, 1.0, 1, 3);
		else
			SA_OPTIONS["puller"] = UnitName(unit);
			SAMsgFrame:AddMessage("The assigned puller is now "..SA_OPTIONS["puller"], 1.0, 1.0, 1.0, 1, 1.5);
		end
	else
		SA_OPTIONS["puller"] = nil;
		SAMsgFrame:AddMessage("Assigned puller cleared", 1.0, 1.0, 1.0, 1, 1.5);
	end
	-- just incase that option window is visible, update the text
	SA_Options_UpdatePullerText();
end

------------------------------------------------------------------------------
-- credits to mozz
-- this is used to disable some anying unit deselect sounds while assisting 
-- (we have to clear the selected unit in some cases)
------------------------------------------------------------------------------

local old_PlaySound = PlaySound;
function PlaySound(name)
    if (not SA_RUNNING) then
    	return old_PlaySound(name);
    end
	if (name ~= "INTERFACESOUND_LOSTTARGETUNIT" and name ~= "igCharacterNPCSelect" ) then
		return old_PlaySound(name);
	end
end

------------------------------------------------------------------------------
-- main method, this is called when smartassist does its thing
------------------------------------------------------------------------------

function DoSmartAssist()
    SA_RUNNING = true;
    -- if has friendly target, straight assist it
    local f = true;
    if (UnitExists("target")) then
	    if (not UnitCanAttack("target", "player") and not (UnitPlayerControlled("target") and UnitFactionGroup("player")~=UnitFactionGroup("target"))) then
	    	if (SA_OPTIONS.VerboseAssist) then
	    		SA_Verbose("Assisting selected player "..UnitName("target"));
	    	end
	    	AssistUnit("target");
	    	f = false;
	    end
	end
	if (f) then
		FindTarget(true, false);
	end
	SA_RUNNING = false;
	SA_PostAssist();
	SA_List_Refresh();
end

------------------------------------------------------------------------------
-- post assist logic (after target has been selected)
------------------------------------------------------------------------------

function SA_PostAssist()
	-- auto-assist, disable if certain modifier key is down
	if ( ((not IsShiftKeyDown() and SA_OPTIONS.DisableAutoCastKeyMode==1) or 
	      (not IsControlKeyDown() and SA_OPTIONS.DisableAutoCastKeyMode==2) or
	      (not IsAltKeyDown() and SA_OPTIONS.DisableAutoCastKeyMode==3)) and 
	      UnitExists("target") and 
		  UnitAffectingCombat("target") ) 
	then
		if (SA_OPTIONS.AutoAssist and SA_OPTIONS.AutoAssistName) 
		then
			SA_Debug("Target already in combat, assisting with "..SA_OPTIONS.AutoAssistName, 3);
			CastSpellByName(SA_OPTIONS.AutoAssistName); -- casts highest level
		end
		if ((SA_OPTIONS.AutoPetAttack and UnitExists("pet") and not UnitIsDead("pet") and not UnitExists("pettarget")) or
			(SA_OPTIONS.AutoPetAttack and UnitExists("pet") and not UnitIsDead("pet") and SA_OPTIONS.AutoPetAttackBusy))
		then
			SA_Debug("Attacking with pet", 3);
			PetAttack();
		end
	end
	-- visual alert, if needed
	SA_ShowWarning();
end

------------------------------------------------------------------------------
-- receives all emote events, implements assist by emote
------------------------------------------------------------------------------

function SA_EmoteAssist(arg1)
	if (not SA_OPTIONS.AssistOnEmote) then
		return;
	end
	-- todo idea: hitting assist key should go back to previous target (if possible)
	-- parse emotes
	for k,v in ASSIST_EMOTES do
		local sp, ep, name = string.find(arg1, "(%w+) "..v);
		if (name ~= nil) then
			SA_Debug("assist emote detected, name="..name, 1);
		end
		-- if name exists, we have a match
		if name and name ~= EMOTE_OWN then
			local candidates,_ = SA_GetCandidates(true);
			if (candidates[name]~=nil) then
				SAMsgFrame:AddMessage("Assisting "..name, 0.0, 0.8, 0.0, 1, 2.5);
				AssistUnit(candidates[name].unitId);
				return true;
			end
			SA_Debug("not assisting "..name..", not in party/raid", 1);
		end
	end
end

function SA_UnitHealth(arg1)
	-- flag is used to reduce overheading, this gets called a lot ..
	if (not PASSIVE_WARN_FLAG) then
		return;
	end
	if (arg1=="target") then
		SA_ResetWarning();
		-- if someone else attacked the target, show another text saying its ok to start blasting
		-- this is not idiot proof check, seems to work fine tough
		if (not UnitIsUnit("targettarget", "pet") and not UnitIsUnit("targettarget", "player")) then
			SA_Debug("Someone else has attacked our target, show notification", 4);
			SA_AggroedTargetMsg();
		else
			SA_Debug("player has most likelly attacked the target", 4);
			-- todo: perhaps send pull message to party?
		end
	end
	-- todo idea: we could watch other players health here and suggest assist
end

-----------------------------------------------------------------
-- show warning if target is not in combat and feature is enabled
-----------------------------------------------------------------
function SA_ShowWarning()
	if (not UnitAffectingCombat("target") and UnitExists("target") and 
		UnitName("targettarget")==nil and SA_OPTIONS.VisualWarning and
		not UnitPlayerControlled("target")) 
	then
		PASSIVE_WARN_FLAG = true;
		SAWarningFrame:Show();
		return true;
	end
end

function SA_AggroedTargetMsg()
	-- the old msg was annoying as hell, replaced with SCT message, only if available
	-- todo: add as option!
	if (SCT_Display) then
		SCT_Display(message, COLOR_DEFAULT);
	end
end

function SA_ResetWarning()
	PASSIVE_WARN_FLAG = false;
	SAWarningFrame:Hide();
end

-------------------------------------------------------------
-- get distance unit is in, 0 if out of range
-- this funnly looking nesting makes minimal calls to api.
-- ie. No need to check for closer ranges if unit is not even
-- in 28yard range
-------------------------------------------------------------

-- TODO: NOT USED ATM!

function SA_GetDistance(unit)
	if (CheckInteractDistance(unit, 4)) then
		if (CheckInteractDistance(unit, 3)) then
			if (CheckInteractDistance(unit, 2)) then
				if (CheckInteractDistance(unit, 1)) then
					return 1;
				end
				return 2;
			end
			return 3;
		end
		return 4;
	end
	return -1;
end

-----------------------------------------------------------------------------------------
-- construct and return one candidate
-- if unit has invalid flag it should not be used because it does not contain all fields
-- foexample pets that are very far away do not have all fields present
-----------------------------------------------------------------------------------------

function SA_GetCandidate(unit, i)
	local candidate = {};
	candidate["unitName"] = UnitName(unit..i);
	candidate["unitId"] = unit..i;
	candidate["target"] = unit..i.."target";
	candidate["health"] = ceil( UnitHealth( unit..i ) / UnitHealthMax( unit..i ) * 100 );
	candidate["class"] = UnitClass(unit..i);
	candidate["dead"] = UnitIsDead(unit..i);
	if (string.find(unit, "pet") ~= nil) then
		candidate["pet"] = true;
	end
	if (candidate.unitName == nil) then 
		--SA_DebugCandidate(candidate);
		candidate["invalid"] = true; 
	end
	if (candidate.class == nil) then
		--SA_DebugCandidate(candidate);
		candidate["invalid"] = true; 
	end
	if (candidate.health == nil) then
		--printInfo("Problem with smartassist; unit health is unknown!");
		candidate["invalid"] = true; 
		SA_DebugCandidate(candidate);
	end
	return candidate;
end

function SA_GetPlayerAsCandidate()
	local candidate = {};
	candidate["unitName"] = UnitName("player");
	candidate["unitId"] = "player";
	candidate["target"] = "target";
	candidate["health"] = ceil( UnitHealth( "player" ) / UnitHealthMax( "player" ) * 100 );
	candidate["class"] = UnitClass("player");
	candidate["dead"] = UnitIsDead("player");
	return candidate;
end

------------------------------------------------------------------
-- return iteration info: is this party or raid, how many members
------------------------------------------------------------------

function SA_GetIterInfo()
	local mode, members = nil;
	if (GetNumRaidMembers()>0) then
		mode = "raid";
		members = GetNumRaidMembers();
	elseif (GetNumPartyMembers()>0) then
		mode = "party";
		members = GetNumPartyMembers();
	end
	return mode, members;
end

------------------------------------------------------------------
-- converts candidate list to map version
-- this is used in some places where we need list AND map versions
------------------------------------------------------------------

function SA_ConvertCandidateListToMap(candidates)
	local map = {};
	for k,v in candidates do
		map[v.unitName] = v;
	end	
	return map;
end

-------------------------------------------------------------
-- params: if map is true then as a map where key is unitName
--         if map is false then as list
-- return list of possible assistable units
-------------------------------------------------------------

local UNIQUE_WARNED = false;
function SA_GetCandidates(map)
	local candidates = {};
	local mode, members = SA_GetIterInfo(); -- got stack overflow once here
	local pullerAdded = false;

	-- for soloing and party, add our pet
	if (UnitExists("pet")) then
		local op = SA_GetCandidate("pet", "");
		if (not op.invalid and not op.dead) then 
			-- fixes problem when there are multiple pets with same name
			if (SA_OPTIONS["puller"] == op.unitName) then
				pullerAdded = true;
			end
			
			if (map) then
				candidates[op.unitName] = op;
			else
				table.insert(candidates, op);
			end
		end
	end
	
	-- if no party/raid, abort here
	if (members==nil) then return candidates, 0; end;

	local myname = UnitName("player");

	for i = 1, members do
		local candidate = SA_GetCandidate(mode, i);
		-- do not add invalid, dead or myself
		if (not candidate.invalid and not candidate.dead and candidate["unitName"] ~= myname) then
			if (map) then
				candidates[candidate.unitName] = candidate;
			else
				table.insert(candidates, candidate);
			end
		
			-- add pet to list if has one
			if (UnitExists(mode.."pet"..i)) then
				local pcandidate = SA_GetCandidate(mode.."pet", i);
				if (not pcandidate.invalid and not pcandidate.dead) then
					-- hotfix for unique pet puller problem
					-- players are unique by server so we don't have to worry about them
					if (SA_OPTIONS["puller"] == pcandidate.unitName and pullerAdded) then
						if (not UNIQUE_WARNED) then
							printInfo("SMARTASSIST PROBLEM: Puller is not unique!");
							UNIQUE_WARNED = true;
						end
					else
						if (map) then
							candidates[pcandidate.unitName] = pcandidate;
						else
							table.insert(candidates, pcandidate);
						end
						
						-- if added puller unit, set flag that no other unit with same name can be added
						if SA_OPTIONS["puller"] == pcandidate.unitName then
							pullerAdded = true;
						end
					end				
				end
			end
		end
	end
	
	return candidates, members;
end

------------------------------------------------------------------------------
-- if we have enabled filtering targets, remove those candidates who have 
-- something else. It was much more convient to implement this way rather 
-- than to modify get candidates and gazillion other places
------------------------------------------------------------------------------

function SA_FilterCandidates(candidates, map)
	local filtered = {};
	for key,candidate in candidates do
		if (UnitExists(candidate.target) and string.find(string.lower(UnitName(candidate.target)), SA_OPTIONS.Filter)) then
			if (map) then
				filtered[key] = candidate;
			else
				table.insert(filtered, candidate);
			end
		end
	end
	return filtered;
end

function SA_FilterCandidatesByDistance(candidates, map)
	--debugprofilestart();
	
	local filtered = {};
	for key,candidate in candidates do
		if (CheckInteractDistance(candidate.unitId, 4)) then
			if (map) then
				filtered[key] = candidate;
			else
				table.insert(filtered, candidate);
			end
		end
	end
	
	--SA_Debug("filtering by distance took "..debugprofilestop().." ms", 1);
	return filtered;
end

------------------------------------------------------------------------------
-- sort method for candidates
------------------------------------------------------------------------------

local detected_bug = false;
function SA_SortCandidate(a, b, members)
	-- TODO: INVESTIGATE, but HOW?
	-- this is added to debug odd behaviour in molten core where crash occured (b was nil, crashed at priorize health)
	-- OKAY: found out the cause, if there are two units with same name (pet. ie. Cat) and your own is set as puller, 
	-- this should be fixed in getCandidates but is UNTESTED, remove this when tested
	-- Update 20.6.2006 - still bugs on VERY rare occasions!
	if (detected_bug==false and (a==nil or b==nil)) then
		printInfo("?? BUG IN SMARTASSIST:");
		local cand,_ = SA_GetCandidates(false);
		local old_state = SA_OPTIONS.Debug;
		SA_OPTIONS.Debug = true;
		SA_DebugCandidates(cand);
		SA_OPTIONS.Debug = old_state;
		detected_bug = true;
	end
	
	-- priority health always first
	-- TODO: DOES NOT TAKE ACCOUNT THE MEMBERS > n DISABLING !!! <----------------xxxxxxxxxxxxxxxxxxxxx---------------xxxxxxxxxxxxxxxxxxxx------------xxxxxxxxxxxxxx
	-- SHOULD DO NOW! TESTING!!! xxxxxxxxxxxxxxx
	local priorize = SA_OPTIONS.PriorizeHealth;
	if (members > SA_OPTIONS.DisableSliderValue and SA_OPTIONS.DisablePriorityHealth) then
		priorize = false;
	end
	if (priorize) then
		if (a.health < SA_OPTIONS.PriorizeHealthValue or b.health < SA_OPTIONS.PriorizeHealthValue)
		then
			return a.health < b.health;
		end
	end
	
	-- depriorize players having passive target
	if (SA_PASSIVECACHE[a.unitName] and not SA_PASSIVECACHE[b.unitName]) then return false; end;
	if (SA_PASSIVECACHE[b.unitName] and not SA_PASSIVECACHE[a.unitName]) then return true; end;
	
	-- our puller should be always top priority
	if (a.unitName == SA_OPTIONS["puller"]) then return true; end
	if (b.unitName == SA_OPTIONS["puller"]) then return false; end
	
	-- priorize players whose target is marked
	if (SA_MARKEDCACHE[a.unitName] and not SA_MARKEDCACHE[b.unitName]) then return true; end;
	if (SA_MARKEDCACHE[b.unitName] and not SA_MARKEDCACHE[a.unitName]) then return false; end;
	
	-- de-priorize pets
	if (a.pet and not b.pet) then return false; end;
	if (b.pet and not a.pet) then return true; end;
	
	-- CT RaidAssist support, if we have main tanks set in CT RaidAssist prefer them
	if (CT_RA_MainTanks ~= nil) then
		local a_tank, b_tank = false;
		for _,v in CT_RA_MainTanks do
			if (a.unitName == v) then a_tank=true; end
			if (b.unitName == v) then b_tank=true; end
		end
		-- if both are CTRA tanks, sort by priority (mt first!) -- TODO: perhaps put OT first ?
		if (a_tank and b_tank) then 
			return SA_TableIndex(CT_RA_MainTanks, a.unitName) < SA_TableIndex(CT_RA_MainTanks, b.unitName); 
		end
		
		if (a_tank) then return true; end
		if (b_tank) then return false; end
	end
	
	-- if we have same class, sort secondary by health
	-- TODO: add option to disable this and use alphabetic order instead (should keep the list more stable)
	-- perhaps use alphabetic order when in groups larger than > n
	-- 20.6.2006 - testing out disabling if larger than > n
	if (a.class == b.class) then
		if (priorize) then
			return a.health < b.health;
		else
			return a.unitName < b.unitName;
		end
	end
	
	-- and last, teh normal case where sorted by class
	return SA_TableIndex(SA_OPTIONS.ClassOrder, a.class) < SA_TableIndex(SA_OPTIONS.ClassOrder, b.class);
end

------------------------------------------------------------------------------
-- Makes sure that the puller is in the party and if not clear / set to pet.
-- This gets called everytime assist is used
------------------------------------------------------------------------------

function SA_RefreshPuller(candidatelist)
	-- check that current puller exists in party, if not clear
	local candidates = SA_ConvertCandidateListToMap(candidatelist);
	if (SA_OPTIONS["puller"]~=nil and candidates[SA_OPTIONS["puller"]]==nil) then
		SA_Debug("Puller does not exist in candidates, clearing", 3);
		SA_OPTIONS["puller"]=nil;
	end
	
	-- if there is no puller and we have pet, set it as one
	if (SA_OPTIONS["puller"]==nil and UnitExists("pet") and not UnitIsDead("pet")) then
		SA_Debug("no puller set, pet exists -> setting it as one", 3);
		SA_OPTIONS["puller"] = UnitName("pet");
	end
end

------------------------------------------------------------------------------------------
-- params:
-- allowNearest = allow fallback to target nearest
-- recursive = is this recursive call, if it is we cannot add outside targets to skip list
-- Todo: better way for recursive?
------------------------------------------------------------------------------------------

function FindTarget(allowNearest, recursive)
	-- reset PREVIOUS_FALLBACK, store real value in local variable. Easier this way than to handle all returns ...
	local previous_fallback = PREVIOUS_FALLBACK;
	PREVIOUS_FALLBACK = false;

	local candidates, members = SA_GetCandidates(false);
	
	-- reset order if target is kept over 3s
	if (time() - PREVIOUS_ASSIST_TIME > 3 and SA_OPTIONS.PauseResetsOrder) then
		SA_Debug("over 3s since last assist, reseting PREVIOUS_ASSISTS", 2);
		PREVIOUS_ASSISTS = {};
		PREVIOUS_ASSIST_TIME = time();
	end
	
	-- allow targeting nearest attacking again
	if (time() - PREVIOUS_NEAREST_TIME > 5) then
		SA_Debug("over 5s since last assist, reseting PREVIOUS_NEAREST", 2);
		PREVIOUS_NEAREST = false;
		PREVIOUS_NEAREST_TIME = time();
	end
	
	if (SA_OPTIONS.Filter) then
		SA_Debug("filtering with "..tostring(SA_OPTIONS.Filter), 2);
		candidates = SA_FilterCandidates(candidates, false);
	end
	
	-- filter out candidates out of range if assisting only members nearby
	if (SA_OPTIONS.AssistOnlyNearest) then
		candidates = SA_FilterCandidatesByDistance(candidates, false);
	end
	
	SA_RefreshPuller(candidates);
	
	-- if we should not assist anyone from raid without puller, clear the candidates list
	if (SA_OPTIONS.DisableAssistWithoutPuller and SA_OPTIONS["puller"]==nil) then
		SA_Debug("DisableAssistWithoutPuller and no puller -> clearing candidates list", 2);
		candidates = {};
	end
	
	-- try to target nearest before going to assist from raid, note that this is different from allowNearest
	if (SA_OPTIONS.CheckNearest and not PREVIOUS_NEAREST and not recursive) then
		--local pre_valid = isValidTarget("target");
		-- okei, eli jos target nearest ei vaiha targettia niin se tuo myhempi else palauttaa edellisen assistauksen targetin -> bug bug!
		TARGET_CHANGED = false;
		TargetNearestEnemy();
		SA_Debug("check nearest got = "..tostring(UnitName("target")),1);
		local valid = isValidTarget("target") and UnitAffectingCombat("target");
		if (SA_OPTIONS.NearestMustBePvP) then
			if (not UnitPlayerControlled("target")) then 
				valid = false;
			end
		end
		if (SA_OPTIONS.NearestMustBeTargetting) then
			--if (UnitIsUnit("targettarget", "player")) then
			if (UnitName("targettarget") ~= UnitName("player")) then
				valid = false;
			end
		end
		if (valid) then
			SA_Debug("*** found good target from check nearest target="..tostring(UnitName("target")),1);
			if (SA_OPTIONS.VerboseNearest) then
				SA_Verbose("Targeted nearest", ALERT_COLOR);
			end
			PREVIOUS_NEAREST = true;
			return;
		else
			if (TARGET_CHANGED) then
				SA_Debug("invalid check nearest, restored target = "..tostring(UnitName("target")),1);
				TargetLastTarget();
			end
		end
	end
	PREVIOUS_NEAREST = false;

	-- store table size to variable because iterating it multiple times is no good
	table.sort(candidates, function(a,b) return SA_SortCandidate(a,b,members) end);
	
	for _,candidate in candidates do
		-- this is ingenious loop which determines if current candidate target has been targetted on previous assists
		-- not a idiot proof check since previous party members might have changed target since then, but works amazingly well
		-- atleast preventing situation where multiple members have same target and you press assist key multiple times
		
		-- 18.1.2006 - changed to show already targetted msg to test if its futile
		-- 21.1.2006 - this is triggered _multiple_ times per assist on some occasions, seems to be when there is only one target..
		
		local previously_targetted = false;
		for assisted,_ in PREVIOUS_ASSISTS do
			if (UnitExists(assisted)) then
				if (UnitIsUnit(candidate.target, assisted.."target")) then
					--SA_Debug("** already targetted once "..tostring(UnitName(assisted)));
					previously_targetted = true;
					break;
				end
			end
		end
		
		-- if current target is same as our candidate, consider it "assisted" (in other words: skip it)
		-- added 24.1.2006 - this should make cycling trough enemies work more smoothly
		if (UnitExists("target") and UnitIsUnit(candidate.target, "target")) then
			SA_Debug(tostring(candidate.unitId).." has same target as we ("..tostring(UnitName("target")).."), consider this unit as assisted", 3);
			PREVIOUS_ASSISTS[candidate.unitId] = true;
		end
		
		-- test each candidate, skips previously assisted UNLESS it has health below critical value
		local priority_health = candidate.health < SA_OPTIONS.PriorizeHealthValue and SA_OPTIONS.PriorizeHealth;
		
		if (members > SA_OPTIONS.DisableSliderValue and SA_OPTIONS.DisablePriorityHealth) then
			priority_health = false;
		end
		
		if ( (PREVIOUS_ASSISTS[candidate.unitId]==nil or priority_health) and (not previously_targetted) ) 
		then
			-- test if candidate (partyN, pet, raidN etc) has valid target
			if (UnitCanAssist("player", candidate.unitId) and isValidTarget(candidate.target)) 
			then
				if (SA_OPTIONS.VerboseAssist) then
					if (priority_health) then
						SA_Verbose("Priority assisting "..candidate.unitName.." ("..candidate.health.."%)", COLOR_ALERT);
					else
						SA_Verbose("Assisting "..candidate.unitName);
					end
				end
				SA_Debug("Found a good target from "..candidate.unitName.." ("..candidate.unitId..")", 3);
				AssistUnit(candidate["unitId"]);
				PREVIOUS_ASSISTS[candidate.unitId] = true;
				return;
			end
		else
			SA_Debug("** Skipping "..candidate.unitName.." ("..candidate.unitId..")", 4);
		end
	end
	
	-- if we have skiplist, now is good time to clear it and try again with all members, recursive call is only made once
	if (SA_TableSize(PREVIOUS_ASSISTS)>0 and not recursive) then
		SA_Debug("** Unable to assist anyone but we have skiplist ("..SA_TableSize(PREVIOUS_ASSISTS)..") -> clearing it and trying again..", 3);
		PREVIOUS_ASSISTS = {};
		return FindTarget(allowNearest, true);
	end
	
	-- we might had good target already when assist was used, if there are no other targets available we must exit now
	-- falling back to target nearest in that case would be idiotic
	-- 29.7.2006 - do not abort on good target if previously acquired target using fallback to nearest, 
	-- this allows toggling targets using smartassist key like TAB key.
	if (isValidTarget("target") and not previous_fallback) then
		SA_Debug("had already good target", 3);
		return;
	end
	
	if (SA_OPTIONS.FallbackTargetNearest and allowNearest) then
		if (SA_OPTIONS.DisableTargetNearest and members > SA_OPTIONS.DisableSliderValue) then
			if (SA_OPTIONS.VerboseUnableToAssist) then
				printInfo("Unable to assist anyone. Targetting nearest is suspended in groups this large.");
			end
			return;
		end
		if (SA_OPTIONS.VerboseUnableToAssist) then
			printInfo("Unable to assist anyone. Trying to target nearest enemy.");
		end
		TargetNearestEnemy();
		if (not isValidTarget("target")) then
			ClearTarget();
		else
			PREVIOUS_FALLBACK = true;
			SA_Debug("fallback to target nearest found good target", 2);
		end
	end	
end
