Module:Table

From IdleOn MMO Wiki

Documentation for this module may be created at Module:Table/doc

local lu = require('Module:LibraryUtil')
local checkType = lu.checkTypeLight

-- Begin Exports
local p = {}

-- Load table library if this module is loaded as "table"
p.concat = table.concat
p.maxn = table.maxn
p.remove = table.remove
p.insert = table.insert
p.sort = table.sort
p.unpack = function(t)
	local mt = getmetatable(t)
	local __m = (mt or {}).__unpack
	
	if __m then
		if type(__m) == 'function' then
			return __m(t)
		else
			return __m
		end
	else
		local _, __ = pcall(unpack, t)
		return _ == false and error(__:gsub('%?', 'unpack'), 2) or unpack(t)
	end
end

p.pack = function(...)
	return { ..., n = select('#', ...) } 
end

---------------------------------------------------------------------------------
-- Local Helper functions
---------------------------------------------------------------------------------
local function isPositiveInteger(v)
	return type(v) == 'number' and v >= 1 and math.floor(v) == v and v < math.huge
end

local function isNaN(v)
	return type(v) == 'number' and tostring(v) == '-nan'
end

local function flattenTable(arr, cb)
	local i = 0
	while arr[i] do
		local v = arr[i]
		
		if type(v) == 'table' then
			flattenTable(v, arr)
		else
			cb(v, i, arr)
		end
	end
end

local function defaultKeySort(a, b)
	return a < b
end

---------------------------------------------------------------------------------
-- function: merge(target: table, ...items: any)
--
-- This function takes each entry in `...items` and adds it to the table.
-- If the item is not a table, it adds it to the table like `push()`.
-- Else, it destructures the table and adds each of its items to the end of `t1`.
---------------------------------------------------------------------------------
function p.merge(target, ...)
	local seen = {}
	checkType('merge', 1, target, 'table')
	
	local function merge(target, merger)
		if type(merger) == 'table' then
			if p.isSequence(merger) then
				p.push(target, unpack(merger))
			else
				for key, value in pairs(merger) do
					local tp = type(value)
					
					if tp == 'table' and not seen[value] then
						seen[value] = 1
						target[key] = target[key] or {}
						merge(target[key], value)
					else
						target[key] = value
					end
				end
			end
		else
			p.push(target, merger)
		end
	end
	
	if select('#', ...) > 1 then
		for i, v in ipairs{ ... } do
			merge(target, v)
		end
	else
		merge(target, ...)
	end
	
	return target
end

---------------------------------------------------------------------------------
-- function: mergeRight(target: table, ...items: any)
--
-- Works like `.merge()` except in reverse.
---------------------------------------------------------------------------------
function p.mergeRight(target, ...)
	local seen = {}
	checkType('mergeRight', 1, target, 'table')
	
	local function merge(target, merger)
		if type(merger) == 'table' then
			if p.isSequence(merger) then
				p.unshift(target, unpack(merger))
			else
				for key, value in pairs(merger) do
					local tp = type(value)
					
					if tp == 'table' and not seen[value] then
						seen[value] = 1
						target[key] = target[key] or {}
						merge(target[key], value)
					else
						target[key] = value
					end
				end
			end
		else
			p.unshift(target, merger)
		end
	end
	
	if select('#', ...) > 1 then
		for i, v in p.reverseIpairs{ ... } do
			merge(target, v)
		end
	else
		merge(target, ...)
	end
	
	return target
end

---------------------------------------------------------------------------------
-- function: mergeNamed(target: table, ...items: any)
--
-- This function takes each entry in `...items` and adds only items with named keys to the table.
-- If the item is not a table, it is ignored.
-- Else, it adds each of its items to the end of `t1`.
---------------------------------------------------------------------------------
function p.mergeNamed(target, ...)
	local seen = {}
	checkType('merge', 1, target, 'table')
	
	local function merge(target, merger)
		if type(merger) == 'table' then
			for key, value in pairs(merger) do
				if type(key) ~= 'number' then
					local tp = type(value)
					
					if tp == 'table' and not seen[value] then
						seen[value] = 1
						target[key] = target[key] or {}
						merge(target[key], value)
					else
						target[key] = value
					end
				end
			end
		end
	end
	
	if select('#', ...) > 1 then
		for i, v in ipairs{ ... } do
			merge(target, v)
		end
	else
		merge(target, ...)
	end
	
	return target
end

---------------------------------------------------------------------------------
-- function: unpackRaw(t: table)
-- 
-- Unpacks a table like `unpack()`, but works with iteration rather than C function.
---------------------------------------------------------------------------------
function p.unpackRaw(t)
	checkType('unpackRaw', 1, t, { 'table' })
	local ret = {}
	local i = 1
	
	while t[i] do
		p.push(ret, t[i])
		i = i+1
	end
	
	return unpack(ret)
end

---------------------------------------------------------------------------------
-- function: length(t: table)
--
-- Gets the length of `t`, as the `#` (length operator) does not work on tables.
-- This function counts all items. To restrict count type, use table.xlength.
----------------------------------------------------------------------------------
function p.length(t)
	checkType('length', 1, t, 'table')
	
	local i = 0
	for k in pairs(t) do
		i = i + 1
	end
	
	return i
end

---------------------------------------------------------------------------------
-- function: xlength(t: table, countNamed?: boolean, countPositional?: boolean)
--
-- Gets the length of `t`, as the `#` (length operator) does not work on tables.
-- Use countNamed and countPositional to restrict which to count.
-- This function runs slower than table.length.
----------------------------------------------------------------------------------
function p.xlength(t, countNamed, countPositional)
	checkType('length', 1, t, 'table')
	checkType('length', 2, countNamed, 'boolean', true)
	checkType('length', 3, countPositional, 'boolean', true)
	
	countNamed = countNamed == nil and true or countNamed
	countPositional = countPositional == nil and true or countPositional
	
	local i = 0
	for k in pairs(t) do
		if type(k) == 'number' then
			if countPositional then
				i = i + 1
			end
		else
			if countNamed then
				i = i + 1
			end
		end
	end
	
	return i
end

---------------------------------------------------------------------------------
-- function: slice(t: table, startIndex: table, endIndex: table)
-- 
-- method returns a shallow copy of a portion of an table into a new table object 
-- selected from start to end (end not included) where start and end represent the index of
-- items in that table. The original table will not be modified.
--
-- ^source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
---------------------------------------------------------------------------------
function p.slice(t, startIndex, endIndex)
	checkType('slice', 1, t, { 'table' })
	checkType('slice', 2, startIndex, { 'number' }, true)
	checkType('slice', 3, endIndex, { 'number' }, true)
	
	if not endIndex and not startIndex then
		return p.from(t)
	end
	local ret, len = {}, #t
	startIndex = startIndex or 1
	endIndex = endIndex or len
	
	if startIndex < 0 then
		startIndex = len+startIndex-1
	end
	if endIndex < 0 then
		endIndex = len+endIndex-1
	end
		
	for i, v in ipairs(t) do
		if i >= startIndex and i <= endIndex then
			p.push(ret, v)
		end
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: setPrototype(t: table, proto: table, indexFunc: function)
--
-- Sets up a metatable prototype with a table.
---------------------------------------------------------------------------------
function p.setPrototype(t, proto, indexFunc, parentProto)
	checkType('setPrototype', 1, t, 'table')
	checkType('setPrototype', 2, proto, 'table')
	checkType('setPrototype', 3, indexFunc, 'function', true)
	checkType('setPrototype', 4, parentProto, 'table', true)
	
	local mt = {}
	mt.__proto = proto or {}
	parentProto = parentProto or {}
	
	if p.length(parentProto) ~= 0 then
		mt.__proto.prototype = parentProto
	end
	
	indexFunc = indexFunc or function(t, k, proto, parentProto)
		if k == 'prototype' then
			return mt.__proto
		else
			return mt.__proto[k] or (p.length(parentProto or {}) ~= 0 and mt.__proto.prototype[k] or nil)
		end
	end
	
	mt.__index = function(t, k)
		return indexFunc(t, k, mt.__proto, mt.__proto.prototype)
	end
	
	return setmetatable(t, mt)
end

---------------------------------------------------------------------------------
-- function: isSequence(t: table)
--
-- Checks if the table is a sequence.
---------------------------------------------------------------------------------
function p.isSequence(t)
	checkType('isSequence', 1, t, 'table')
	
	return p.every(t, function(i) 
		return type(i) == 'number' and i > 0
	end)
end

---------------------------------------------------------------------------------
-- function: push(t: table, ...items: any)
--
-- Appends each of `...items` to the end of `t`.
---------------------------------------------------------------------------------
function p.push(t, ...)
	checkType('push', 1, t, 'table')
	
	for _, v in ipairs{ ... } do
		table.insert(t, v)
	end
	
	return t
end

---------------------------------------------------------------------------------
-- function: makeReadonlyTable(t: table, realTable: t)
--
-- Makes a read-only table where the real table is a dummy table.
---------------------------------------------------------------------------------
function p.makeReadonlyTable(t, metatable)
	checkType('makeReadonlyTable', 1, t, 'table', true)
	checkType('makeReadonlyTable', 2, metatable, 'table', true)
	
	local tmp, mt, readableMt = {}, {}
	
	metatable = metatable or {}
	t = t or {}
	metatable.__index, metatable.__newindex, metatable.__pairs, metatable.__ipairs, metatable.__metatable = nil, nil, nil, nil, nil
	
	for k, v in pairs(metatable) do
		mt[k] = v
	end
	
	mt.__realTable = t
	
	local function __index(t, k)
		return mt.__realTable[k]
	end
	
	local function __newindex()
		return error('table is read-only', 2)
	end
	
	local function __pairs()
		return pairs(mt.__realTable)
	end

	local function __ipairs()
		return ipairs(mt.__realTable)
	end
	
	local function len()
		return #mt.__realTable
	end
	
	mt.__index = __index
	mt.__newindex = __newindex
	mt.__pairs = __pairs
	mt.__ipairs = __ipairs
	mt.__len = len
	mt.__metatable = {}
	
	return setmetatable(tmp, mt)
end

---------------------------------------------------------------------------------
-- function: isStaticClass(t: table)
-- 
-- Checks if a class is static.
---------------------------------------------------------------------------------
function p.isStaticClass(t)
	local mt = getmetatable(t)
	return type(t) == 'table' and mt and mt.__isClass and not mt.__constructorCalled
end

---------------------------------------------------------------------------------
-- function: isClass(t: table)
--
-- Determines if the table is a class.
---------------------------------------------------------------------------------
function p.isClass(t)
	return type(t) == 'table' and (getmetatable(t) or {}).__isClass or false
end

---------------------------------------------------------------------------------
-- Base helper methods for .makeClass()
---------------------------------------------------------------------------------
local baseMethods = {}
local baseStaticMethods = {}

function baseMethods:instanceof(class)
	checkType('instanceof', 1, class, { 'table' })
	assertTrue(p.isClass(class), 'class expected', 2)
	assertTrue(p.isStaticClass(class), 'the class must be static', 2)
	
	if not self.parent then
		return false
	end
	
	local tmp = self
	while tmp.parent do
		if tmp.parent.static == class then
			return true
		else
			tmp = tmp.parent
		end
	end
	
	return false
end

function baseStaticMethods:checkSelf(toCheck)
	if type(toCheck) ~= 'table' or toCheck.static.constructor ~= self.constructor then
		local method = getStackName()
		
		error(string.format(
			'%sinvalid self object. Did you call .%s() with a dot instead of a colon, i.e. ' ..
			'%s%s(...) instead of %s%s(...), or did you call the method directly from the prototype?',
			self.name and self.name .. ': ' or '', method, self.name and self.name .. '.' or '.', method, self.name and self.name .. ':' or ':', method
		 ), 3)
	end
end

function baseStaticMethods:extendPrototype(methods)
	checkType('extendPrototype', 1, methods, { 'table' })
	
	p.merge(self.prototype, methods)
	return self
end

---------------------------------------------------------------------------------
-- function: makeClass(constructor?: function, methods: table, parentClass?: table, options?: table)
--
-- Creates a class table.
---------------------------------------------------------------------------------
function p.makeClass(constructor, methods, parentClass, options)
	checkType('makeClass', 1, constructor, { 'function', 'table', 'nil' })
	checkType('makeClass', 2, methods, { 'table', 'nil' })
	checkType('makeClass', 3, parentClass, { 'table', 'nil' })
	checkType('makeClass', 4, options, 'table', true)
	
	local options = options or {}
	local ignoreTableRet = unpack{
			options.ignoreTableRet or options.ignoreTable or options.ignore,
		}
	local origConstructor = constructor
		
	function tmpFunc(t1)
		return t1
	end
	
	function tmpChildFunc(t1, ...)
		t1.super(...)
		return t1
	end
	
	local get, set = 'get', 'set'
	local methodIsClass = p.isClass(methods)
	local constructorIsClass = p.isClass(constructor)
	local tpConstructor = type(constructor)
	
	if tpConstructor == 'table' and (methodIsClass or (methods == nil and parentClass == nil and p.length(options) == 0)) and not constructorIsClass then
		parentClass = methods
		methods = constructor
		origConstructor, constructor = nil, nil
	elseif constructorIsClass then
		parentClass = constructor
		origConstructor, constructor = nil, nil
	elseif tpConstructor == 'table' and type(methods) == 'table' then
		options = methods
		methods = nil
		parentClass = nil
	elseif tpConstructor == 'function' and methodIsClass then
		parentClass = methods
		methods = nil
	end
	methods = methods ~= nil and methods or {}
	
	if methods.constructor then
		constructor = methods.constructor
	elseif not methods.constructor and type(origConstructor) == 'table' then
		constructor = parentClass and tmpChildFunc or tmpFunc
	end
	
	local util = {}
	local t2 = {}
	local staticReservedNames = p.sequenceToSet{
		'parent',
		'methods',
		'super',
		'__constructorCalled',
		'__isClass',
		'prototype',
		'name',
	}
	
	local reservedNames = p.sequenceToSet{
		'parent',
		'methods',
		'static',
		'__proto__',
		'super',
		'__constructorCalled',
		'__isClass',
		'name',
	}
	
	local allowedTypes = p.sequenceToSet{
		'static',
	}
	
	if constructor == nil then
		constructor = parentClass and tmpChildFunc or tmpFunc
	end
	
	function util:methodType(f, searchType)
		if searchType then
			searchType = searchType:lower()
		end
		
		if type(f) == 'function' then return 'function' end
		if type(f) ~= 'table' then return false end
		if type(f[2]) ~= 'function' then return false end
		
		local f1 = f[1]
		local tpF1 = type(f1)
		
		if tpF1 == 'table' then
			if #f1 == 1 then
				if searchType then
					return f1[1] == searchType
					and searchType
					or false
				else
					return f1[1]
				end
			else
				if searchType then
					return f1[1] == searchType
					and searchType
					or f1[2] == searchType
					and searchType
					or false
				else
					return unpack(f1)
				end
			end
		elseif tpF1 == 'string' and searchType then
			return f1 == searchType
			and searchType 
			or false
		else
			return f1
		end
	end
	
	function util:filterMethods(t, _type, invert)
		local ret = {}
		for k, v in pairs(t) do
			local tmp = util:methodType(v, _type) == _type
			
			local compare
			if invert then
				compare = not tmp
			else
				compare = tmp
			end
			
			if compare then
				ret[k] = v
			end
		end
		return ret
	end
	
	function util:new(...)
		local methods = util:filterMethods(methods, 'static', true)
		
		local mt = {
			methods = methods,
			parent = parentClass,
			static = t2,
			name = options.name,
			__isClass = true,
		}
		
		for k, v in pairs(t2.prototype) do
			methods[k] = v
		end
		
		local t1 = {}
		local metatableMethods = {}
		
		local nonFuncMethods = util:filterMethods(methods, false)
		for k, v in pairs(methods) do
			if not reservedNames[k] then
				t1[k] = nonFuncMethods[k]
			end
		end
			
		if parentClass and (getmetatable(parentClass) or {}).__constructorCalled == true then
			for k, v in pairs(parentClass) do
				if not reservedNames[k] then
					t1[k] = v
				end
			end
		end
		
		mt.__proto__ = mt.methods
		mt.super = function(...)
			if mt.parent == nil then
				error('the class must have a parent when calling the super constructor', 2)
			end
			for k, v in pairs(mt.parent) do
				if not reservedNames[k] then
					t1[k] = v
				end
			end
			
			mt.parent = mt.parent:constructor(...)
			return mt.parent
		end
		
		mt.methods.getmetatable = getmetatable
		p.merge(mt.methods, baseMethods)
		
		mt.__index = function(_, k)
			local value
			
			if reservedNames[k] then
				return mt[k]
			elseif reservedNames[k] and (k == '__isClass' or k == '__constructorCalled') then
				return nil
			end
			
			if parentClass then
				value = mt.methods[k] or (mt.parent.methods or {})[k]
			else
				value = mt.methods[k]
			end
			
			return value
		end
		
		mt.__newindex = function(_, k, v)
			local mType, mType2 = util:methodType(v)
			
			if metatableMethods[k] then
				rawset(mt.methods, k, metatableMethods[k])
				rawset(mt, k, metatableMethods[k])
				return mt.methods[k]
			elseif reservedNames[k] then
				formattedError('class property %q is readonly', 2, k)
			elseif mType == 'static' or mType2 == 'static' then
				return rawset(mt.static, k, v[2])
			else
				return rawset(t1, k, v)
			end
		end
		
		local function methodExists(index)
			local ind = mt.methods[index] or (mt.parent and mt.parent.methods or {})[index]
			if ind then
				return true
			end
		end
		
		metatableMethods.__tostring = function() return t1:__tostring(t1) end
		metatableMethods.__concat = function(a, b) return t1:__concat(a, b) end
		metatableMethods.__call = t1.__call
		metatableMethods.__pairs = function() return t1:__pairs(t1, t1.methods, t1.static) end
		metatableMethods.__ipairs = function() return t1:__ipairs(t1, t1.methods, t1.static) end
		metatableMethods.__add = function(a, b) return t1:__add(a, b) end
		metatableMethods.__sub = function(a, b) return t1:__sub(a, b) end
		metatableMethods.__mul = function(a, b) return t1:__mul(a, b) end
		metatableMethods.__div = function(a, b) return t1:__div(a, b) end
		metatableMethods.__mod = function(a, b) return t1:__mod(a, b) end
		metatableMethods.__pow = function(a, b) return t1:__pow(a, b) end
		metatableMethods.__unm = function(a, b) return t1:__unm(a, b) end
		
		-- Custom metamethods
		metatableMethods.__tonumber = function(a) return t1:__tonumber(a) end
		metatableMethods.__type = function(a, b) return t1:__type(a, b) end
		metatableMethods.__unpack = function(a, b) return t1:__type(a, b) end
		metatableMethods.__toboolean = function(a) return t1:__toboolean(a, b) end
		metatableMethods.__totable = function(a, b, c, d) return t1:__totable(a, b, c, d) end
		metatableMethods.__tconcat = function(a, b, c, d) return t1:__tconcat(a, b, c, d) end
		
		for k, v in pairs(metatableMethods) do
			if methodExists(k) then
				mt[k] = v
			elseif k == '__tostring' and t2.name then
				mt[k] = function(self)
					return '[class ' .. name .. ']'
				end
			end
		end
		
		mt.__metatable = mt 
		setmetatable(t1, mt)
		
		local constructorValue = constructor(t1, ...) 
		
		if t1.parent and p.isStaticClass(t1.parent) then
			error('makeClass(): parent class constructor must be called if the child constructor was called', 2)
		end
		mt.__constructorCalled = true
		
		local ret
		
		if ignoreTableRet or type(constuctorValue) == 'table' then
			ret = constructorValue
		else
			ret = t1
		end
		
		if ret == nil then
			ret = t1
		end
		
		return ret
	end
	
	local metatableMethods, mt = {}, {}
	mt.parent = (parentClass or {}).static
	mt.name = options.name
	mt.methods = util:filterMethods(methods, 'static') or {}
	mt.prototype = p.merge(util:filterMethods(methods, 'static', true), (parentClass or {}).prototype or {}) or {}
	mt.methods.constructor, mt.prototype.constructor = util.new, util.new
	
	p.merge(mt.methods, baseStaticMethods)
	
	mt.__isClass = true
	mt.__constructorCalled = false
	
	mt.__call = util.new
	mt.__index = function(_, k)
		local value
		if mt.parent ~= nil then
			value = mt.methods[k] or mt.parent[k] or (mt.parent.methods or {})[k]
		else
			value = mt.methods[k]
		end
		
		local mType, mType2 = util:methodType(value)
		
		if staticReservedNames[k] then
			return mt[k]
		elseif mType == 'static' or mType == 'static' then
			return value[2]
		else
			return value
		end
	end
	
	mt.__newindex = function(_, k, v)
		local function mType(type)
			return util:methodType(v, type)
		end
		
		if metatableMethods[k] then
			rawset(mt.methods, k, v)
			rawset(mt, k, metatableMethods[k])
		elseif staticReservedNames[k] then
			formattedError('class property %q is readonly', 2, k)
		elseif type(v) == 'function' then
			rawset(mt.methods, k, v)
		elseif util:methodType(v, 'static') then
			rawset(mt, k, v[2])
		else
			rawset(t2, k, v)
		end
		
		return t2[k]
	end
	
	local function methodExists(index)
		local ind = (mt.methods or {})[index] or ((mt.parent or {}).methods or {})[index]
		if ind then
			return true
		end
	end
	
	metatableMethods.__tostring = function() return t2:__tostring(t2) end
	metatableMethods.__concat = function(a, b) return t2:__concat(a, b) end
	metatableMethods.__pairs = function() return t2:__pairs(t2, t2.methods) end
	metatableMethods.__ipairs = function() return t2:__ipairs(t2, t2.methods) end
	metatableMethods.__add = function(a, b) return t2:__add(a, b) end
	metatableMethods.__sub = function(a, b) return t2:__sub(a, b) end
	metatableMethods.__mul = function(a, b) return t2:__mul(a, b) end
	metatableMethods.__div = function(a, b) return t2:__div(a, b) end
	metatableMethods.__mod = function(a, b) return t2:__mod(a, b) end
	metatableMethods.__pow = function(a, b) return t2:__pow(a, b) end
	metatableMethods.__unm = function(a, b) return t2:__unm(a, b) end
	
	-- Custom metamethods
	metatableMethods.__tonumber = function(a) return t2:__tonumber(a) end
	metatableMethods.__type = function(a, b) return t2:__type(a, b) end
	metatableMethods.__unpack = function(a, b) return t2:__type(a, b) end
	metatableMethods.__toboolean = function(a) return t2:__toboolean(a, b) end
	metatableMethods.__totable = function(a, b, c, d) return t2:__totable(a, b, c, d) end
	metatableMethods.__tconcat = function(a, b, c, d) return t2:__tconcat(a, b, c, d) end
	
	for k, v in pairs(metatableMethods) do
		if methodExists(k) then
			mt[k] = v
		end
	end
	
	mt.__metatable = {
		__isClass = true,
		__constructorCalled = false,
	}
	
	return setmetatable(t2, mt)
end
function p.test(...)
	local Class = p.makeClass{ '$', constructor = function() error() end }
	
	Class.prototype.test = function()
		return '$'
	end
	
	Class.prototype.__tostring = function()
		error()
	end
	
	return Class
end

---------------------------------------------------------------------------------
-- function: unshift(t: table, ...items: any)
-- 
-- Prepends each of `...items` to the front of `t`.
---------------------------------------------------------------------------------
function p.unshift(t, ...)
	checkType('unshift', 1, t, 'table')
	
	if select('#', ...) > 1 then
		for _, v in p.reverseIpairs{ ... } do
			table.insert(t, 1, v)
		end
	else
		table.insert(t, 1, ...)
	end
	
	return t
end

---------------------------------------------------------------------------------
-- function: map(t: table, callbackfn: function(v: any, i?: number, t?: table))
--
-- Creates a new table and populates with results from the callback function.
--
---------------[ CALLBACK PARAMETERS ]-------------
-- The callback to `map` has three values passed to it described above.
-- `v`, the value of the table index, `i`, the table index, and `t`, the actual table.
-- `map` will popluate the new table with the return values of this function.
---------------------------------------------------------------------------------
function p.map(t, callbackfn)
	checkType('map', 1, t, 'table')
	checkType('map', 2, callbackfn, 'function')
	
	local ret = {}
	
	for i, v in ipairs(t) do
		ret[#ret+1] = callbackfn(v, i, t) 
	end
	
	return ret
end


---------------------------------------------------------------------------------
-- function: mapNamed(t: table, callback: function(v: any, i?: number, t?: table) => any)
-- 
-- Similar to map, but iterates through a table
-- Creates a new table and populates with results from the callback function.
---------------------------------------------------------------------------------
function p.mapNamed(t, callback)
	checkType('mapWith', 1, t, 'table')
	checkType('mapWith', 2, callback, 'function')
	
	local ret = {}
	
	for k, v in pairs(t) do
		ret[k] = callback(k, v, t)
	end
	
	return ret
end


---------------------------------------------------------------------------------
-- function: mapWith(t: table, callback: function(v: any, i?: number, t?: table) => any)
-- 
-- Maps over a table and sets the corresponding key with the callback's return value.
---------------------------------------------------------------------------------
function p.mapWith(t, callback)
	checkType('mapWith', 1, t, 'table')
	checkType('mapWith', 2, callback, 'function')
	
	for k, v in pairs(t) do
		t[k] = callback(k, v, t)
	end
	
	return t
end

---------------------------------------------------------------------------------
-- function: filter(t: table, callbackfn: function(v: any, i?: number, t?: table))
--
-- Creates a new table with all elements that pass the test implemented by the provided function.
---------------------------------------------------------------------------------
function p.filter(t, callbackfn)
	checkType('filter', 1, t, 'table')
	checkType('filter', 2, callbackfn, 'function')
	
	local ret = {}
	
	for i, v in ipairs(t) do
		if callbackfn(v, i, t) then
			p.push(ret, v)
		end
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: filterWith(t: table, callbackfn: function(v: any, i?: number, t?: table))
--
-- Returns the same table with any values that did not pass the filter function removed.
---------------------------------------------------------------------------------
function p.filterWith(t, callbackfn)
	checkType('filterWith', 1, t, 'table')
	checkType('filterWith', 2, callbackfn, 'function')
	
	local temp = p.from(t)
	p.empty(t)
	
	for i, v in ipairs(temp) do
		if callbackfn(v, i, t) then
			p.push(t, v)
		end
	end
	
	return t
end


---------------------------------------------------------------------------------
-- function: filterNamed(t: table, callbackfn: function(v: any, i?: number, t?: table))
--
-- Creates a new table with all key/value pairs that pass the test implemented by the provided function.
---------------------------------------------------------------------------------
function p.filterNamed(t, callbackfn)
	checkType('filterNamed', 1, t, 'table')
	checkType('filterNamed', 2, callbackfn, 'function')
	
	local ret = {}
	
	for k, v in pairs(t) do
		if callbackfn(k, v, t) then
			ret[k] = v
		end
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: filterWithNamed(t: table, callbackfn: function(v: any, i?: number, t?: table))
--
-- Return the same table with any key/value pairs that did not pass the filter function removed.
---------------------------------------------------------------------------------
function p.filterWithNamed(t, callbackfn)
	checkType('filterWithNamed', 1, t, 'table')
	checkType('filterWithNamed', 2, callbackfn, 'function')
	
	local temp = p.from(t)
	p.empty(t)
	
	for k, v in pairs(temp) do
		if callbackfn(k, v, t) then
			t[k] = v
		end
	end
	
	return t
end

---------------------------------------------------------------------------------
-- function: filterEntriesByKey(t: table, keys: Sequence<any>)
-- 
-- Removes the keys in `keys` from `t`.
---------------------------------------------------------------------------------
function p.filterEntriesByKey(t, keys)
	checkType('filterEntriesByKey', 1, t, 'table')
	checkType('filterEntriesByKey', 2, keys, 'table', true)
	
	return p.filterNamed(t, function(k)
		return not p.includes(keys or {}, k)
	end)
end

---------------------------------------------------------------------------------
-- function: removeLast(t: table)
--
-- Removes the last element of the table and returns it.
---------------------------------------------------------------------------------
function p.removeLast(t)
	checkType('removeLast', 1, t, 'table')
	
	return table.remove(t, p.length(t, false))
end
p.pop = p.removeLast

---------------------------------------------------------------------------------
-- function: removeFirst(t: table)
--
-- Removes the First element of the table and returns it.
---------------------------------------------------------------------------------
function p.removeFirst(t)
	checkType('removeFirst', 1, t, 'table')
	
	return table.remove(t, 1)
end
p.shift = p.removeFirst

---------------------------------------------------------------------------------
-- function: flat(t: table)
-- 
-- Takes a sequence table and flattens all elements down into a single-depth table recursively.
---------------------------------------------------------------------------------
function p.flat(t)
	checkType('flat', 1, t, { 'table' })
	assertTrue(p.isSequence(t), 'bad argument #1 to flat (table is not a sequence)', 2)
	
	local ret = {}
	local seen = {}
	local function _recurse(t)
		for i, v in ipairs(t) do
			if type(v) == 'table' and not seen[v] then
				_recurse(v)
				seen[v] = 1
			else
				p.push(ret, v)
			end
		end
	end
	
	_recurse(t)
	
	return ret
end

---------------------------------------------------------------------------------
-- function: flatMap(t: function, cb: function)
-- 
-- calls flat() on the `t` then calls map() on the flattened table with `cb`.
---------------------------------------------------------------------------------
function p.flatMap(t, cb)
	checkType('flatMap', 1, t, 'table')
	checkType('flatMap', 2, cb, { 'function' })
	
	return p.map(p.flat(t), cb)
end

---------------------------------------------------------------------------------
-- function: Map(items: table)
-- 
-- Creates a Map.
---------------------------------------------------------------------------------
function p.Map(items)
	local ret = {}
	local keys = {}
	local seenkeys = {}
	local Map = {}
	Map.getters = {}
	
	function Map.getters:size()
		return p.length(self, true)
	end
	
	function Map:set(k, v)
		assertFalse(k == nil, 'Map index is nil', 2)
		assertFalse(isNaN(k), 'Map index is NaN', 2)
		
		self[k] = v
		return self
	end
	
	function Map:get(k)
		assertFalse(k == nil, 'Map index is nil', 2)
		assertFalse(isNaN(k), 'Map index is NaN', 2)
		
		self[k] = v
		return self
	end
	
	function Map:remove(k)
		for i = 1, #keys do
			if keys[i] == k then
				table.remove(keys, i)
				break
			end
		end
		self[k] = nil
		
		return self
	end
	
	function Map:clear()
		p.empty(self)
		p.empty(keys)
		
		return self
	end
	
	function Map:forEach(cb)
		checkType('forEach', 1, cb, 'function', true)
		
		for k, v in pairs(self) do
			(cb or mw.log)(k, v, self)
		end
		
		return self
	end
	
	function Map:keys()
		return keys
	end
	
	function Map:values()
		local ret = {}
		
		for _, val in ipairs(keys) do
			p.push(ret, val)
		end
		
		return ret
	end
	
	
	function Map:entries()
		local ret = {}
		for i = 1, #keys do
			ret[i] = { keys[i], self[keys[i]] }
		end
		
		return ret
	end
	
	local function __pairs(t)
		local i, len = 0, #keys
		
		return function()
			i = i+1
			if keys[i] and i <= len then
				return keys[i], ret[keys[i]]
			else
				return nil, nil
			end
		end
	end
	
	local function __ipairs(t)
		local i, len = 0, #keys
		
		return function()
			i = i+1
			if keys[i] and i <= len then
				return i, ret[keys[i]], keys[i]
			else
				return nil, nil, nil
			end
		end
	end
	
	Map.constructor = p.Map
	
	setmetatable(ret, {
		__index = function(t, k)
			if Map.getters[k] then
				return Map.getters[k](t)
			elseif k ~= 'getters' then
				return Map[k]
			end
			
			return nil
		end,
		__newindex = function(t, k, v)
			if seenkeys[k] and not t[k] then
				for i = 1, #keys do
					if keys[i] == k then
						table.remove(keys, i)
						break
					end
				end
			end
			
			rawset(seenkeys, k, 1)
			table.insert(keys, k)
			rawset(t, k, v)
		end,
		__pairs = __pairs,
		__ipairs = __ipairs,
		__tostring = function() return 'map' end
	})
	
	return ret
end

---------------------------------------------------------------------------------
-- function: Set(t)
-- 
-- Turns a table into a set.
---------------------------------------------------------------------------------
function p.Set(t)
	checkType('Set', 1, t, 'table', true)
	t = t or {}
	local ret = {}
	local keys = {}
	local size = 0
	local reservedKeys = {
		['size'] = 1,
	}
	
	for k, v in pairs(t) do
		if type(k) == 'number' then
			if not keys[v] then size = size + 1 end
			keys[v] = 1
		else
			if not keys[k] then size = size + 1 end
			keys[k] = 1
		end
	end
	
	local Set = {}
	
	Set.constructor = p.Set
	
	function Set:has(...)
		if select('#', ...) == 1 then
			local value = ...
			assertFalse(value == nil, 'Set value is nil', 2)
			assertFalse(isNaN(value), 'Set value is NaN', 2)
			
			for v in pairs(keys) do
				if v == value then
					return true
				end
			end
			
			return false
		else
			local has = {}
			for val in forEachArgs({ 'any', required = 1 }, ...) do
				has[val] = self:has(val)
			end
			
			return has
		end
	end
	
	function Set:remove(...)
		local removed
		local len = select('#', ...)
		
		if len == 1 then
			assertFalse(... == nil, 'Set value is nil', 2)
			assertFalse(isNaN(...), 'Set value is NaN', 2)
			
			local has = self:has(...)
			if has then size = size - 1 end
			keys[...] = nil
			
			return has
		else
			removed = {}
			
			for val in forEachArgs({'any', required = 1}, ...) do
				removed[val] = self:remove(val)
			end
		end
	end
	
	function Set:clone()
		return p.Set(keys)
	end
	
	function Set:add(...)
		for val in forEachArgs({'any', required = 1}, ...) do
			if not keys[val] then size = size + 1 end
			keys[val] = 1
		end
		
		return self
	end
		
	function Set:find(callbackfn)
		checkType('find', 1, callbackfn, { 'function' })
		
		for val in self:pairs() do
			if callbackfn(val, self) then
				return val
			end
		end
		
		return nil
	end
	
	function Set:each(callbackfn)
		checkType('each', 1, callbackfn, { 'function' })
		
		for val in pairs(self) do
			callbackfn(val, self)
		end
		
		return self
	end
	
	function Set:map(callbackfn)
		checkType('map', 1, callbackfn, { 'function' })
		
		return self:clone():mapWith(callbackfn)
	end
	
	function Set:mapWith(callbackfn)
		checkType('mapWith', 1, callbackfn, { 'function' })
		
		for val in pairs(self) do
			self:remove(val)
			self:add(callbackfn(val, self))
		end
		
		return self
	end
	
	function Set:pairs()
		return pairs(self)
	end
	
	function Set:ipairs()
		return ipairs(self)
	end
	
	function Set:filter(callbackfn)
		checkType('filter', 1, callbackfn, { 'function' })
		
		return self:clone():filterWith(callbackfn)
	end
	
	function Set:filterWith(callbackfn)
		checkType('filterWith', 1, callbackfn, { 'function' })
		
		for val in pairs(self) do
			if not callbackfn(val, self) then self:remove(val) end
		end
		
		return self
	end
	
	function Set:intersection(otherSet)
		assertTrue(otherSet and otherSet.constructor == p.Set, 'bad argument #1 to \'intersection\' (compare value must be another set)', 2)
		
		local values = {}
		
		for val in pairs(self) do
			if otherSet:has(val) then
				p.push(values, val)
			end
		end
		
		return p.Set(values)
	end
	
	function Set:difference(otherSet)
		assertTrue(otherSet and otherSet.constructor == p.Set, 'bad argument #1 to \'difference\' (compare value must be another set)', 2)
		
		local values = {}
		
		for val in pairs(self) do
			if not otherSet:has(val) then
				p.push(values, val)
			end
		end
		
		return p.Set(values)
	end
	
	function Set:symDifference(otherSet)
		assertTrue(otherSet and otherSet.constructor == p.Set, 'bad argument #1 to \'symDifference\' (compare value must be another set)', 2)
		
		return self:difference(otherSet):union(otherSet:difference(self))
	end
	
	function Set:union(otherSet)
		assertTrue(otherSet and otherSet.constructor == p.Set, 'bad argument #1 to \'union\' (other value must be another set)', 2)
		
		local values = {}
		
		for val in pairs(self) do
			p.push(values, val)
		end
		
		return p.Set(values)
	end
	
	function Set:merge(...) 
		for val, i in forEachArgs({'any', required = 1}, ...) do
			checkType(i, val, 'table')
			
			if p.constructor == Set.constructor then
				for v in pairs(val) do
					self:add(v)
				end
			elseif not p.isSequence(val) then
				for v in pairs(val) do
					self:add(v)
				end
			else
				for _, v in ipairs(val) do
					self:add(v)
				end
			end
		end
	end
	
	function Set:clear()
		keys = {}
		size = 0
		
		return self
	end
	
	function Set:values()
		return p.keys(keys)
	end
	
	return setmetatable(ret, {
		__newindex = function(t, k, v)
			assertFalse(reservedKeys[k], 'Set property %q is read-only', 2, k)
			
			if not t[k] then size = size + 1 end
			return rawset(keys, k, 1)
		end,
		
		__index = function(t, k)
			if k == 'size' then
				return size
			elseif Set[k] then
				return Set[k]
			else
				return t:has(k)
			end
		end,
		
		__pairs = function(t)
			local keys = p.keys(keys)
			local i = 0
			
			return function()
				i = i+1
				if keys[i] and i <= size then
					return keys[i]
				else
					return nil
				end
			end
		end,
		
		__ipairs = function(t)
			local keys = p.keys(keys)
			local i = 0
			
			return function()
				i = i+1
				if keys[i] and i <= size then
					return i, keys[i]
				else
					return nil, nil
				end
			end
		end,
		
		__tostring = function()
			return string.format('Set: { %s }', table.concat(p.map(p.keys(keys), function(k)
				if (type(k) == 'table') then
					return 'table'
				elseif (type(k) == 'function') then
					return 'function'
				elseif (type(k) == 'string') then
					return string.format('"%s"', string.gsub(k, '"', '\\"'))
				else
					return k
				end
			end), ', '))
		end
	})
end

---------------------------------------------------------------------------------
-- function: find(t: table, compare: any)
-- 
-- Checks each result found by `compare` and compares it against `t`. If an index is found,
-- If `compare` is not a function, it compares each value to `compare`.
-- the result is returned along with it's index. If no value is found, it returns nil.
---------------------------------------------------------------------------------
function p.find(...)
	local t, compare = checkArgs({ 'table', { 'any', emptyOk = true } }, ...)
	
	local isFunction = type(compare) == 'function'
	for k, v in pairs(t) do
		if type(compare) == 'function' and compare(v, k, t) or compare == v then
			return v, k
		end
	end
	
	return
end

---------------------------------------------------------------------------------
-- function: findIndex(t: table, compare: any)
-- 
-- Very similar to find(), but returns only the index.
---------------------------------------------------------------------------------
function p.findIndex(...)
	local _, ret = p.find(checkArgs({ 'table', { 'any', emptyOk = true } }, ...))
	
	return ret
end

---------------------------------------------------------------------------------
-- function: splice(t: table, start: number, deleteCount?: number, ...items: any)
-- 
-- Removes/replaces/adds table elements in place. It may do the following functions:
-- *Insert elements at a certain index
-- *Remove a number of a element starting at a sertain index
-- *Replace a number of elements at a certain inxex
-- See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice for more info.
---------------------------------------------------------------------------------
function p.splice(t, start, deleteCount, ...)
	checkType('splice', 1, t, { 'table' })
	checkType('splice', 2, start, { 'number' })
	checkType('splice', 3, deleteCount, { 'number' }, true)
	
	start = (start and start ~= 0) and start or 1
	local ret, len = {}, #t
	count = 0
	
	if - -start < 0 then
		start = len+start+1
	end
	maxCount = - -deleteCount or math.huge
	
	for i = start, len, 1 do
		count = count+1
		if count > maxCount then break end
		
		p.push(ret, table.remove(t, start))
	end
	
	if select('#', ...) > 0 then
		for _, v in forEachArgs({ 'any' }, ...) do
			table.insert(t, start, v)
		end
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: fill(t?: table, v: any, startIndex: number, endIndex?: number)
--
-- Sets the table index starting at `startIndex` with the value of `v` ending at `endIndex`.
-- If end is not specified, it defualts to the length of the table.
---------------------------------------------------------------------------------
function p.fill(t, v, startIndex, endIndex)
	checkType('fill', 1, t, 'table', true)
	checkType('fill', 3, startIndex, 'number')
	checkType('fill', 4, endIndex, 'number', true)
	
	local t = t or {}
	
	for i = - -startIndex, endIndex and - -endIndex or #t, 1 do
		t[i] = v
	end
	
	return t
end

---------------------------------------------------------------------------------
-- function: indexOf(t: table, v: any)
--
-- Searches for `v` the first index in `t`. If nothing is found, it returns `-1`.
---------------------------------------------------------------------------------
function p.indexOf(t, value)
	checkType('indexOf', 1, t, 'table')
	
	local index
	p.some(t, function(k, v) 
		if v == value then
			index = k
			return true 
		else
			return false
		end
	end)
	
	return index or -1
end

---------------------------------------------------------------------------------
-- function: lastIndexOf(t: table, v: any)
--
-- Searches for `v` the first index in `t`. If nothing is found, it returns `-1`.
---------------------------------------------------------------------------------
function p.lastIndexOf(t, v)
	checkType('lastIndexOf', 1, t, 'table')
	
	for i, value in p.reverseIpairs(t) do
		if v == value then return i end 
	end
	return -1
end

---------------------------------------------------------------------------------
-- function: includes(t: table, v: any)
--
-- Checks if `v` is included in any indexes in `t`.
---------------------------------------------------------------------------------
function p.includes(t, v)
	checkType('includes', 1, t, 'table')
	
	return p.some(t, v)
end

---------------------------------------------------------------------------------
-- function: every(t: table, callbackfn: function(k: any, v?: any, t?: table, i?: number))
-- 
-- Tests every element from the return value from `callbackfn`. If any elements fail
-- the test, it returns false.
-- 
---------------[ CALLBACK PARAMETERS ]-------------
-- callbackfn(k: any, v?: any, t?: table, i?: number)
--
-- If the callback returns false, `every()` will consider the test failed and
-- return false.
--	 *`k` is the table key `every()` is currently over.
--	 *`v` is the value of the table key `every()` is currently over.
--	 *`t` is the table `every()` was called on.
--	 *`i` is the number of iterations `every()` has iterated over.
---------------------------------------------------------------------------------
function p.every(t, callbackfn)
	checkType('every', 1, t, 'table')
	assertTrue(callbackfn ~= nil, 'Compare value must not be nil', 2)
	
	local tp = type(callbackfn)
	local i = 0
	for k, v in pairs(t) do
		i = i+1
		if tp == 'function' then
			if not callbackfn(k, v, t, i) then
				return false
			end
		else
			return v == callbackfn
		end
	end
	
	return true
end

---------------------------------------------------------------------------------
-- function: some(t: table, callbackfn: (v: any, i: number, t: table) => boolean|any)
--
-- Tests whether at least one element in the table passes the test implemented 
-- by the provided function. It returns a Boolean value.
----------------------------------------------------------------------------------
function p.some(t, callbackfn)
	checkType('some', 1, t, 'table')
	assertTrue(callbackfn ~= nil, 'Compare value must not be nil', 2)
	
	local tp = type(callbackfn)
	for k, v in pairs(t) do
		if tp == 'function' then
			if callbackfn(k, v, t) then
				return true
			end
		else
			if v == callbackfn then
				return true
			end
		end
	end
	return false
end

---------------------------------------------------------------------------------
-- function: reduce(
--	 t: table, 
--	 callbackfn: (accumlator: any, curVal: any, i?: number, t?: table) => any, 
--	 initVal: any
-- )
-- 
-- Executes a reducer callback function on each element of the table, resulting in single output value.
--
---------------[ CALLBACK PARAMETERS ]-------------
-- callbackfn(accumlator: any, curVal: any, i?: number, t?: table)
--
-- The callback to `every` has four values passed to it described above.
-- `accumlator` is the accumulated value previously returned in the last invocation 
-- of the callback value of to accumalate.
-- `curVal` is the current element being processed in the table.
-- `i` is the current index of the processes element.
-- `t` is the table `reduce()` was called on.
---------------------------------------------------------------------------------
function p.reduce(t, callbackfn, initVal)
	checkType('reduce', 1, t, 'table')
	checkType('reduce', 2, callbackfn, 'function')
	assertTrue(callbackfn('', '', '', '') ~= nil, 'bad argument #1 to reduce (no return value for callback)', 2)
	
	local accumulator
	for i, v in ipairs(t) do
		if i == 1 then
			accumulator = initVal and callbackfn(initVal, v, i, t) or v
		elseif i ~= 1 then
			accumulator = callbackfn(accumulator, v, i, t)
		end
	end
	
	return accumulator
end


---------------------------------------------------------------------------------
-- function: reduceRight(
--	 t: table, 
--	 callbackfn: (accumlator: any, curVal: any, i?: number, t?: table) => any, 
--	 initVal: any
-- )
-- 
-- Executes a reducer callback function on each element of the table from left to right,
-- resulting in single output value. Very similar to `reduce()`.
--
---------------[ CALLBACK PARAMETERS ]-------------
-- callbackfn(accumlator: any, curVal: any, i?: number, t?: table)
--
-- The callback to `every` has four values passed to it described above.
-- `accumlator` is the accumulated value previously returned in the last invocation 
-- of the callback value of to accumalate.
-- `curVal` is the current element being processed in the table.
-- `i` is the current index of the processes element.
-- `t` is the table `reduceRight()` was called on.
---------------------------------------------------------------------------------
function p.reduceRight(t, callbackfn, initVal)
	checkType('reduceRight', 1, t, 'table')
	checkType('reduceRight', 2, callbackfn, 'function')
	assertTrue(callbackfn('', '', '', '') ~= nil, 'bad argument #1 to reduceRight (no return value for callback)', 2)
	
	local accumulator
	for i, v, start in p.reverseIpairs(t) do
		if i == start then
			accumulator = initVal and callbackfn(initVal, v, i, t) or v
		elseif i ~= start then
			accumulator = callbackfn(accumulator, v, i, t)
		end
	end
	return accumulator
end

---------------------------------------------------------------------------------
-- function: keys(t: table)
--
-- Returns a table containing all keys of this table.
---------------------------------------------------------------------------------
function p.keys(t)
	checkType('keys', 1, t, 'table')
	local ret = {}
	
	for k, v in pairs(t) do
		p.push(ret, k)
	end
	return ret
end

---------------------------------------------------------------------------------
-- function: namedKeys(t: table)
--
-- Returns a table containing all named keys of this table.
---------------------------------------------------------------------------------
function p.namedKeys(t)
	checkType('keys', 1, t, 'table')
	local ret = {}
	
	for k, v in pairs(t) do
		if type(k) ~= 'number' then
			p.push(ret, k)
		end
	end
	return ret
end

---------------------------------------------------------------------------------
-- function: values(t: table)
--
-- Returns a table containing all values of this table.
---------------------------------------------------------------------------------
function p.values(t)
	checkType('values', 1, t, 'table')
	local ret = {}
	
	for k, v in p.sortedPairs(t) do
		p.push(ret, v)
	end
	return ret
end

---------------------------------------------------------------------------------
-- function: entries(t: table)
--
-- Returns a table with each subtable containing the tables key in the first value,
-- and the original table's value corresponding to that key.
---------------------------------------------------------------------------------
function p.entries(t)
	checkType('entries', 1, t, 'table')
	local ret = {}
	
	for k, v in p.sortedPairs(t) do
		p.push(ret, { k, v })
	end
	return ret
end

---------------------------------------------------------------------------------
-- function: keySubset(t: table)
--
-- This takes a table and returns an array containing the numbers of any numerical
-- keys that have non-nil values, sorted in numerical order.
--
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
---------------------------------------------------------------------------------
function p.keySubset(t)
	checkType('keySubset', 1, t, 'table')
	
	local nums = {}
	for k, v in pairs(t) do
		if isPositiveInteger(k) then
			nums[#nums + 1] = k
		end
	end
	table.sort(nums)
	
	return nums
end

---------------------------------------------------------------------------------
-- function: reverseIpairs(t: table)
-- 
-- Returns a iterator function to iterate backwards over a sequence table.
-- This works like `ipairs()` except it works backwards, and it provides an additional
-- value in the iteration, `start`. The `start` value is the index the function
-- started iterating at.
--
-----------------[ EXAMPLE ]----------------
-- for i, v, start in table.reverseIpairs(t) do
--	 -- code block
-- end
---------------------------------------------------------------------------------
function p.reverseIpairs(t)
	checkType('reverseIpairs', 1, t, 'table')
	
	local len = p.length(t)
	return function(a, i)
		i = i - 1
		local v = a[i]
		if v ~= nil then
			return i, v, len
		end
	end, t, len+1
end

---------------------------------------------------------------------------------
-- function: reverse(t: table)
-- 
-- Reverses the table in place. The first array element becomes the last, and 
-- the last array element becomes the first.
---------------------------------------------------------------------------------
function p.reverse(t)
	checkType('reverse', 1, t, 'table')
	
	local n = #t
	local i = 1
	
	while i < n do
		t[i], t[n] = t[n], t[i]
		i = i + 1
		n = n - 1
	end
	
	return t
end

---------------------------------------------------------------------------------
-- function: empty(t: table)
-- 
-- Empties the table of all keys.
---------------------------------------------------------------------------------
function p.empty(t)
	checkType('empty', 1, t, 'table')
	
	for k, v in pairs(t) do
		t[k] = nil
	end
		
	return t
end

---------------------------------------------------------------------------------
-- function: isEmpty(t: table)
-- 
-- Checks if a table is completly empty
---------------------------------------------------------------------------------
function p.isEmpty(t)
	checkType('isEmpty', 1, t, 'table')
	
	return next(t) == nil
end

---------------------------------------------------------------------------------
-- function: clean(t: table)
-- 
-- Removes any indexes from the table which are nil, or are not a postive number.
---------------------------------------------------------------------------------
function p.clean(t)
	checkType('clean', 1, t, 'table')
	
	local ret = {}
	local nums = p.keySubset(t)
	
	for _, num in ipairs(nums) do
		ret[#ret+1] = t[num]
	end
	return ret
end

------------------------------------------------------------------------------------
-- function: affixNums(t: table, prefix?: string, suffix?: string)
--
-- This takes a table and returns an array containing the numbers of keys with the
-- specified prefix and suffix. For example, for the table
-- {a1 = 'foo', a3 = 'bar', a6 = 'baz'} and the prefix "a", affixNums will
-- return {1, 3, 6}.
--
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
------------------------------------------------------------------------------------
function p.affixNums(t, prefix, suffix)
	checkType('affixNums', 1, t, 'table')
	checkType('affixNums', 2, prefix, 'string', true)
	checkType('affixNums', 3, suffix, 'string', true)
	
	local function cleanPattern(s)
		-- Cleans a pattern so that the magic characters ()%.[]*+-?^$ are interpreted literally.
		return s:gsub('([%(%)%%%.%[%]%*%+%-%?%^%$])', '%%%1')
	end
	
	prefix = prefix or ''
	suffix = suffix or ''
	prefix = cleanPattern(prefix)
	suffix = cleanPattern(suffix)
	local pattern = table.concat{ '^', prefix, '([1-9]%d*)', suffix, '$' }
	
	local nums = {}
	for k, v in pairs(t) do
		if type(k) == 'string' then
			local num = string.match(k, pattern)
			if num then
				nums[#nums + 1] = tonumber(num)
			end
		end
	end
	table.sort(nums)
	return nums
end


------------------------------------------------------------------------------------
-- function: numData(t: table, compress?: boolean)
--
-- Given a table with keys like ("foo1", "bar1", "foo2", "baz2"), returns a table
-- of subtables in the format 
-- { [1] = {foo = 'text', bar = 'text'}, [2] = {foo = 'text', baz = 'text'} }
-- Keys that don't end with an integer are stored in a subtable named "other".
-- The compress option compresses the table so that it can be iterated over with
-- `ipairs()`.
--
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
------------------------------------------------------------------------------------
function p.numData(t, compress)
	checkType('numData', 1, t, 'table')
	checkType('numData', 2, compress, 'boolean', true)
	
	local ret = {}
	for k, v in pairs(t) do
		local prefix, num = string.match(tostring(k), '^([^0-9]*)([1-9][0-9]*)$')
		if num then
			num = tonumber(num)
			local subtable = ret[num] or {}
			if prefix == '' then
				-- Positional parameters match the blank string; put them at the start of the subtable instead.
				prefix = 1
			end
			subtable[prefix] = v
			ret[num] = subtable
		else
			local subtable = ret.other or {}
			subtable[k] = v
			ret.other = subtable
		end
	end
	if compress then
		local other = ret.other
		ret = p.compressSparseArray(ret)
		ret.other = other
	end
	return ret
end


---------------------------------------------------------------------------------
-- function: compressSparseArray(t: table)
--
-- This takes an array with one or more nil values, and removes the nil values
-- while preserving the order, so that the array can be safely traversed with
-- ipairs.
--
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
---------------------------------------------------------------------------------
function p.compressSparseArray(t)
	checkType('compressSparseArray', 1, t, 'table')
	local ret = {}
	local nums = p.keySubset(t)
	for _, num in ipairs(nums) do
		ret[#ret + 1] = t[num]
	end
	return ret
end


---------------------------------------------------------------------------------
-- function: sparseIpairs(t: table)
--
-- This is an iterator for sparse arrays. It can be used like `ipairs()`, but can
-- handle nil values.
--
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
---------------------------------------------------------------------------------
function p.sparseIpairs(t)
	checkType('sparseIpairs', 1, t, 'table')
	local nums = p.keySubset(t)
	local i = 0
	local lim = #nums
	return function()
		i = i + 1
		if i <= lim then
			local key = nums[i]
			return key, t[key]
		else
			return nil, nil
		end
	end
end


------------------------------------------------------------------------------------
-- function: keysToList(t: table, keySort?: function|boolean, checked?: boolean)
-- 
-- Returns a list of the keys in a table, sorted using either a default
-- comparison function or a custom keySort function.
-- 
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
------------------------------------------------------------------------------------
function p.keysToList(t, keySort, checked)
	if not checked then
		checkType('keysToList', 1, t, 'table')
		checkType('keysToList', 2, keySort, { 'function', 'boolean', 'nil' })
	end
	
	local list = {}
	local index = 1
	for key, value in pairs(t) do
		list[index] = key
		index = index + 1
	end
	
	if keySort ~= false then
		keySort = type(keySort) == 'function' and keySort or defaultKeySort
		
		table.sort(list, keySort)
	end
	
	return list
end

-------------------------------------------------------------------------------
-- function: invert(t: table)
-- 
-- Replaces keys with thier values and vice versa for thier values.
---------------------------------------------------------------------------------
function p.invert(t)
	checkType('invert', 1, t, 'table')
	-- assertTrue(p.isSequence(t), 'bad argument #1 to invert (table is not a sequence)', 2)
	
	local ret = {}
	for k, v in pairs(t) do
		ret[v] = k
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: from(t: table)
-- 
-- Creates a shallow copy of `t`. This means any subtables and functions will be shared.
-- Use `deepCopy()` for a deep copy function.
---------------------------------------------------------------------------------
function p.from(t)
	checkType('from', 1, t, 'table')
	
	local ret = {}
	for k, v in p.sortedPairs(t) do
		ret[k] = v
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: sequenceToSet(t: table)
-- 
-- Creates a shallow copy of `t`. This means any subtables and functions will be shared.
-- Use `deepCopy()` for a deep copy function.
---------------------------------------------------------------------------------
function p.sequenceToSet(t)
	checkType('sequenceToSet', 1, t, 'table')
	assertTrue(p.isSequence(t), 'bad argument #1 to sequenceToSet (table is not a sequence)', 2)
	
	local ret = {}
	p.each(t, function(v)
		ret[v] = true
	end)
	return ret
end
---------------------------------------------------------------------------------
-- function: sortedPairs(t: table, keySort?: boolean)
-- 
-- Iterates through a table, with the keys sorted using the keysToList function.
-- If there are only numerical keys, sparseIpairs is probably more efficient.
-- 
--------------[ ATTRIBUTION ]------------
-- This function was taken from `en.wikipedia.org` in `Module:TableTools`.
---------------------------------------------------------------------------------
function p.sortedPairs(t, keySort)
	checkType('sortedPairs', 1, t, 'table')
	checkType('sortedPairs', 2, keySort, 'function', true)
	
	local list = p.keysToList(t, keySort, true)
	
	local i = 0
	return function()
		i = i + 1
		local key = list[i]
		if key ~= nil then
			return key, t[key]
		else
			return nil, nil
		end
	end
end

---------------------------------------------------------------------------------
-- function: sortedPairsByValue(t: table, keySort?: function|boolean)
-- 
-- Iterates through a table, sorted using either a default comparison
-- function or a custom keySort function on the value
-- Returns a generator
---------------------------------------------------------------------------------
function p.sortedPairsByValue(t, keySort)
	checkType('sortedPairsByValue', 1, t, 'table')
	checkType('sortedPairsByValue', 2, keySort, { 'function', 'boolean', 'nil' })
	
	local function toArray(t)
		local ret = {}
		for k, v in pairs(t) do
			p.push(ret, { key = k, value = v })
		end
		return ret
	end
	
	local list = toArray(p.deepCopy(t, true))
	
	if keySort ~= false then
		keySort = type(keySort) == 'function' and keySort or defaultKeySort
		
		table.sort(list, function(a, b)
			a, b = a.value, b.value
			return keySort(a, b)
		end)
	end
	
	local i = 0
	return function()
		i = i + 1
		local data = list[i]
		if data ~= nil then
			return data.key, data.value
		else
			return nil, nil
		end
	end
end

---------------------------------------------------------------------------------
-- function: toCustomArrayNamed(t: table, customFn?: table|generator)
-- 
-- Iterates through a named table or using a generator, map each values
-- returned by a custom function into a table
-- There is no counterpart for indexed table because that would be the same as p.map
---------------------------------------------------------------------------------
function p.toCustomArrayNamed(t, customFn)
	checkType('toCustomArrayNamed', 1, t, { 'function', 'table' })
	checkType('toCustomArrayNamed', 2, customFn, { 'function', 'nil' })
	
	local ret = {}
	customFn = type(customFn) == 'function' and customFn or function(v, k) return k end
	if type(t) == 'function' then -- t as generator
		for k, v in t do
			p.push(ret, customFn(v, k, t))
		end
	else -- t as table
		for k, v in pairs(t) do
			p.push(ret, customFn(v, k, t))
		end
	end
	
	return ret
end

---------------------------------------------------------------------------------
-- function: recursiveConcat(t: table, sep?: string, i?: number, j?: number)
-- 
-- Takes all values of this table and any subtables then recursively goes through then
-- and adds them to new table, then joins concatnates that table.
---------------------------------------------------------------------------------
function p.recursiveConcat(t, sep, k, l)
	checkType('recursiveConcat', 1, t, 'table')
	checkType('recursiveConcat', 2, sep, 'string', true)
	checkType('recursiveConcat', 3, k, 'number', true)
	checkType('recursiveConcat', 4, l, 'number', true)
	
	return table.concat(p.flat(t), sep or '', k, l)
end

---------------------------------------------------------------------------------
-- function: each(t: table, callbackfn: function(v: any, k?: number, t?: table)
-- 
-- Executes a provided function once for each table element.
---------------------------------------------------------------------------------
function p.each(t, callbackfn)
	checkType('each', 1, t, 'table')
	checkType('each', 2, callbackfn, 'function')
	
	for k, v in ipairs(t) do
		callbackfn(v, k, t)
	end
	return t
end
---------------------------------------------------------------------------------
-- function: eachNamed(t: table, callbackfn: function(v?: any, k: any, t?: table, i?: number)
-- 
-- Executes a provided function once for key-value pair in a table
---------------------------------------------------------------------------------
function p.eachNamed(t, callbackfn)
	checkType('eachNamed', 1, t, 'table')
	checkType('eachNamed', 2, callbackfn, 'function')
	
	local i = 0
	for k, v in pairs(t) do
		i = i+1
		callbackfn(v, k, t, i)
	end
	return t
end


---------------------------------------------------------------------------------
-- function: format(t: table)
-- 
-- Takes the table and takes each value as an argument and puts it through `string.format()`.
---------------------------------------------------------------------------------
function p.format(t)
	checkType('format', 1, t, { 'table', 'number', 'string' }, true)
	
	t = t or ''
	local tp = type(t)
	local mt = getmetatable(t)
	
	if tp == 'string' or tp == 'number' or not (t[1] or ''):match('%%%a') then
		if tp == 'number' or mt and mt.__tostring then
			return tostring(t)
		elseif tp == 'table' then
			return table.concat(t, t.sep or t.s or t.seperator or '')
		elseif t == '' then
			return nil
		else
			return t
		end
	end
	
	p.each(t, function(v, k, t)
		local tp = type(v)
		t[k] = 
			p.includes({'string', 'number', 'boolean'}, tp) 
				and tostring(v) 
			or tp == 'table' 
				and table.concat(v, v.sep) 
			or v
	end)
	
	local success, result = pcall(string.format, unpack(t))
	
	if not success then
		local match = { result:match('bad argument #(%d+) .-%(.- expected, got (.-)%)') }
		
		if not match[1] then
			match = { result:match('bad argument #(%d+) .-%((.-)%)') }
		end
		
		if match[1] then
			error(string.format('invalid value (%s) at index %s in table for \'format\'', match[2] ~= 'no value' and match[2] or 'nil', match[1]), 2)
		else
			error(result, 2)
		end
	end
	
	return result
end

---------------------------------------------------------------------------------
-- function: deepCopy(t: table)
-- 
-- Recursively goes through the table and copies it preserving all identities of 
-- the subtables.
--
----------------[ ATTRIBUTION ]--------------
-- This function was taken from `en.wikipedia.org` from `Module:TableTools`.
---------------------------------------------------------------------------------
local function _deepCopy(orig, includeMetatable, already_seen)
	-- Stores copies of tables indexed by the original table.
	already_seen = already_seen or {}
	
	local copy = already_seen[orig]
	if copy ~= nil then
		return copy
	end
	
	if type(orig) == 'table' then
		copy = {}
		for orig_key, orig_value in pairs(orig) do
			copy[_deepCopy(orig_key, includeMetatable, already_seen)] = _deepCopy(orig_value, includeMetatable, already_seen)
		end
		already_seen[orig] = copy
		
		if includeMetatable then
			local mt = getmetatable(orig)
			if mt ~= nil then
				local mt_copy = _deepCopy(mt, includeMetatable, already_seen)
				setmetatable(copy, mt_copy)
				already_seen[mt] = mt_copy
			end
		end
	else -- number, string, boolean, etc
		copy = orig
	end
	return copy
end

function p.deepCopy(orig, noMetatable, already_seen)
	checkType('deepCopy', 3, already_seen, 'table', true)
	
	return _deepCopy(orig, not noMetatable, already_seen)
end

---------------------------------------------------------------------------------
-- function: dumpObject(t: table)
-- 
-- Takes the table and recursively goes through each and every key, creating
-- a string repersenting the whole table
--
-------------------[ ATTRIBUTION ]--------------
-- This function was taken from the media wiki source file (../includes/engines/LuaCommon/lualib/mw.lua)
---------------------------------------------------------------------------------
function p.dumpObject(object)
	local doneTable = {}
	local doneObj = {}
	local ct = {}
	
	local function sorter(a, b)
		local ta, tb = type(a), type(b)
		if ta ~= tb then
			return ta < tb
		end
		if ta == 'string' or ta == 'number' then
			return a < b
		end
		if ta == 'boolean' then
			return tostring(a) < tostring(b)
		end
		return false -- Incomparable
	end
	
	local function _dumpObject(object, indent, expandTable)
		local tp = type(object)
		if tp == 'number' or tp == 'nil' or tp == 'boolean' then
			return tostring(object)
		elseif tp == 'string' then
			return string.format('%q', object)
		elseif tp == 'table' then
			if not doneObj[object] then
				if type(object) == 'table' then
					ct[tp] = (ct[tp] or 0) + 1
					doneObj[object] = 'table#' .. ct[tp]
				else
					doneObj[object] = tostring(object)
					doneTable[object] = true
				end
			end
			if doneTable[object] or not expandTable then
				return doneObj[object]
			end
			doneTable[object] = true
			
			local ret = { doneObj[object], ' {\n' }
			local mt = getmetatable(object)
			if mt then
				ret[#ret + 1] = string.rep(' ', indent + 2)
				ret[#ret + 1] = 'metatable = '
				ret[#ret + 1] = _dumpObject(mt, indent + 2, false)
				ret[#ret + 1] = '\n'
			end
			
			local doneKeys = {}
			for key, value in ipairs(object) do
				doneKeys[key] = true
				ret[#ret + 1] = string.rep(' ', indent + 2)
				ret[#ret + 1] = _dumpObject(value, indent + 2, true)
				ret[#ret + 1] = ',\n'
			end
			local keys = {}
			for key in pairs(object) do
				if not doneKeys[key] then
					keys[#keys + 1] = key
				end
			end
			table.sort(keys, sorter)
			for i = 1, #keys do
				local key = keys[i]
				ret[#ret + 1] = string.rep(' ', indent + 2)
				ret[#ret + 1] = '['
				ret[#ret + 1] = _dumpObject(key, indent + 3, false)
				ret[#ret + 1] = '] = '
				ret[#ret + 1] = _dumpObject(object[key], indent + 2, true)
				ret[#ret + 1] = ',\n'
			end
			ret[#ret + 1] = string.rep(' ', indent)
			ret[#ret + 1] = '}'
			return table.concat(ret)
		else
			if not doneObj[object] then
				ct[tp] = (ct[tp] or 0) + 1
				doneObj[object] = table.concat{ tostring(object), '#', ct[tp] }
			end
			return doneObj[object]
		end
	end
	return _dumpObject(object, 0, true)
end
p.dump = p.dumpObject
---------------------------------------------------------------------------------
-- function: logObject(t: table)
--
-- Calls `dumpObject()` on the table then logs it using `mw.log()`.
---------------------------------------------------------------------------------
function p.logObject(...)
	local args
	for i, v, _ in forEachArgs({'any'}, ...) do
		if i == 1 then args = _ end
		_[i] = p.dumpObject(v)
	end
	return mw.oldLog(getCodeLocation(), p.unpack(args or {}))
end
p.log = p.logObject

---------------------------------------------------------------------------------
-- function: tableUtil(t?: table)
-- 
-- Takes the table and makes all the methods above availble. It also includes
-- an option to set a metatable to this table.
---------------------------------------------------------------------------------
function p.tableUtil(...)
	local t, metatable = ...
	local len = select('#', ...)
	local doTable
	
	error('TableUtil is deprecated')
	
	if not (((type(t) == 'table' or type(metatable) == 'table') or t == nil and metatable == nil) and len <= 2) then
		t = { ... }
	end
	
	local methods = {}
	local t = t or {}
	
	local mt = {
		__index = function(_, k)
			return methods[k]
		end,
	}
	
	if metatable or getmetatable(t) then
		local metatable = getmetatable(t) or metatable
		if metatable.__index then
			setmetatable(methods, { __index = metatable.__index }) 
			metatable.__index = nil
		end
		
		for k, v in pairs(metatable) do
			mt[k] = v
		end
	end
	
	local keys = {
		rawset = rawset,
		rawget = rawget,
		pairs = pairs,
		ipairs = ipairs,
		getmetatable = getmetatable,
		setmetatable = setmetatable,
		next = next,
		pcall = pcall,
		xpcall = xpcall,
		tostring = tostring,
		tonumber = tonumber,
		type = type,
		unpack = unpack,
		len = function(self, countNamed)
			checkType('len', 1, countNamed, 'boolean', true)
			if countNamed then
				return p.length(self)
			else
				return #self
			end
		end,
		
		select = function(self, ops)
			return select(ops, unpack(self))
		end,
	}
	
	for k, v in pairs(keys) do
		methods[k] = v
	end
	
	for k, v in pairs(p) do
		methods[k] = v
	end
	
	return setmetatable(t, mt)
end


-- Finish Module/Exports
return p