--[[ 
	
	Utility Class Version		1.02
	Command Class Version	1.03
	Timer Class Version		1.03

	Mairelon's Utility Classes
	Place me in your mod directory.
	In your XML file, load me prior to any scripts.
	
	Version checking occurs - if there is a later
	version of one of these classes alread available,
	it won't overwrite it.

	Utility_Class just provides 2 methods at the 
	moment (And isn't technically an object as there is no
	state maintained).  Echo displays a message over your
	character's head, Print displays the message in the
	chat box.
	
	Command_Class simplifies the addition of sophisticated
	slash commands.  It replaces large if-then-elseif-else-end
	constructs with a callback function system.  It also 
	encapsulates basic usage messages and limited parameter
	parsing.
	
	Timer_Class encapsulates a basic timer, capable of displaying
	end of timer messages, playing sounds or calling a callback.
	Timers can be one-shot or recurring, be paused, restarted and
	reset.
	
	Last Modified
		01/13/2005
			Fixed mac crash in Timer_Class
		01/16/2005
			added parameter checking to command class
		03/31/2005
			added ability to specify command text to command class
		02/06/2005
			Added stack class 1.0
		04/14/2005
			Fixed bug in timer class that was eating memory
		08/21/2005
			Fixed potential bug - %%qt changed to %&qt in GetParameters for consistancy
--]]
-- Class declarations

-- Utility class provides print (to the chat box) and echo (displays over your character's head).
-- Instantiate it and use the colon syntax.
-- Color is an optional argument.  You can either use one of 7 named colors
-- "red", "green", "blue", "yellow", "cyan", "magenta", "white" or
-- a table with the r, g, b values.
-- IE foo:Print("some text", {r = 1.0, g=1.0, b=.5})

-- Version 1.02 has a new table copy function.

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.
if not Utility_Class or (not Utility_Class.version) or (Utility_Class.version < 1.02) then
	Utility_Class = {};
	Utility_Class.version = 1.02
	function Utility_Class:New ()
		local o = {}   -- create object
		setmetatable(o, self)
		self.__index = self
		return o
	end
	
	function Utility_Class:Print(msg, color)
	-- the work for these is done in get-color, otherwise it's just adding
	-- the text to the chat frame.
	if msg == nil then return end;
	local r, g, b;
		if msg == nil then return; end
		if color == nil then color = "white"; end
		r, g, b = self:GetColor(color);
		
		if( DEFAULT_CHAT_FRAME ) then
			DEFAULT_CHAT_FRAME:AddMessage(msg,r,g,b);
		end
		
	end
	
	function Utility_Class:Echo(msg, color)
	-- the work for these is done in get-color, otherwise it's just adding
	-- the text to the UIERRORS frame.
	if msg == nil then return end;
	local r, g, b;
		if msg == nil then return; end
		if color == nil then color = "white"; end
		r, g, b = self:GetColor(color);
		
		UIErrorsFrame:AddMessage(msg, r, g, b, 1.0, UIERRORS_HOLD_TIME);
		
	end
	
	function Utility_Class:GetColor(color)
	-- Turn any string color into its rgb, check any table arg for
	-- being in-bounds, return the appropriate RGB values.
		if color == nil then color = self end
		if color == nil then return 1, 1, 1 end
	
		if type(color) == "string" then 
			color = Utility_Class.ColorList[string.lower(color)];
		end
		
		if type(color) == "table" then
			if color.r == nil then color.r = 0.0 end
			if color.g == nil then color.g = 0.0 end
			if color.b == nil then color.b = 0.0 end
		else
			return 1, 1, 1 
		end
	
		if color.r < 0 then color.r = 0.0 end
		if color.g < 0 then color.g = 0.0 end
		if color.b < 0 then color.g = 0.0 end
	
		if color.r > 1 then color.r = 1.0 end
		if color.g > 1 then color.g = 1.0 end
		if color.b > 1 then color.g = 1.0 end
		
		return color.r, color.g, color.b
		
	end
	
	-- Straight forward list of primary/complement colors and their r, g, b values.
	Utility_Class.ColorList = {}
	Utility_Class.ColorList["red"] = { r = 1.0, g = 0.0, b = 0.0 }
	Utility_Class.ColorList["green"] = { r = 0.0, g = 1.0, b = 0.0 }
	Utility_Class.ColorList["blue"] = { r = 0.0, g = 0.0, b = 1.0 }
	Utility_Class.ColorList["white"] = { r = 1.0, g = 1.0, b = 1.0 }
	Utility_Class.ColorList["magenta"] = { r = 1.0, g = 0.0, b = 1.0 }
	Utility_Class.ColorList["yellow"] = { r = 1.0, g = 1.0, b = 0.0 }
	Utility_Class.ColorList["cyan"] = { r = 0.0, g = 1.0, b = 1.0 }

-- Recursive table copy function.  Copies by value
	function Utility_Class:TableCopy(table1)
	local table2 = {};
	if table1 == nil then return table2 end
	local index, value
		for index, value in pairs(table1) do
		local text = index .. " "
			if type(value) == "table" then
				table2[index] = Utility_Class:TableCopy(value)
			else
				table2[index] = value
			end
		end
		return table2;
	end

end


-- Command_Class provides an easy way to handle slash commands and their associated
-- sub-commands, parameters and usage statements.
-- Instantiate a Command_Class variable with Command_Class:New(modname) instead of
-- SlashCmdList[modname] = Slash_Command_Callback.
-- After that, use :AddCommand("command", callback, "group", "usage") to add new
-- sub-commands.

-- If the command on the command line does not match one of the added commands, 
-- displayusage() will be called for the main group.  Use seperate groups to partition
-- your usage statements into manageable sets.

-- Version 1.01 - fixed error in dispatch code.
-- Version 1.02 - add parameter checking methods
-- Version 1.03 - added command text

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.

if (not Command_Class) or (not Command_Class.version) or (Command_Class.version < 1.03) then
    Command_Class = {}
    Command_Class.version=1.03
    
--  Instantiate the new object  - for more on OO in Lua see the book
--  Programming in Lua at Lua.org.
    function Command_Class:New(modname,command)
		if (modname == nil) then return nil end
		if (command == nil) then command = "/" .. string.lower(modname) end
		local o = {}
        o.CommandList = {}
        o.UsageList = {}
		o.UsageList.n=0
        o.GroupList = {}
		o.ParamList = {}
		SlashCmdList[modname .. "COMMAND"] = function(msg) o:Dispatch(msg) end
		setglobal("SLASH_" .. modname .. "COMMAND1", command)
        setmetatable(o, self)
        self.__index = self
        return o
    end

--  This the instance method that will be assigned to the slash command.
--  this replaces the huge if/then/elseif structures common to my mods so far.    
--  msg             is the text passed to the command by the WoW environment
--  command     	is the text of the command we are looking for
--  dispatch       	is the function to call on that command.
    function Command_Class:Dispatch(msg)
    --  first, pull off the first token to compare against the command text
		if msg == nil then msg = "" end
		local token
        local firsti, lasti = string.find(msg, " ")
        if firsti then
            token = string.sub(msg, 1, firsti-1)
        else
            token = msg
        end
    -- the command function will expect the remainder of msg as an argument
        if lasti then
            msg = string.sub(msg, lasti+1)
        else
            msg = ""
        end
    -- ensure it gets the empty string rather than nil
        if msg == nil then msg = "" end;
    --  if command exists - dispatch it.
		if self.CommandList[string.lower(token)] then
			local dispatch = self.CommandList[string.lower(token)]
			dispatch(msg)
		else
		--  no match found - display the usage message.
			self:DisplayUsage()
		end
    end

--  Method to display the optional usage message attached to the command
--  if no group is specified, the main group usage is displayed.  To partition
--  large command lists, seperate them into groups and add commands to 
--  display non-main groups.
    function Command_Class:DisplayUsage(group)
    --  Simply iterate through and display the usage line for each command in group
		local util = Utility_Class:New()
        local index, usage
        if group == nil then group = "main" end
        for index, usage in ipairs(self.UsageList) do
            if self.GroupList[index] == group then
                util:Print(self.UsageList[index])
            end
        end
    end

--  Method to add commands to the recoginized command list.
--  Arguments are:
--  command:    The text we are searching for in the slash command line 
--                      (case insensitive)
--  dispatch:      The name of the function that will handle this command
--  group:           Optional (will default to main) used in display usage
--  usage           Optional (will default to command) used in display usage
    function Command_Class:AddCommand(command, dispatch, group, usage)
        self.CommandList[command]=dispatch
        if usage == nil then usage =command end
		self.UsageList.n = self.UsageList.n + 1
        self.UsageList[self.UsageList.n]=usage
        if group == nil then group = "main" end
        group = string.lower(group)
        self.GroupList[self.UsageList.n]=group
		self.ParamList[command]={}
    end

-- Method to parse msg for name=value pairs
-- handles the following types:
-- name=<number>  where <number> is any whole number
-- returns <number> in return["name"]

-- name=<number>-<number> where number1 and number2 are any whole number
-- returns a table in return["name"] containing all the numbers from
-- number1 to number 2 (inclusive)

-- name=[<number> <number> .... <number>]  where number is a whole number.
-- returns the table with all the numbers in return["name"]

-- name='<string with spaces>'  returns the arbitrary string in return["name"], all
-- characters allowed except '


	function Command_Class:GetParameters(msg)
	if msg == nil then return {} end
	-- pattern to return name=
	msg = string.gsub(msg,"\\'","%&qt")
	local pattern = "([%a_][%w_]*)="
	local index = 1
	local params = {}
		local firsti, lasti, capture = string.find(msg,pattern)
		-- while we have a name=, process the info after it.
		while capture do
			local varname=string.lower(capture)
			index = index+lasti
			firsti, lasti, capture = string.find(string.sub(msg, index),pattern)
			if not firsti then firsti=string.len(msg) else firsti=firsti-2 end
			local str = string.sub(msg,index, index+firsti)
			-- if the start is ', we have a string
			if string.sub(str,1,1) == "'" then
				local location = string.find(string.sub(str,2),"'")
				if location and (location>1) then
					params[varname]=string.gsub(string.sub(str,2,location),"%&qt","'")
				end
			-- we have a table
			elseif string.sub(str,1,1) == "[" then
				local table1={}
				local element
				local index = 1
				for element in string.gfind(str,"'([^']+)'") do
					table1[index] = string.gsub(element,"%&qt","'")
					index = index+1
				end
				if not (index > 1) then
				-- no strings in table - look for numbers
					for element in string.gfind(str,"([-]?%d+)") do
						table1[index] = element+0
						index = index+1
					end
				end
				if index > 1 then
					params[varname] = table1
				end
			-- we have a range of values
			elseif string.find(str,"([-]?%d+)-([-]?%d+)") then
				local firsti, lasti, startrange, endrange = string.find(str,"([-]?%d+)-([-]?%d+)")
				local index = 1
				local table = {}
				local value;
				if firsti then
					for value = startrange+0, endrange+0 do
						table[index]=value
						index=index+1
					end
				end
				if index>1 then
					params[varname]=table
				end
			-- Not a string, range or table, so extract a number from it
			else
				local firsti, lasti, value = string.find(str,"([-]?%d+)")
				if value then
					params[varname]=value+0
				end
			end
		end
		return params
	end	
	
-- Method to add a parameter to a command for automatic checking.
-- Inputs:
-- command 	= the command this parameter is for (string)
-- param 		= the parameter name (string)
-- required	= whether the parameter is required or optional (true or nil)
-- strict		= if strict is true, error if values are not in allowed values and return false
--			= otherwise show a warning on values not in allowed range and return true
-- types		= what types are allowed for this param - needs to be a table of strings.
--			if "string" is in the table then type string is allowed
--			if "number" is in the table then type number is allowed
--			if "table" is in the table then tables are allowed BUT - if either of the
--			previous 2 are specified, only tables of that type.
--			EG 	{"string"}  allows only strings
--				{"number"} allows only numbers
--				{"table"} allows only tables (no restriction on contents)
--				{"string" "table"} allows strings or tables of strings
--				{"number" "table"} allows numbers or tables of numbers
--				{"number" "string" "table"} allows  any value my command class can parse,
--				equivalent to {} or nil
-- allowed		Values that are allowed for this parameter.  Must be a table.
--			if lowerbound or upperbound are set, it will check numeric values against them:
--			eg { lowerbound=5, upperbound=10 } will only allow values that are >= 5 and <=10
--			but will be ignored if the value is non-numeric.  Enforce that with types
--			if lowerbound and upperbound are both omitted, it is simply a list of allowed values
--			eg { 'true', 'false' } will only allow those 2 strings and IS case sensitive
-- default		default value to assign in the case this item is optional and nil

	function Command_Class:AddParam(command, param, required, strict, types, allowed, default)
		if not self.CommandList[command] then return end
		self.ParamList[command][param] = {}
		local p = self.ParamList[command][param]
		
		p["required"] = required
		p["strict"] = strict
		p["types"] = types
		p["default"] = default
		p["allowed"] = allowed
	end

-- Method to actually verify the params against the paramlist
-- return true if they are valid, false otherwise
	function Command_Class:CheckParameters(command, args)
		local util = Utility_Class:New()
		local plist = self.ParamList[command]
		local index, param
		-- first check for existence.  If required and nil, raise error
		-- if optional and nil assign default value
		for index, param in pairs(plist) do
			if not args[index] and param["required"] then
				util:Print("Error: " .. index .. " is required")
				return false
			elseif not args[index] and not param["required"] then
				args[index] = param["default"]
			end
			
			-- next - check types
			if param["types"] and args[index] then
				local types={}	-- used to simplify type checking
				local index2
				for index2=1,3 do
					if param["types"][index2] then
						types[param["types"][index2]]=true
					end
				end
				
				-- basic type checking 
				if not types[type(args[index])] then 
					util:Print("Error: " .. index .. " type mismatch " .. type(args[index]) .. " not allowed")
					return false
				end
				
				-- if the argument was a table, and tables are allowed then check each value in the table.
				-- since tables of tables will not be parsed by Command_Class we can explicitly check one level down
				-- without needing recursion
				if type(args[index]) == "table" then
					local index3, value
					for index3, value in ipairs(args[index]) do
						if not types[type(value)] then 
							util:Print("Error: " .. index .. " type mismatch " .. type(value) .. " not allowed")
							return false
						end
					end
				end
			end

			-- if allowed values were specified, check against them
			if param["allowed"] and (args[index]) then
				local vals = param["allowed"]
				-- first, check for lower and upper bounds
				if vals.lowerbound or vals.upperbound then
					-- simple bounds checking for non-table types
					if type(args[index]) == "number" then
						if vals.lowerbound and args[index] < vals.lowerbound then 
							if param["strict"] then
								util:Print("Error: " .. index .. " must be higher than (or equal to)" .. vals.lowerbound)
							else
								util:Print("Warning: " .. index .. " should be higher than (or equal to)" .. vals.lowerbound)
							end
						elseif  vals.upperbound and args[index] > vals.upperbound then
							if param["strict"] then
								util:Print("Error: " .. index .. " must be lower than (or equal to)" .. vals.upperbound)
								return false
							else
								util:Print("Warning: " .. index .. " should be lower than (or equal to) " .. vals.upperbound) 
							end
						end
					-- bounds checking for each individual value for table types
					elseif (type(args[index]) == "table" and type(args[index][1]) == "number") then
						local index5, value
						for index5, value in ipairs(args[index]) do
							if vals.lowerbound and value < vals.lowerbound then 
								if param["strict"] then
									util:Print("Error: " .. index .. " must be higher than (or equal to)" .. vals.lowerbound)
								else
									util:Print("Warning: " .. index .. " should be higher than (or equal to)" .. vals.lowerbound)
								end
							elseif  vals.upperbound and value > vals.upperbound then
								if param["strict"] then
									util:Print("Error: " .. index .. " must be lower than (or equal to)" .. vals.upperbound)
									return false
								else
									util:Print("Warning: " .. index .. " should be lower than (or equal to) " .. vals.upperbound) 
								end
							end
						end
					end
				else
					-- without bounds, we just check against each individual element of allowed
					local valid = false
					-- check arg against each element in allowed
					if type(args[index]) ~= "table" then
						local index6, allowedvalue
						for index6, allowedvalue in ipairs(vals) do
							if args[index] == allowedvalue then
								valid = true
							end
						end
					else
					-- if it's a table, we compare each element of the table against the elements of allowed
						local index7, value						
						for index7, value in ipairs(args[index]) do
							local index6, allowedvalue
							for index6, allowedvalue in ipairs(vals) do
								if value == allowedvalue then
									valid = true
								end
							end
						end
					end
					if (not valid) and param["strict"] then
						util:Print("Error: " .. index .. " not in range of allowed values")
						return false
					elseif (not valid) then
						util:Print("Warning: " .. index .. " has an unrecognized value -- errors may result")
					end
				end
			end
		end
		return true
	end
end

-- A simple timer class.  Timers can be one shot or recurring, they can be paused, reset and restarted
-- Timers can echo a message over head, play a sound file or execute a function at the end of their
-- run

-- Version 1.01 fixed Mac crash.
-- Version 1.03 altered code so the timer is disposed of at the end of it's run

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.

if (not Timer_Class) or (not Timer_Class.version) or (Timer_Class.version < 1.03) then
    Timer_Class = {}
    Timer_Class.version=1.03
	Timer_Class.Util = Utility_Class:New()
-- Create a new timer.  Arguments are:
-- duration	How long (in seconds) the timer should run)
-- recurring	Whether the timer should reset after finishing
-- message	What message to display overhead at the end of the timer
-- sound		Soundfile to play at the end of the timer*****
--		****	The soundfile MUST be in your World of Warcraft/Data directory BEFORE UI load
-- callback	A function to be called at the end of the timer.

-- Duration must exist and be non-negative, or it returns nil
-- at least one of message, sound, callback must be non-nil
-- the timer defaults to running.  If you need it paused, call timer:Pause() after creating it.

	function Timer_Class:New (duration, recurring, message, sound, callback)
		if duration == nil or type(duration) ~= "number" or duration < 0 then return nil end
		if message == nil and sound == nil and callback == nil then return nil end
--		if message == nil then message = ""; end
--		if sound == nil then sound = ""; end
--		if callback == nil then callback = function() end end
		local o = {}   -- create object
		setmetatable(o, self)
		self.__index = self
		o.duration = duration
		o.message = message
		o.recurring = recurring
		o.sound = sound
		o.callback = callback
		o.running = true
		o.currenttime = o.duration
		return o
	end
	
-- Method to call in the mod's OnUpdate function.  Pass OnUpdate's arg1 to the method.
-- The timer is disposed of when it finishes it's run

	function Timer_Class:Update(elapsed)
		if self.running then
			self.currenttime = self.currenttime - elapsed
			if self.currenttime <= 0 then
				if self.recurring then
					self.currenttime = self.duration
				else
					self.running = false
				end
				if self.message then Timer_Class.Util:Echo(self.message) end
				if self.sound then PlaySoundFile(self.sound) end
				if self.callback then self.callback() end
			end
		end
	end	

-- Helper methods to pause a running timer, start a paused timer and reset the timer.	
	function Timer_Class:Pause()
		self.running = nil
	end
	
	function Timer_Class:Start()
		self.running = true;
	end
	
	function Timer_Class:Reset()
		self.currenttime = self.duration
	end

-- Helper methods to return time currently left on the timer, max duration and running state.
	function Timer_Class:GetTimeLeft()
		return self.currenttime;
	end
	
	function Timer_Class:GetDuration()
		return self.duration
	end
	
	function Timer_Class:GetRunning()
		if self.running then return true else return false end
	end
end


-- A simple Stack class.  

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.

if (not Stack_Class) or (not Stack_Class.version) or (Stack_Class.version < 1.02) then
    Stack_Class = {}
    Stack_Class.version=1.02

	function Stack_Class:New ()
		local o = {}   -- create object
		setmetatable(o, self)
		self.__index = self
		o.n = 0
		return o
	end
	
	function Stack_Class:IsEmpty()
		if self.n == 0 then
			return true
		else
			return false
		end
	end
	
	function Stack_Class:Top()
		if not self:IsEmpty() then
			return self[self.n]
		else
			return nil
		end
	end
	
	function Stack_Class:Pop()
		if not self:IsEmpty() then
			local value = self:Top()
			self.n = self.n - 1
			return value
		else
			return nil
		end
	end
	
	function Stack_Class:Push(value)
		if value ~= nil then
			self.n = self.n + 1
			self[self.n] = value
		end
	end
end
