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

function onInit()
	if super and super.onInit then
		super.onInit();
	end
	
	Input.onControl = onControl;
end

function onControl(pressed)
	if hoverAttackCombo or hoverDamage then
		onHoverUpdate(hoverx, hovery);
	end
end

function onHover(oncontrol)
	if dragging then
		return;
	end

	-- Reset selection when the cursor leaves the control
	if not oncontrol then
		hoverDamage = nil;
		hoverAttackCombo = nil;
		
		setSelectionPosition(0);
		--setCursorPosition(0);
	end
end

function onHoverUpdate(x, y)
	hoverx, hovery = x, y;

	if hasFocus() or dragging then
		return;
	end

	-- Hilight skill hovered on
	components = parseComponents();
	local index = getIndexAt(x, y);

	hoverDamage = nil;
	hoverAttackCombo = nil;
	
	for i = 1, #damages do
		if damages[i].startpos < index and damages[i].endpos > index then
			setCursorPosition(damages[i].startpos);
			setSelectionPosition(damages[i].endpos);

			hoverDamage = i;			
			
			setHoverCursor("hand");
			
			return;
		end
	end
	
	if Input.isControlPressed() then
		for i = 1, #attacks do
			if attacks[i].startpos < index and attacks[i].endpos > index then
				setCursorPosition(damages[i].startpos);
				setSelectionPosition(damages[i].endpos);
	
				hoverDamage = i;			
				
				setHoverCursor("hand");
				
				return;
			end
		end
	end
	
	for i = 1, #attackcombinations do
		local firstattack = attackcombinations[i][1];
		local lastattack = attackcombinations[i][#(attackcombinations[i])];
		
		if attacks[firstattack].startpos < index and attacks[lastattack].endpos > index then
			setCursorPosition(attacks[firstattack].startpos);
			setSelectionPosition(attacks[lastattack].endpos);

			hoverAttackCombo = i;
			
			setHoverCursor("hand");
			
			return;
		end
	end
	
	setHoverCursor("arrow");
	
	setCursorPosition(0);
end

function onClickDown(button, x, y)
	-- Suppress default processing to support dragging
	clickDamage = hoverDamage;
	clickAttackCombo = hoverAttackCombo;
	
	return true;
end

function onClickRelease(button, x, y)
	-- Enable edit mode on mouse release
	setFocus();
	
	local n = getIndexAt(x, y);
	
	setSelectionPosition(n);
	setCursorPosition(n);
	
	return true;
end

function onDoubleClick(x, y)
	if hoverDamage then
		local dmg_text, dmg_dice, dmg_bonus = getDamage(damages[hoverDamage]);
		ChatManager.DoubleClickNPC("damage", dmg_bonus, dmg_text, {npc = window.getDatabaseNode()}, dmg_dice);
		
		return true;
	end
	
	if hoverAttackCombo then
		local firstattack = attackcombinations[hoverAttackCombo][1];
		local lastattack = attackcombinations[hoverAttackCombo][#(attackcombinations[hoverAttackCombo])];
		local a = 1;
		
		-- Make sure we preserve the current modifier for all attacks
		local mod_count = 0;
		for i = firstattack, lastattack do
			local attack = attacks[i];
			local modifiers = {}
			for w in string.gmatch(attack.modifier, "([%+%-]?%d+)/?") do
				table.insert(modifiers, w);
			end
			for j = 1, attack.count do
				for k = 1, #modifiers do
					mod_count = mod_count + 1;
				end
			end
		end
		ModifierStack.setLockCount(mod_count);
		
		-- Calculate the crit range
		local crit_threshold = 20;
		local crit_start, crit_end, critrange = string.find(damages[hoverAttackCombo].damage, "/(%d+)-%d+");
		if critrange then
			crit_threshold = tonumber(critrange);
			if not crit_threshold then
				crit_threshold = 20;
			end
		end

		-- Now execute the attacks
		for i = firstattack, lastattack do
			local attack = attacks[i];

			-- Break modifier into attacks
			local modifiers = {}
			for w in string.gmatch(attack.modifier, "([%+%-]?%d+)/?") do
				table.insert(modifiers, w);
			end
			
			local desc = attack.label;
			
			for j = 1, attack.count do
				for k = 1, #modifiers do
					
					local attack_name = "[ATTACK";
					
					-- Decide if we should add an attack #
					local attack_num = 1;
					if #modifiers <= 1 then
						if tonumber(attack.count) > 1 then
							attack_num = j;
						end
					else
						attack_num = k;
					end
					if attack_num > 1 then
						attack_name = attack_name .. " #" .. attack_num;
					end
					
					-- Add the attack description
					attack_name = attack_name .. "] " .. desc;
					
					-- Add the crit tag if there is an expanded crit range
					if crit_threshold < 20 then
						attack_name = attack_name .. " [CRIT " .. crit_threshold .. "]";
					end
					
					ChatManager.DoubleClickNPC("attack", modifiers[k], attack_name, {npc = window.getDatabaseNode(), crit = critrange});
				end
			end
		end
		
		return true;
	end
end

function onDrag(button, x, y, draginfo)
	if dragging then
		return true;
	end

	if clickDamage then
		local dmg_text, dmg_dice, dmg_bonus = getDamage(damages[clickDamage]);
		
		draginfo.setType("damage");
		draginfo.setDescription(dmg_text);
		draginfo.setDieList(dmg_dice);
		draginfo.setNumberData(dmg_bonus);
		draginfo.setShortcutData("npc", window.getDatabaseNode().getNodeName());
		
		clickDamage = nil;
		dragging = true;
		return true;
	end

	if clickAttackCombo then
		draginfo.setType("fullattack");

		local firstattack = attackcombinations[clickAttackCombo][1];
		local lastattack = attackcombinations[clickAttackCombo][#(attackcombinations[clickAttackCombo])];
		local a = 1;
		
		-- Calculate the crit range
		local crit_threshold = 20;
		local crit_start, crit_end, critrange = string.find(damages[clickAttackCombo].damage, "/(%d+)-%d+");
		if critrange then
			crit_threshold = tonumber(critrange);
			if not crit_threshold then
				crit_threshold = 20;
			end
		end

		for i = firstattack, lastattack do
			local attack = attacks[i];

			-- Break modifier into attacks
			local modifiers = {}
			for w in string.gmatch(attack.modifier, "([%+%-]?%d+)/?") do
				table.insert(modifiers, w);
			end
			
			local desc = attack.label;
			
			for j = 1, attack.count do
				for k = 1, #modifiers do
					-- Build the attack name
					local attack_name = "[ATTACK";
					
					-- Decide if we should add an attack #
					local attack_num = 1;
					if #modifiers <= 1 then
						if tonumber(attack.count) > 1 then
							attack_num = j;
						end
					else
						attack_num = k;
					end
					if attack_num > 1 then
						attack_name = attack_name .. " #" .. attack_num;
					end
					
					-- Add the attack description
					attack_name = attack_name .. "] " .. desc;
					
					-- Add the crit tag if there is an expanded crit range
					if crit_threshold < 20 then
						attack_name = attack_name .. " [CRIT " .. crit_threshold .. "]";
					end
					
					draginfo.setDieList({ "d20" });
					draginfo.setNumberData(modifiers[k]);
					draginfo.setShortcutData("npc", window.getDatabaseNode().getNodeName());
					draginfo.setStringData(attack_name);

					a = a + 1;
					draginfo.setSlot(a);
				end
			end
		end
		
		clickAttackCombo = nil;
		dragging = true;
		return true;
	end

	return true;
end

function onDragEnd(dragdata)
	setCursorPosition(0);
	dragging = false;
end

function getDamage(dmg_entry)
	-- Set up variables to catch base damage dice, damage bonus and critical multiplier/threshold
	local basebonus = 0;
	local basedice = {};
	local critmult = 2;
	local crit_threshold = 20;

	-- Parse the attack line to determine the base damage dice and bonus
	local s = dmg_entry.damage;
	local starts = true;
	local nextindex = 1;
	while starts and nextindex < #s do
		if string.sub(s, nextindex, nextindex) == "/" then
			-- Critical range declaration
			
			-- Crit threshold
			local crit_str = string.match(s, "(%d+)%-20", nextindex + 1);
			if crit_str then
				crit_threshold = tonumber(crit_str) or 0;
				if crit_threshold <= 0 or crit_threshold > 20 then
					crit_threshold = 20;
				end
			end

			-- Crit multiplier
			crit_str = string.match(s, "x(%d)", nextindex + 1);
			if crit_str then
				critmult = tonumber(crit_str) or 0;
				if critmult < 2 then
					critmult = 2;
				end
			end
			
			-- End at critical range declaration
			break;
		end

		starts, ends, sign, count, die, remainder = string.find(s, "([+-]?)(%d*)(%w*)([^+-/]*)", nextindex);

		local signmultiplier = 1;
		if sign == "-" then
			signmultiplier = -1;
		end

		if #die == 0 then
			basebonus = basebonus + signmultiplier * count;
		else
			local diecount = tonumber(count) or 1;
			for c = 1, diecount do
				table.insert(basedice, die);
			end
		end

		nextindex = ends+1;
	end

	local dmg_text = "[DAMAGE] " .. dmg_entry.label;
	
	local dmg_dice = {};
	local dmg_bonus = 0;
	local isCritical = Input.isShiftPressed();
	if isCritical then
		for i = 1, critmult do
			for j = 1, #basedice do
				table.insert(dmg_dice, basedice[j]);
			end
		end

		dmg_bonus = critmult * basebonus;

		dmg_text = dmg_text .. " [CRITICAL]";
	else
		dmg_dice = basedice;
		dmg_bonus = basebonus;
	end
		
	return dmg_text, dmg_dice, dmg_bonus;
end

function parseComponents()
	-- Check the anon-npc option
	local opt_anpc = OptionsManager.getOption("ANPC");
		
	-- Break the string in the control into attacks, and record their respective positions
	str = getValue();

	damages = {};
	attacks = {};
	attackcombinations = {};
	
	local currentcombination = {};
	
	or_index = 1;
	attackindex = 1;
	
	while or_index < #str do
	
		-- Parse out each OR phrase of the attack string
		or_phrase = "";
		or_starts, or_ends = string.find(str, ' or ', or_index);
		if not or_starts then
			or_phrase = string.sub(str, or_index);
		else
			or_phrase = string.sub(str, or_index, or_ends);
		end
		
		-- Within the OR phrase, parse out each AND phrase
		and_index = 1;
		while and_index < #or_phrase do
		
			-- Parse out each AND phrase
			and_phrase = "";
			and_starts, and_ends = string.find (or_phrase, ' and ', and_index);
			if not and_starts then
				and_phrase = string.sub(or_phrase, and_index);
			else
				and_phrase = string.sub(or_phrase, and_index, and_ends);
			end

			-- Look for the right patterns
			starts, ends, all, pcount, plabel, pmodifier, patktype, dmgstart, pdamage, dmgend = string.find(and_phrase, '((%+?%d*) ?([%w%s,%[%]%(%)%+%-]*) ([%+%-%d/]*) (%w*%s?%w*) ?%(()([^%)]*)()%))');
			
			-- Make sure we got a match
			if starts then
				
				-- Clean up the pcount field (i.e. magical weapon bonuses up front, no attack count)
				if #pcount < 1 then
					pcount = 1;
				end
				if string.sub(pcount, 1, 1) == "+" then
					plabel = pcount .. " " .. plabel;
					pcount = 1;
				end

				-- If the anonymize option is on, then remove any label text within parentheses or brackets
				if opt_anpc == "on" then
					-- Strip out label information enclosed in ()
					local newlabel = "";
					local paren_index = 1;
					local paren_starts = true;
					while paren_starts and paren_index < #plabel do
						paren_starts, paren_ends = string.find(plabel, "%b()", paren_index);
						if paren_starts then
							newlabel = newlabel .. string.sub(plabel, paren_index, paren_starts-1);
							paren_index = paren_ends + 1;
						else
							newlabel = newlabel .. string.sub(plabel, paren_index);
						end
					end
					plabel = newlabel;

					-- Strip out label information enclosed in []
					newlabel = "";
					paren_index = 1;
					paren_starts = true;
					while paren_starts and paren_index < #plabel do
						paren_starts, paren_ends = string.find(plabel, "%s?%b[]", paren_index);
						if paren_starts then
							newlabel = newlabel .. string.sub(plabel, paren_index, paren_starts-1);
							paren_index = paren_ends + 1;
						else
							newlabel = newlabel .. string.sub(plabel, paren_index);
						end
					end
					plabel = newlabel;
				end

				-- Capitalize first letter of label
				plabel = string.upper(string.sub(plabel, 1, 1)) .. string.sub(plabel, 2);
				
				-- Add to the attack list
				table.insert(damages, { startpos = or_index+and_index+dmgstart-2, endpos = or_index+and_index+dmgend-2, damage = pdamage, label = plabel });
				table.insert(attacks, { startpos = or_index+and_index+starts-2, endpos = or_index+and_index+dmgend-1, label = plabel, modifier = pmodifier, count = pcount });
				table.insert(currentcombination, attackindex);
				attackindex = attackindex + 1;

			end
			
			-- Increment to get the next AND phrase
			and_index = and_index + #and_phrase;
			
		end
		
		-- Finish combination
		if #currentcombination > 0 then
			table.insert(attackcombinations, currentcombination);
			currentcombination = {};
		end

		-- Increment to get the next OR phrase
		or_index = or_index + #or_phrase;

	end
		
	return components;
end
