--[[
	Title: Miscellaneous

	Some utility functions. Unlike the functions in util.lua, this file only holds non-HL2 specific functions.
]]

--[[
	Function: explode

	Split a string by a separator.

	Parameters:

		separator - The separator string.
		str - A string.
		limit - *(Optional)* Max number of elements in the table

	Returns:

		A table of str split by separator, nil and error message otherwise.

	Revisions:

		v2.10 - Initial (dragged over from a GM9 archive though)
]]
function ULib.explode( separator, str, limit )
	local t = {}
	local curpos = 1
	while true do -- We have a break in the loop
		local newpos, endpos = str:find( separator, curpos ) -- find the next separator in the string
		if newpos ~= nil then -- if found then..
			table.insert( t, str:sub( curpos, newpos - 1 ) ) -- Save it in our table.
			curpos = endpos + 1 -- save just after where we found it for searching next time.
		else
			if limit and #t > limit then
				return t -- Reached limit
			end
			table.insert( t, str:sub( curpos ) ) -- Save what's left in our array.
			break
		end
	end

	return t
end


--[[
	Function: stripComments

	Strips comments from a string

	Parameters:

		str - The string to stip comments from
		comment - The comment string. If it's found, whatever comes after it on that line is ignored. ( IE: "//" )
		blockcommentbeg - *(Optional)* The block comment begin string. ( IE: "/<star>" )
		blockcommentend - *(Optional, must be specified if above parameter is)* The block comment end string. ( IE: "<star>/" )

	Returns:

		The string with the comments stripped, nil and error otherwise.

	Revisions:

		v2.02 - Fixed block comments in more complicated files.
]]
function ULib.stripComments( str, comment, blockcommentbeg, blockcommentend )
	if blockcommentbeg and string.sub( blockcommentbeg, 1, string.len( comment ) ) == comment then -- If the first of the block comment is the linecomment ( IE: --[[ and -- ).
		string.gsub( str, ULib.makePatternSafe( comment ) .. "[%S \t]*", function ( match )
			if string.sub( match, 1, string.len( blockcommentbeg ) ) == blockcommentbeg then
				return "" -- No substitution, this is a block comment.
			end
			str = string.gsub( str, ULib.makePatternSafe( match ), "", 1 )
			return ""
		end )

		str = string.gsub( str, ULib.makePatternSafe( blockcommentbeg ) .. ".-" .. ULib.makePatternSafe( blockcommentend ), "" )
	else -- Doesn't need special processing.
		str = string.gsub( str, ULib.makePatternSafe( comment ) .. "[%S \t]*", "" )
		if blockcommentbeg and blockcommentend then
			str = string.gsub( str, ULib.makePatternSafe( blockcommentbeg ) .. ".-" .. ULib.makePatternSafe( blockcommentend ), "" )
		end
	end

	return str
end


--[[
	Function: makePatternSafe

	Makes a string safe for pattern usage, like in string.gsub(). Basically replaces all keywords with % and keyword.

	Parameters:

		str - The string to make pattern safe

	Returns:

		The pattern safe string
]]
function ULib.makePatternSafe( str )
	return str:gsub( "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1" )
end


--[[
	Function: stripQuotes

	Trims leading and tailing quotes from a string

	Parameters:

		s - The string to strip

	Returns:

		The stripped string
]]
function ULib.stripQuotes( s )
	return s:gsub( "^%s*[\"]*(.-)[\"]*%s*$", "%1" )
end


--[[
	Function: unescapeBackslash

	Converts '\\' to '\'

	Parameters:

		s - The string to convert

	Returns:

		The converted string
]]
function ULib.unescapeBackslash( s )
	return s:gsub( "\\\\", "\\" )
end


--[[
	Function: splitPort

	Parameters:

		ipAndPort - An IP address in the form xxx.xxx.xxx.xxx:xxxx

	Returns:

		The IP as the first return value, the port as the second return value

	Revisions:

		v2.40 - Initial.
]]
function ULib.splitPort( ipAndPort )
	return unpack( ULib.explode( ":", ipAndPort ) )
end

--[[
	Function: splitArgs

	This is similar to string.Explode( " ", str ) except that it will also obey quotation marks.

	Parameters:

		args - The string to split from
		start_token - The string character to start a string with.
		end_token - The string character to end a string with.

	Returns:

		A table containing the individual arguments and a boolean stating whether or not mismatched quotes were found.

	Example:

		:ULib.splitArgs( "This is a \"Cool sentence to\" make \"split up\"" )

		returns...

		:{ "This", "is", "a", "Cool sentence to", "make", "split up" }

	Notes:

		* Mismatched quotes will result in having the last quote grouping the remaining input into
			one argument.
		* Arguments outside of quotes are trimmed (via string.Trim), while what's inside quotes is not
			trimmed at all.

	Revisions:

		v2.10 - Can now handle tabs and trims strings before returning.
		v2.30 - Rewrite. Can now properly handle escaped quotes. New param, ignore_mismatch.
		v2.40 - Rewrite. Much more stable and predictable now. Removed ignore_mismatch param. As
			far as I can tell, it now matches the source engine's split arg behavior exactly. Also
			accepts tokens to consider a string.
]]
function ULib.splitArgs( args, start_token, end_token )
	args = args:Trim()
	local argv = {}
	local curpos = 1 -- Our current position within the string
	local in_quote = false -- Is the text we're currently processing in a quote?
	start_token = start_token or "\""
	end_token = end_token or "\""
	local args_len = args:len()

	while in_quote or curpos <= args_len do
		local quotepos = args:find( in_quote and end_token or start_token, curpos, true )

		-- The string up to the quote, the whole string if no quote was found
		local prefix = args:sub( curpos, (quotepos or 0) - 1 )
		if not in_quote then
			local trimmed = prefix:Trim()
			if trimmed ~= "" then -- Something to be had from this...
				local t = ULib.explode( "%s+", trimmed )
				table.Add( argv, t )
			end
		else
			table.insert( argv, prefix )
		end

		-- If a quote was found, reduce our position and note our state
		if quotepos ~= nil then
			curpos = quotepos + 1
			in_quote = not in_quote
		else -- Otherwise we've processed the whole string now
			break
		end
	end

	return argv, in_quote
end


--[[
	Function: parseKeyValues

	Parses a keyvalue formatted string into a table.

	Parameters:

		str - The string to parse.
		convert - *(Optional, defaults to false)* Setting this to true will convert garry's keyvalues to a better form. This has two effects.
		  First, it will remove the "Out"{} wrapper. Second, it will convert any keys that equate to a number to a number.

	Returns:

		The table, nil and error otherwise. *If you find you're missing information from the table, the file format might be incorrect.*

	Example format:
:test
:{
:	"howdy"   "bbq"
:
:	foo
:	{
:		"bar"   "false"
:	}
:
:}

	Revisions:

		v2.10 - Initial (but tastefully stolen from a GM9 version)
		v2.30 - Rewrite. Much more robust and properly unescapes backslashes now.
		v2.40 - Properly handles escaped quotes now.
]]
function ULib.parseKeyValues( str, convert )
	local lines = ULib.explode( "\r?\n", str )
	local parent_tables = {} -- Traces our way to root
	local current_table = {}
	local is_insert_last_op = false

	for i, line in ipairs( lines ) do
		local tmp_string = string.char( 01, 02, 03 ) -- Replacement
		local tokens = ULib.splitArgs( (line:gsub( "\\\"", tmp_string )) )
		for i, token in ipairs( tokens ) do
			tokens[ i ] = ULib.unescapeBackslash( token ):gsub( tmp_string, "\"" )
		end

		local num_tokens = #tokens

		if num_tokens == 1 then
			local token = tokens[ 1 ]
			if token == "{" then
				local new_table = {}
				if is_insert_last_op then
					current_table[ table.remove( current_table ) ] = new_table
				else
					table.insert( current_table, new_table )
				end
				is_insert_last_op = false
				table.insert( parent_tables, current_table )
				current_table = new_table

			elseif token == "}" then
				is_insert_last_op = false
				current_table = table.remove( parent_tables )
				if current_table == nil then
					return nil, "Mismatched recursive tables on line " .. i
				end

			else
				is_insert_last_op = true
				table.insert( current_table, tokens[ 1 ] )
			end

		elseif num_tokens == 2 then
			is_insert_last_op = false
			if convert and tonumber( tokens[ 1 ] ) then
				tokens[ 1 ] = tonumber( tokens[ 1 ] )
			end

			current_table[ tokens[ 1 ] ] = tokens[ 2 ]

		elseif num_tokens > 2 then
			return nil, "Bad input on line " .. i
		end
	end

	if #parent_tables ~= 0 then
		return nil, "Mismatched recursive tables"
	end

	if convert and table.Count( current_table ) == 1 and istable( current_table.Out ) then -- If we caught a stupid garry-wrapper

		current_table = current_table.Out
	end

	return current_table
end


--[[
	Function: makeKeyValues

	Makes a key values string from a table.

	Parameters:

		t - The table to make the keyvalues from. This can only contain tables, numbers, and strings.
		tab - *Only for internal use*, this helps make inline tables look better.
		completed - A list of table values that have already been parsed, this is *only for internal use* to make sure we don't hit an infinite loop.

	Returns:

		The string, nil and error otherwise.

	Notes:

		If you use numbers as keys in the table, just the values will be used.

	Example table format:
:{ test = { howdy = "bbq", foo = { bar = "false" } } }

	Example return format:
:test
:{
:	"howdy"	  "bbq"
:
:	foo
:	{
:		"bar"	"false"
:	}
:
:}

	Revisions:

		v2.10 - Initial (but tastefully stolen from a GM9 version)
		v2.40 - Increased performance for insanely high table counts.
]]
function ULib.makeKeyValues( t, tab, completed )
	ULib.checkArg( 1, "ULib.makeKeyValues", "table", t )

	tab = tab or ""
	completed = completed or {}
	if completed[ t ] then return "" end -- We've already done this table.
	completed[ t ] = true

	local str = ""

	for k, v in pairs( t ) do
		str = str .. tab
		if !isnumber( k ) then
			str = string.format( "%s%q\t", str, tostring( k ) )
		end

		if istable( v ) then
			str = string.format( "%s\n%s{\n%s%s}\n", str, tab, ULib.makeKeyValues( v, tab .. "\t", completed ), tab )
		elseif isstring( v ) then
			str = string.format( "%s%q\n", str, v )
		else
			str = str .. tostring( v ) .. "\n"
		end
	end

	return str
end


--[[
	Function: toBool

	Converts a bool, nil, string, or number to a bool

	Parameters:

		x - The string or number

	Returns:

		The bool

	Revisions:

		v2.10 - Initial.
		v2.40 - Added ability to convert nils and bools.
]]
function ULib.toBool( x )
	if isbool( x ) then return x end
	if x == nil then return false end

	if tonumber( x ) ~= nil then
		x = math.Round( tonumber( x ) )
		if x == 0 then
			return false
		else
			return true
		end
	end

	x = x:lower()
	if x == "t" or x == "true" or x == "yes" or x == "y" then
		return true
	else
		return false
	end
end


local function navigateUpTo(currentPointer, tableCrumbs)
	for i=1, #tableCrumbs-1 do
		local nextTableName = tableCrumbs[i]
		currentPointer = currentPointer[ nextTableName ]
		if !istable(currentPointer) then return false end -- Not found
	end
	return true, currentPointer
end

local function getCrumbsTable( varLocation )
	local tableCrumbs = ULib.explode( "[%.%[]", varLocation )
	for i=1, #tableCrumbs do
		local newCrumb, replaced = string.gsub( tableCrumbs[i], "]$", "" )
		if replaced > 0 then tableCrumbs[i] = tonumber( newCrumb ) end
	end
	return tableCrumbs
end


--[[
	Function: findVar

	Given a string, find a var starting from the global namespace. This will correctly parse tables. IE, "ULib.serialize".

	Parameters:

		varLocation - The string location of the variable you wish to find or set (E.G., "ULib.myTable.MyVariable").
		rootTable - The optional root table to search. Defaults to _G, the global environment.

	Returns:

	Two values as follows...

		Status - A boolean indicating whether or not it was found.
		Value - The value of the variable.

	Revisions:

		v2.40 - Removed dependency on gmod functions.
		v2.60 - Now returns two values to indicate success and value.
		        Added second parameter for root table and added better handling for nil values.
]]
function ULib.findVar( varLocation, rootTable )
	ULib.checkArg( 1, "ULib.findVar", "string", varLocation )
	ULib.checkArg( 2, "ULib.findVar", {"table", "nil"}, rootTable )
	rootTable = rootTable or _G

	local tableCrumbs = getCrumbsTable( varLocation )
	local success, lastTable = navigateUpTo(rootTable, tableCrumbs)
	if not success then return false end

	local lastCrumb = tableCrumbs[#tableCrumbs]
	return true, lastTable[lastCrumb]
end


--[[
	Function: setVar

	Given a string, find and set a var starting from the global namespace. This will correctly parse tables. IE, "ULib.serialize".

	Parameters:

		varLocation - The string location of the variable you wish to find or set (E.G., "ULib.myTable.MyVariable").
		varValue - The value to set it to.
		rootTable - The optional root table to search. Defaults to _G, the global environment.

	Returns:

	Two values as follows...

		Status - A boolean indicating whether or not it was found and set.
		Value - The PREVIOUS value of the variable.

	Revisions:

		v2.60 - Initial.
]]
function ULib.setVar( varLocation, varValue, rootTable )
	ULib.checkArg( 1, "ULib.setVar", "string", varLocation )
	ULib.checkArg( 3, "ULib.setVar", {"table", "nil"}, rootTable )
	rootTable = rootTable or _G

	local tableCrumbs = getCrumbsTable( varLocation )
	local success, lastTable = navigateUpTo(rootTable, tableCrumbs)
	if not success then return false end

	local lastCrumb = tableCrumbs[#tableCrumbs]
	local prevVal = lastTable[lastCrumb]
	lastTable[lastCrumb] = varValue

	return true, prevVal
end


--[[
	Function: throwBadArg

	Throws an error similar to the lua "bad argument #x to <fn_name> (<type> expected, got <type>).

	Parameters:

		argnum - *(Optional)* The argument number that was bad.
		fnName - *(Optional)* The name of the function being called.
		expected - *(Optional)* The string of the type you expected.
		data - *(Optional)* The actual data you got.
		throwLevel - *(Optional, defaults to 3)* How many levels up to throw the error.

	Returns:

		Never returns, throws an error

	Revisions:

		v2.40 - Initial.
]]
function ULib.throwBadArg( argnum, fnName, expected, data, throwLevel )
	throwLevel = throwLevel or 3

	local str = "bad argument"
	if argnum then
		str = str .. " #" .. tostring( argnum )
	end
	if fnName then
		str = str .. " to " .. fnName
	end
	if expected or data then
		str = str .. " ("
		if expected then
			str = str .. expected .. " expected"
		end
		if expected and data then
			str = str .. ", "
		end
		if data then
			str = str .. "got " .. type( data )
		end
		str = str .. ")"
	end

	error( str, throwLevel )
end


--[[
	Function: checkArg

	Checks to see if an arg matches what is expected, if not, calls throwBadArg().

	Parameters:

		argnum - *(Optional)* The argument number you're.
		fnName - *(Optional)* The name of the function being called.
		expected - The string of the type you expect or a table of types you expect.
		data - The actual data you got.
		throwLevel - *(Optional, defaults to 4)* How many levels up to throw the error.

	Returns:

		Never returns if the data is bad, throws an error. Otherwise returns nil.

	Revisions:

		v2.40 - Initial.
]]
function ULib.checkArg( argnum, fnName, expected, data, throwLevel )
	throwLevel = throwLevel or 4
	if isstring( expected ) then
		if type( data ) == expected then
			return
		else
			return ULib.throwBadArg( argnum, fnName, expected, data, throwLevel )
		end
	else
		if table.HasValue( expected, type( data ) ) then
			return
		else
			return ULib.throwBadArg( argnum, fnName, table.concat( expected, "," ), data, throwLevel )
		end
	end
end


--[[
	Function: isValidSteamID

	Checks to see if a given string is a valid steamid.

	Parameters:

		steamid - The string of the supposed steamid.

	Returns:

		True if it's valid, false if not.

	Revisions:

		v2.40 - Initial.
]]
function ULib.isValidSteamID( steamid )
	return steamid:match( "^STEAM_%d:%d:%d+$" ) ~= nil
end


--[[
	Function: removeCommentHeader

	Removes a comment header.

	Parameters:

		data - The string to remove the comment from.
		comment_char - The comment char.

	Returns:

		Data without the comment header.

	Revisions:

		v2.40 - Initial.
]]
function ULib.removeCommentHeader( data, comment_char )
	comment_char = comment_char or ";"
	local lines = ULib.explode( "\r?\n", data )
	local end_comment_line = 0
	for _, line in ipairs( lines ) do
		local trimmed = line:Trim()
		if trimmed == "" or trimmed:sub( 1, 1 ) == comment_char then
			end_comment_line = end_comment_line + 1
		else
			break
		end
	end

	local not_comment = table.concat( lines, "\n", end_comment_line + 1 )
	return not_comment:Trim()
end


--[[
	Function: stringTimeToMinutes

	Converts a string containing time information to minutes.

	Parameters:

		str - The time string. Defaults to minutes, "h" is for hours, "d" is for days, "w" is for weeks.

	Returns:

		The number of minutes represented by the string or nil if it's unable to parse the string.

	Revisions:

		v2.41 - Initial
		v2.43 - Added year parameter
		v2.60 - Renamed function from "stringTimeToSeconds" to "stringTimeToMinutes", because I am dumb
]]
function ULib.stringTimeToMinutes( str )
	if str == nil or isnumber( str ) then
		return str
	end

	str = str:gsub( " ", "" )
	local minutes = 0
	local keycode_location = str:find( "%a" )
	while keycode_location do
		local keycode = str:sub( keycode_location, keycode_location )
		local num = tonumber( str:sub( 1, keycode_location - 1 ) )
		if not num then
			return nil
		end

		local multiplier
		if keycode == "h" then
			multiplier = 60
		elseif keycode == "d" then
			multiplier = 60 * 24
		elseif keycode == "w" then
			multiplier = 60 * 24 * 7
		elseif keycode == "y" then
			multiplier = 60 * 24 * 365
		else
			return nil
		end

		str = str:sub( keycode_location + 1 )
		keycode_location = str:find( "%a" )
		minutes = minutes + num * multiplier
	end

	local num = 0
	if str ~= "" then
		num = tonumber( str )
	end

	if num == nil then
		return nil
	end

	return minutes + num
end
ULib.stringTimeToSeconds = ULib.stringTimeToMinutes -- Remove in the future


--[[
	Function: secondsToStringTime

	Converts a number of seconds to a string describing the time span.
	Note that it rounds up to the minute level (ten seconds will be one minute).

	Parameters:

		secs - The number of seconds.

	Returns:

		A string representing the length of the span.

	Revisions:

		v2.60 - Initial
]]
function ULib.secondsToStringTime( secs )
	local str = ""
	local mins = math.ceil(secs / 60)

	local minsInYear = 60 * 24 * 365
	if mins >= minsInYear then
		local years = math.floor( mins / minsInYear )
		mins = mins % minsInYear
		str = string.format( "%s%i year%s ", str, years, (years > 1 and "s" or "") )
	end

	local minsInWeek = 60 * 24 * 7
	if mins >= minsInWeek then
		local weeks = math.floor( mins / minsInWeek )
		mins = mins % minsInWeek
		str = string.format( "%s%i week%s ", str, weeks, (weeks > 1 and "s" or "") )
	end

	local minsInDay = 60 * 24
	if mins >= minsInDay then
		local days = math.floor( mins / minsInDay )
		mins = mins % minsInDay
		str = string.format( "%s%i day%s ", str, days, (days > 1 and "s" or "") )
	end

	local minsInHour = 60
	if mins >= minsInHour then
		local hours = math.floor( mins / minsInHour )
		mins = mins % minsInHour
		str = string.format( "%s%i hour%s ", str, hours, (hours > 1 and "s" or "") )
	end

	if mins > 0 then
		str = string.format( "%s%i minute%s ", str, mins, (mins > 1 and "s" or "") )
	end

	return str:Trim()
end


--[[
	Section: Inheritance
]]

--[[
	Function: inheritsFrom

	Creates a psudeo-inheritance for lua. It will search for variables that do
	not exist in derived 'classes' in the parent 'classes', among other things
	explained below.

	Parameters:

		baseClass - The class to derive from. This value *must* either be nil
			or a class created using <inheritsFrom()>.

	Returns:

		The table of the derived class.

	Revisions:

		v2.40 - Initial.

	Notes:

		* Adapted with improvements from a lua-users inheritance tutorial
		<http://lua-users.org/wiki/InheritanceTutorial>.
		* Create using Class:create( ... ) or Class( ... ) (equivalent).
		* Whatever's passed in the '...', above, is passed to
		derived_class:instantiate(). This allows for a 'constructor'.

	See Also:

		* <root_class>
		* <root_class:create>
		* <root_class:class>
		* <root_class:superClass>
		* <root_class:instantiate>
		* <root_class:isa>

	Example:

:b = inherits_from( nil )
:function b:instantiate( ... )
:	print( "base", unpack( arg ) )
:end
:
:d = inherits_from( b )
:function d:instantiate( ... )
:	print( "derived", unpack( arg ) )
:end
:
:b1 = b( "should be base" )
:d1 = d( "should be derived" )
:print( "d1 is d?", d1:isa( d ), "is b?", d1:isa( b ) )
:print( "b1 is d?", b1:isa( d ), "is b?", b1:isa( b ) )

	Output:

:base	 should be base
:derived should be derived
:d1 is d?		 true	 is b?	 true
:b1 is d?		 false	 is b?	 true
]]
function inheritsFrom( base_class )
	local new_class = {}

	-- The meta-table for INSTANCES (IE, created with Class:create())
	local instance_mt = { __index = new_class, class=new_class, base_class=base_class }

	-- The meta-table for the root_class (this will only ever have one table associated with it)
	local class_mt = table.Copy( instance_mt ) -- Only a few differences so copy
	class_mt.__index = base_class or root_class -- Use base or our special meta-base
	class_mt.__call = root_class.call -- Set up call alias
	class_mt.class = new_class -- Set up alias to ourself
	class_mt.instance_mt = instance_mt -- Need this for root_class:create()

	setmetatable( new_class, class_mt )

	return new_class
end


--[[
	Table: root_class

	This is a local table that holds our functions that we want *all* classes
	to have.
]]
root_class = {}


--[[
	Function: root_class:call

	This is a utility function used by the metatable __call to resolve Class( ... ) to Class:create( ... ).

	Parameters:

		parent_table - The table of the caller.
		... - Extra construction parameters, passed to Class:instantiate.

	Returns:

		The 'class instance'.

	Revisions:

		v2.40 - Initial.
]]
function root_class.call( parent_table, ... )
	return parent_table:class():create( ... )
end


--[[
	Function: root_class:create

	This is used to create new 'class instances'.

	Parameters:

		... - Extra construction parameters, passed to Class:instantiate.

	Revisions:

		v2.40 - Initial.
]]
function root_class:create( ... )
	local newinst = {}
	setmetatable( newinst, getmetatable( self ).instance_mt )
	newinst:instantiate( ... ) -- 'Constructor'
	return newinst
end


-- Return the class object of the instance
function root_class:class()
	return getmetatable( self ).class
end


-- Return the super class object of the instance
function root_class:superClass()
	base_class = getmetatable( self ).base_class
	return base_class ~= root_class and base_class or nil -- Nil if root class
end


-- We need to make sure this func exists, but can be overridden
function root_class:instantiate()
end


-- Return true if the caller is an instance of theClass
function root_class:isa( target_class )
	local cur_class = self:class()

	while cur_class and not b_isa do
		if cur_class == target_class then
			return true
		else
			cur_class = cur_class:superClass()
		end
	end

	return false
end

function isClass( obj )
	return istable( obj ) and isfunction( obj.isa ) and obj:isa( root_class )
end


-- This wonderful bit of following code will make sure that no rogue coder can screw us up by changing the value of '_'
_ = nil -- Make sure we're starting out right.
local meta = getmetatable( _G ) or {}
if isbool( meta ) then return end -- Metatable is protected, so we aren't able to run this code without erroring.
local old__newindex = meta.__newindex
setmetatable( _G, meta )
function meta.__newindex( t, k, v )
	if k == "_" then
		-- If you care enough to fix bad scripts uncomment this following line.
		-- error( "attempt to modify global variable '_'", 2 )
		return
	end

	if old__newindex then
		old__newindex( t, k, v )
	else
		rawset( t, k, v )
	end
end
