-- 
-- Please see the readme.txt file included with this distribution for 
-- attribution and copyright information.
--

enableglobaltoggle = true;
ct_active_name = "";

function onInit()
	Interface.onHotkeyActivated = onHotkey;
	
	-- Make sure all the clients can see the combat tracker
	for k,v in ipairs(User.getAllActiveIdentities()) do
		local username = User.getIdentityOwner(v);
		if username then
			NodeManager.addWatcher("combattracker", username);
		end
	end
	
	-- Create a blank window if one doesn't exist already
	if not getNextWindow(nil) then
		NodeManager.createSafeWindow(self);
	end
	
	-- Register a menu item to create a CT entry
	registerMenuItem("Create Item", "insert", 5);

	-- Update the effects references now that all the tracker entry windows are loaded
	for k, v in ipairs(getWindows()) do
		for en, ew in ipairs(v.effects.getWindows()) do
			if ew.caster_ref_node.getValue() ~= "" then
				for k2, v2 in ipairs(getWindows()) do
					if v2.getDatabaseNode().getNodeName() == ew.caster_ref_node.getValue() then
						ew.caster.setSource(v2);
						break;
					end
				end
			end
		end
	end
end

function onMenuSelection(selection)
	if selection == 5 then
		NodeManager.createSafeWindow(self);
	end
end

function onSortCompare(w1, w2)
	if w1.initresult.getValue() == w2.initresult.getValue() then
		return w1.init.getValue() < w2.init.getValue();
	end
	
	return w1.initresult.getValue() < w2.initresult.getValue();
end

function onHotkey(draginfo)
	if draginfo.isType("combattrackernextactor") then
		nextActor();
		return true;
	end
	if draginfo.isType("combattrackernextround") then
		nextRound();
		return true;
	end
end

function onDrop(x, y, draginfo)
	-- Capture certain drag types meant for the host only
	local dragtype = draginfo.getType();

	-- PC
	if dragtype == "playercharacter" then
		addPc(draginfo.getDatabaseNode());
		return true;
	end

	if dragtype == "shortcut" then
		local class, datasource = draginfo.getShortcutData();

		-- NPC
		if class == "npc" then
			addNpc(draginfo.getDatabaseNode());
			return true;
		end

		-- ENCOUNTER
		if class == "battle" then
			addBattle(draginfo.getDatabaseNode());
			return true;
		end
	end

	-- Capture any drops meant for specific CT entries
	local win = getWindowAt(x,y);
	if win then
		return CombatCommon.onDrop("ct", win.getDatabaseNode().getNodeName(), draginfo);
	end
end

function toggleDefensive()
	if not enableglobaltoggle then
		return;
	end
	
	local defensiveon = window.button_global_defensive.getValue();
	for k,v in pairs(getWindows()) do
		if defensiveon ~= v.activatedefensive.getValue() then
			v.activatedefensive.setValue(defensiveon);
			v.setDefensiveVisible(v.activatedefensive.getValue());
		end
	end
end

function toggleActive()
	if not enableglobaltoggle then
		return;
	end
	
	local activeon = window.button_global_active.getValue();
	for k,v in pairs(getWindows()) do
		if activeon ~= v.activateactive.getValue() then
			v.activateactive.setValue(activeon);
			v.setActiveVisible(v.activateactive.getValue());
		end
	end
end

function toggleEffects()
	if not enableglobaltoggle then
		return;
	end
	
	local effectson = window.button_global_effects.getValue();
	for k,v in pairs(getWindows()) do
		if effectson ~= v.activateeffects.getValue() then
			v.activateeffects.setValue(effectson);
			v.setEffectsVisible(v.activateeffects.getValue());
			v.effects.checkForEmpty();
		end
	end
end

function onEntrySectionToggle()
	local anyDefensive = false;
	local anyActive = false;
	local anyEffects = false;

	for k,v in pairs(getWindows()) do
		if v.activatedefensive.getValue() then
			anyDefensive = true;
		end
		if v.activateactive.getValue() then
			anyActive = true;
		end
		if v.activateeffects.getValue() then
			anyEffects = true;
		end
	end

	enableglobaltoggle = false;
	window.button_global_defensive.setValue(anyDefensive);
	window.button_global_active.setValue(anyActive);
	window.button_global_effects.setValue(anyEffects);
	enableglobaltoggle = true;
end

function addPc(source)
	-- Parameter validation
	if not source then
		return nil;
	end

	-- Create a new combat tracker window
	local newentry = NodeManager.createSafeWindow(self);
	if newentry then
		-- Shortcut
		newentry.link.setValue("charsheet", source.getNodeName());
		newentry.link.setVisible(true);

		-- Type
		-- NOTE: Set to PC after link set, so that fields are linked correctly
		newentry.type.setValue("pc");

		-- Token
		local tokenval = NodeManager.getSafeChildValue(source, "combattoken", nil);
		if tokenval then
			newentry.token.setPrototype(tokenval);
		end

		-- FoF
		newentry.friendfoe.setSourceValue("friend");
	end
	
	return newentry;
end

function addBattle(source)
	-- Parameter validation
	if not source then
		return nil;
	end

	-- Cycle through the NPC list, and add them to the tracker
	local npclistnode = source.getChild("npclist");
	if npclistnode then
		for k,v in pairs(npclistnode.getChildren()) do
			local n = NodeManager.getSafeChildValue(v, "count", 0);
			for i = 1, n do
				if v.getChild("link") then
					local npcclass, npcnodename = v.getChild("link").getValue();
					local npcnode = DB.findNode(npcnodename);
					
					local newentry = addNpc(npcnode, NodeManager.getSafeChildValue(v, "name", ""));
					if newentry then
						local npctoken = NodeManager.getSafeChildValue(v, "token", "");
						if npctoken ~= "" then
							newentry.token.setPrototype(npctoken);
						end
					else
						ChatManager.SystemMessage("Could not add '" .. NodeManager.getSafeChildValue(v, "name", "") .. "' to combat tracker");
					end
				end
			end
		end
	end
end

function addNpc(source)
	-- Determine the options relevant to adding NPCs
	local opt_nnpc = OptionsManager.getOption("NNPC");
	local opt_rhps = OptionsManager.getOption("RHPS");
	local opt_init = OptionsManager.getOption("INIT");

	-- Create a new NPC window to hold the data
	local newentry = NodeManager.createSafeWindow(self);
	if newentry then
		newentry.type.setValue("npc");

		newentry.link.setValue("npc", source.getNodeName());
		newentry.link.setVisible(true);

		-- Name
		local namelocal = "";
		local namecount = 0;
		local highnum = 0;
		local last_init = 0;
		if source.getChild("name") then
			namelocal = source.getChild("name").getValue();
		end
		newentry.name.setValue(namelocal);
		if string.len(namelocal) > 0 then
			for k, v in ipairs(getWindows()) do
				if newentry.name.getValue() == getWindows()[k].name.getValue() then
					namecount = 0;
					for l, w in ipairs(getWindows()) do
						local check = null;
						if getWindows()[l].name.getValue() == namelocal then
							check = 0;
						elseif string.sub(getWindows()[l].name.getValue(), 1, string.len(namelocal)) == namelocal then
							check = tonumber(string.sub(getWindows()[l].name.getValue(), string.len(namelocal)+2));
						end
						if check then
							namecount = namecount + 1;
							local cur_init = getWindows()[l].initresult.getValue();
							if cur_init ~= 0 then
								last_init = cur_init;
							end
							if highnum < check then
								highnum = check;
							end
						end
					end 
					if opt_nnpc == "append" then
						getWindows()[k].name.setValue(newentry.name.getValue().." "..highnum+1); 
					end
				end
			end
		end
		if namecount < 2 then
			newentry.name.setValue(namelocal);
		end

		-- Space/reach
		if source.getChild("spacereach") then
			local spacereachstr = source.getChild("spacereach").getValue();
			local space, reach = string.match(spacereachstr, "(%d+)%D*/?(%d+)%D*");
			if space then
				newentry.space.setValue(space);
				newentry.reach.setValue(reach);
			end
		end

		-- Token
		if source.getChild("token") then
			newentry.token.setPrototype(source.getChild("token").getValue());
		end

		-- FoF
		if source.getChild("alignment") then
			local alignment = source.getChild("alignment").getValue();
			if string.find(string.lower(alignment), "good", 0, true) then
				newentry.friendfoe.setSourceValue("friend");
			elseif string.find(string.lower(alignment), "evil", 0, true) then
				newentry.friendfoe.setSourceValue("foe");
			else
				newentry.friendfoe.setSourceValue("neutral");
			end
		else
			newentry.friendfoe.setSourceValue("neutral");
		end

		-- HP
		if opt_rhps == "all" then
			local hdstring = "";
			if source.getChild("hd") then
				hdstring = source.getChild("hd").getValue();
			end
			local rollhp = 0;
			for clause in string.gmatch(hdstring, "(%-?[%dd]+)") do
				i, j = string.find(clause, "d");
				if i then
					neg = 0;
					numdie = tonumber(string.sub(clause, 1, i-1));
					if numdie < 0 then
						neg = 1;
						numdie = -numdie;
					end
					dietype = tonumber(string.sub(clause, j+1));
					for i = 1, numdie do
						if neg > 0 then
							rollhp = rollhp - math.random(dietype);
						else
							rollhp = rollhp + math.random(dietype);
						end
					end
				else
					rollhp = rollhp + tonumber(clause);
				end
			end
			newentry.hp.setValue(rollhp);
		elseif opt_rhps == "group" then
			if source.getChild("hp") and namecount < 2 then
				newentry.hp.setValue(source.getChild("hp").getValue());
			else
				local hdstring = "";
				if source.getChild("hd") then
					hdstring = source.getChild("hd").getValue();
				end
				local rollhp = 0;
				for clause in string.gmatch(hdstring, "(%-?[%dd]+)") do
					i, j = string.find(clause, "d");
					if i then
						neg = 0;
						numdie = tonumber(string.sub(clause, 1, i-1));
						if numdie < 0 then
							neg = 1;
							numdie = -numdie;
						end
						dietype = tonumber(string.sub(clause, j+1));
						for i = 1, numdie do
							if neg > 0 then
								rollhp = rollhp - math.random(dietype);
							else
								rollhp = rollhp + math.random(dietype);
							end
						end
					else
						rollhp = rollhp + tonumber(clause);
					end
				end
				newentry.hp.setValue(rollhp);
			end
		else
			if source.getChild("hp") then
				newentry.hp.setValue(source.getChild("hp").getValue());
			end
		end

		-- Defensive properties
		if source.getChild("ac") then newentry.ac.setValue(source.getChild("ac").getValue()) end;
		if source.getChild("fortitudesave") then newentry.fortitudesave.setValue(source.getChild("fortitudesave").getValue()) end;
		if source.getChild("reflexsave") then newentry.reflexsave.setValue(source.getChild("reflexsave").getValue()) end;
		if source.getChild("willsave") then newentry.willsave.setValue(source.getChild("willsave").getValue()) end;

		-- Active properties
		if source.getChild("init") then newentry.init.setValue(source.getChild("init").getValue()) end;
		if source.getChild("speed") then newentry.speed.setValue(source.getChild("speed").getValue()) end;
		if source.getChild("atk") then newentry.atk.setValue(source.getChild("atk").getValue()) end;
		if source.getChild("fullatk") then newentry.fullatk.setValue(source.getChild("fullatk").getValue()) end;

		-- If the NPC has fast healing then, add an effect for it
		local sq_str = string.lower(NodeManager.getSafeChildValue(source, "specialqualities", ""));
		local fheal_val = string.match(sq_str, "fast healing (%d+)");
		if fheal_val then
			CombatCommon.addEffect("", "", newentry.getDatabaseNode(), "FHEAL: " .. fheal_val);
		end

		--Roll initiative and sort
		if opt_init == "group" then
			if (namecount < 2) or (last_init == 0) then
				newentry.initresult.setValue(math.random(20)+newentry.init.getValue());
			else
				newentry.initresult.setValue(last_init);
			end
			applySort(getWindows());
		elseif opt_init == "all" then
			newentry.initresult.setValue(math.random(20)+newentry.init.getValue());
			applySort(getWindows());
		end
	end

	return newentry;
end

function getActiveEntry()
	for k, v in ipairs(getWindows()) do
		if v.isActive() then
			return v;
		end
	end
	
	return nil;
end

function requestActivation(entry)
	-- Make all the CT entries inactive
	for k, v in ipairs(getWindows()) do
		v.setActive(false);
	end
	
	-- Make the given CT entry active
	entry.setActive(true);

	-- If we created a new speaker, then remove it
	if ct_active_name ~= "" then
		GmIdentityManager.removeIdentity(ct_active_name);
		ct_active_name = "";
	end

	-- Check the option to set the active CT as the GM voice
	if OptionsManager.isOption("CTAV", "on") then
		-- Set up the current CT entry as the speaker if NPC, otherwise just change the GM voice
		if entry.type.getValue() == "pc" then
			GmIdentityManager.activateGMIdentity();
		else
			local name = entry.name.getValue();
			if GmIdentityManager.existsIdentity(name) then
				GmIdentityManager.setCurrent(name);
			else
				ct_active_name = name;
				GmIdentityManager.addIdentity(name);
			end
		end
	end
end

function nextActor()
	-- Find the active actor.  If no active actor, then start the next round
	local active = getActiveEntry();
	if not active then
		nextRound();
		return;
	end
	
	-- Find the next actor.  If no next actor, then start the next round
	local nextactor = getNextWindow(active);
	if not nextactor then
		nextRound();
		return;
	end

	for k, v in ipairs(getWindows()) do
		v.effects.progressEffects(active, nextactor);
	end
			
	requestActivation(nextactor);
end

function nextRound()
	-- Find the active entry, if available
	local active = getActiveEntry();

	-- Find the next actor, if available
	local nextactor = getNextWindow(nil);
	
	-- Progress the effects on each combat tracker entry over the round boundary
	for k, v in ipairs(getWindows()) do
		if active then
			v.effects.progressEffects(active, nil);
		end
		if nextactor then
			v.effects.progressEffects(nil, nextactor);
		end
	end
	
	-- If we have a next actor, then activate it
	if nextactor then
		requestActivation(nextactor);
	end
	
	-- Increment the combat tracker round counter
	window.roundcounter.setValue(window.roundcounter.getValue() + 1);
end

function resetInit()
	-- Set all CT entries to inactive and reset their init value
	for k, v in ipairs(getWindows()) do
		v.setActive(false);
		v.initresult.setValue(0);
	end
	
	-- Remove the active CT from the speaker list
	if ct_active_name ~= "" then
		GmIdentityManager.removeIdentity(ct_active_name);
		ct_active_name = "";
	end
end

function resetEffects()
	for k, v in ipairs(getWindows()) do
		-- Delete all current effects
		v.effects.reset(true);

		-- Hide the effects sub-section
		v.activateeffects.setValue(false);
		v.setEffectsVisible(false);
	end
end

function stripCreatureNumber(s)
	local starts, ends, creature_number = string.find(s, " ?(%d+)$");
	if not starts then
		return s;
	end
	return string.sub(s, 1, starts), creature_number;
end

function rollEntryInit(ctentry)
	-- For PCs, we always roll unique initiative
	if ctentry.type.getValue() == "pc" then
		ctentry.initresult.setValue(math.random(20) + ctentry.init.getValue());
		return;
	end
	
	-- For NPCs, if NPC init option is not group, then roll unique initiative
	local opt_init = OptionsManager.getOption("INIT");
	if opt_init ~= "group" then
		ctentry.initresult.setValue(math.random(20) + ctentry.init.getValue());
		return;
	end

	-- For NPCs with group option enabled
	
	-- Get the entry's database node name and creature name
	local ctentrynodename = ctentry.getDatabaseNode().getNodeName();
	local ctentryname = stripCreatureNumber(ctentry.name.getValue());
	if ctentryname == "" then
		ctentry.initresult.setValue(math.random(20) + ctentry.init.getValue());
		return;
	end
		
	-- Iterate through list looking for other creature's with same name
	local last_init = 0;
	for k,v in pairs(getWindows()) do
		if ctentrynodename ~= v.getDatabaseNode().getNodeName() then
			local tempentryname = stripCreatureNumber(v.name.getValue());
			if tempentryname == ctentryname then
				local cur_init = v.initresult.getValue();
				if cur_init ~= 0 then
					last_init = cur_init;
				end
			end
			
		end
	end
	
	-- If we found similar creatures with non-zero initiatives, then match the initiative of the last one found
	if last_init == 0 then
		ctentry.initresult.setValue(math.random(20) + ctentry.init.getValue());
	else
		ctentry.initresult.setValue(last_init);
	end
end

function rollAllInit()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			v.initresult.setValue(0);
		end
	end

	for k, v in ipairs(getWindows()) do
		rollEntryInit(v);
	end
end

function rollPCInit()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "pc" then
			rollEntryInit(v);
		end
	end
end

function rollNPCInit()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			v.initresult.setValue(0);
		end
	end

	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			rollEntryInit(v);
		end
	end
end

function deleteNPCs()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			v.delete();
		end
	end
end
