Module:Class

From EarthMC
Jump to navigation Jump to search

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

local p = {}
local helpers = {}
local util = {}
local classMtFuncs = {}
local staticMtFuncs = {}
local protoMtFuncs = {}
local classCount = 0

_G._DEBUG = _G.DEBUG or false
local _DEBUG = _G._DEBUG
local getmetatable, setmetatable, string, table, select, error, ipairs, pairs, tostring, type, rawget, rawset, next, unpack = getmetatable, setmetatable, string, table, select, error, ipairs, pairs, tostring, type, rawget, rawset, next, unpack
local tinsert = table.insert
local sformat = string.format
local debug = debug

-- Currently missing features:
-- Super indexing (`self.super[k]`, `self.super:method()`)
-- Mixins

local MESSAGES = {
	INVALID_SUPER_CALL = "Invalid super constructor call. The class must have a parent to call the super constructor",
	GETTER_ONLY_ASSIGNMENT = 'Cannot set class property %q{property} which has only a getter',
	GETTER_ONLY_STATIC_ASSIGNMENT = 'Cannot set static class property %q{property} which has only a getter',
	STATIC_GETTER_ONLY_ASSIGNMENT = 'Cannot set static class property %q{property} which has only a getter',
	MUST_CALL_SUPER = "Invalid class construction. The child class constructor must call `self:super(...)`",
	INVALID_CLASS_ACCESSOR = "Invalid class %{accessor} %q{property}: class %{accessor}s must be functions, recived a %{type} (%q{value}) instead",
	INVALID_STATIC_CLASS_ACCESSOR = "Invalid static class %{accessor} %q{property}: class %{accessor}s must be functions, recived a %{type} (%q{value}) instead",
	INVAID_SELF_SUPER = ":super() was passed an invalid self object of type %{type} (%q{value}). Did you call :super() with a '.' instead of a ':', i.e `.super(...)` instead of `:super(...)`",
	CLASS_FIELD_RESERVED = "Cannot set class property %q{property} because it is reserved",
	STATIC_CLASS_FIELD_RESERVED = "Cannot set static class property %q{property} because it is reserved",
	PROTO_FIELD_RESERVED = "Cannot set class prototype property %q{property} because it is reserved",
	PROPERTIES_TYPE = 'Class properties must be of type table, recived type %q{type} instead',
	STATIC_PROPERTIES_TYPE = 'Class static properties must be of type table, recived type %q{type} instead',
	INVALID_PARENT_CLASS = "Invalid parent class. The parent class must be a class created by makeClass()",
	VALUE_NOT_CLASS = "bad argument #%{pos} to %q{name} (value of type %{type} (%q{value}) is not a class)",
	VALUE_NOT_INSTANCE = "bad argument #%{pos} to %q{name} (value of type %{type} (%q{value}) is not a class instance)",
	VALUE_NOT_CLASS_OR_INSTANCE = "bad argument #%{pos} to %q{name} (value of type %{type} (%q{value}) is not a class instance or class)",
	VALUE_NOT_CLASS_OR_INSTANCE_OR_PROTO = "Value of type %{type} (%q{value}) is not a class instance or class or a class prototype object",
	INVALID_SELF_OBJECT = "Class method :%{name}() was passed an invalid self object of type %{type} (%q{value}). Did you mean to use ':' instead of '.' to call this method (`:%{name}(...)` instead of `.%{name}(...)`)",
	INVALID_STATIC_SELF_OBJECT = "Static class method :%{name}() was passed an invalid self object of type %{type} (%q{value}). Did you mean to use ':' instead of '.' to call this method (`:%{name}(...)` instead of `.%{name}(...)`)",
	INVALID_PROTO_VALUE = "Cannot reassign the class prototype to a value whose type is not a table, recived a value of type %{type} (%q{value}) instead",
	INVALID_METHOD_ARG_TYPE = "bad argument #%{pos} to class method %q{name} (%{expected} expected, got %{type})",
	VALUE_EXPECTED = "bad argument #%{pos} to class method %q{name} (value expected)",
	INVALID_STATIC_METHOD_ARG_TYPE = "bad argument #%{pos} to static class method %q{name} (%{expected} expected, got %{type})",
	VALUE_EXPECTED_STATIC = "bad argument #%{pos} to static class method %q{name} (value expected)",
	INVAID_ARG_TYPE = "bad argument #%{pos} to %q{name} (%{expected} expected, got %{type})",
	INVALID_CONSTRUCTOR_VALUE = "Cannot set property 'constructor' to a value which is not a function",
}

-- Global list of classes
local classRegistry = setmetatable({}, { __mode="k" })
local instanceRegistry = setmetatable({}, { __mode="k" })
local prototypeRegistry = setmetatable({}, { __mode="k" })
---------------------------------------------------------------------------------
-- Helper functions
---------------------------------------------------------------------------------
-- Pack values
function helpers.pack(...)
	local n = select("#", ...)

	return { n = n, ... };
end

local function noop() end

function helpers.safetostring(v)
	local _, res = xpcall(function()
		return tostring(v)
	end, function()
		if p.isClass(v) then
			return "class"
		elseif p.isInstance(v) then
			return "class instance"
		elseif p.isPrototype(v) then
			return "class prototype"
		else
			return type(v)
		end
	end)

	return res
end

-- interpolate a formatted string, syntax is the same as `string.format(...)` except items are annotated with `%<format options>{<tableKey>}`
-- ex: helpers.interpolate("%q{test}", { test="Test1" }) -> "\"Test1\""
function helpers.interpolate(s, substutions)
	local items = {}
	local i = 0

	s = s:gsub('(%%([%d%.%#+%-]*)(%w?)%b{})', function(w, options, substutionType)
		i = i + 1
		local start, stop = 3 + (#(options .. substutionType)), -2
		tinsert(items, substutions[w:sub(start, stop)] or error("Missing interpolation parameter '" .. w:sub(start, stop) .. "'", 4))

		return "%" .. (substutionType ~= "" and substutionType or "s")
	end)

	return sformat(s, unpack(items, 1, i))
end

-- Bind a `self` value to a function
-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
if _DEBUG then
	function helpers.proxy(t, func)
		return function(...)
			local res = helpers.pack(({ ["<class internals>"] = func })["<class internals>"](t, ...))

			if t then return unpack(res, 1, res.n) end
		end
	end
else
	function helpers.proxy(t, func)
		return function(...)
			return func(t, ...)
		end
	end
end

-- Get table keys
function helpers.tableKeys(t)
	local ret = {}

	for k in pairs(t) do
		tinsert(ret, k)
	end

	return ret
end

-- Merge keys into table
function helpers.merge(t1, ...)
	if select('#', ...) > 1 then
		for _, t in ipairs{ ... } do
			for k, v in pairs(t) do
				t1[k] = v
			end
		end
	else
		for k, v in pairs(...) do
			t1[k] = v
		end
	end

	return t1
end

function helpers.clearTable(t)
	for k in pairs(t) do
		t[k] = nil
	end

	return t
end

function helpers.getMethodName(level)
	local methodName = mw.text.split(debug.traceback(), "\n")[(level or 0) + 3]:match("in function [\'\"](.-)[\'\"]")

	return methodName
end

function helpers.getName(value)
	local s

	if p.isInstance(value) then s = 'instance of class "' .. debug.getNameOf(value) .. '"'
	elseif p.isPrototype(value) then s = 'prototype of class "' .. debug.getNameOf(value) .. '"'
	elseif p.isClass(value) then s = 'class "' .. debug.getNameOf(value) .. '"' 
	else s = type(value) .. ' ("' .. helpers.safetostring(value) .. '")'
	end
	
	return s
end

-- Function to create a child class form a given parent
function helpers.createChildClass(parent, classData)
	helpers.assertStaticSelf(parent)
	classData = classData or {}
	
	if type(classData) == 'string' then
		return function(data)
			data = data or {}
			
			data.parent = parent
			data.__metadata = data.__metadata or {}
			data.__metadata.name = classData
			
			return p.makeClass(data)
		end
	else
		classData.parent = parent
		return p.makeClass(classData)
	end
end
---------------------------------------------------------------------------------
-- Assertion utilities
---------------------------------------------------------------------------------
function helpers.assertIsClass(v, pos)
	if not p.isClass(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_CLASS, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

	return v
end

function helpers.assertIsInstance(v, pos)
	if not p.isInstance(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_INSTANCE, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

	return v
end

function helpers.assertIsInstanceOrClass(v, pos)
	if not p.isInstance(v) and not p.isClass(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_CLASS_OR_INSTANCE, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

	return v
end

function helpers.assertIsInstanceOrClassOrProto(v, pos)
	if not p.isInstance(v) and not p.isClass(v) and not p.isPrototype(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_CLASS_OR_INSTANCE_OR_PROTO, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

	return v
end

function helpers.assertStaticSelf(v)
	if not p.isClass(v) then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(1), value = helpers.safetostring(v), type = type(v) }), 3) end

	return v
end

---------------------------------------------------------------------------------
-- functions used inside of interals
---------------------------------------------------------------------------------
function helpers.defaultConstructor(self)
	return self
end

function helpers.defaultInheritedConstructor(self, ...)
	self:super(...)

	return self
end

function helpers.invalidSuperCall()
	error(MESSAGES.INVALID_SUPER_CALL, 2)
end

function helpers.ipairsFunc(t, i)
	i = i + 1

	local v = t[i]
	if v ~= nil then return i, v end
end

local oldLog = mw.log
---------------------------------------------------------------------------------
-- Overwrite mw.dumpObject to accept classes
---------------------------------------------------------------------------------
function mw.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
			local s = helpers.safetostring(object)

			if not doneObj[object] then
				if s == 'table' or classRegistry[object] or instanceRegistry[object] or prototypeRegistry[object] then
					if classRegistry[object] then s = 'class(' .. object.__metadata__.name .. ')'
					elseif instanceRegistry[object] then s = 'class instance('  .. object.__class__.__metadata__.name .. ')'
					elseif prototypeRegistry[object] then s = 'class prototype(' .. object.__holdingClass__.__metadata__.name .. ')'
					end

					ct[tp] = (ct[tp] or 0) + 1
					doneObj[object] = s .. '#' .. ct[tp]
				else
					doneObj[object] = s
					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

			if p.isClass(object) or p.isPrototype(object) or p.isInstance(object) then
				local proto = object.__proto__

				if proto then
					ret[#ret + 1] = string.rep(" ", indent + 2)
					ret[#ret + 1] = '(Prototype) = '
					ret[#ret + 1] = _dumpObject(object.__proto__, indent + 2, true)
					ret[#ret + 1] = ",\n"
				end
			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] = tostring(object) .. '#' .. ct[tp]
			end
			return doneObj[object]
		end
	end
	return _dumpObject(object, 0, true)
end

function mw.logObject(v, prefix)
	mw.log((prefix and prefix .. " " or "") .. mw.dumpObject(v))
end
p.dump = mw.dumpObject
p.dumpObject = mw.dumpObject
p.log = mw.logObject
p.logObject = mw.logObject

---------------------------------------------------------------------------------
-- Internal Data
---------------------------------------------------------------------------------
local metaProperties = {
	['__index'] = 1,
	['__newindex'] = 1,
	['__mode'] = 1,
	['__tostring'] = 1,
	['__concat'] = 1,
	['__metatable'] = 1,
	['__ipairs'] = 1,
	['__pairs'] = 1,
	['__pow'] = 1,
	['__add'] = 1,
	['__sub'] = 1,
	['__div'] = 1,
	['__mul'] = 1,
	['__unm'] = 1,
	['__eq'] = 1,
	['__lt'] = 1,
	['__le'] = 1,
}

-- TODO: Maybe add custom metamethods?
local customStaticMetaProperties = {
	['__getter'] = 1,
	['__setter'] = 1,
}

local customMetaProperties = {
	['__getter'] = 1,
	['__setter'] = 1,
	['__protoIndex'] = 1,
}

-- Properties that maybe added directly to the metatable without need for further modificiation
local contextSafeProperties = {
	['__tostring'] = 1,
	['__pairs'] = 1,
	['__ipairs'] = 1,
	["__unm"] = 1,
}

-- Relational operators
local relationalOperators = {
	['__add'] = 1,
	['__sub'] = 1,
	['__div'] = 1,
	['__mul'] = 1,
	['__pow'] = 1,
	['__le'] = 1,
	['__lt'] = 1,
	['__concat'] = 1,
}

local reservedProps = {
	["super"] = 1,
	["__proto__"] = 1,
	['__getters__'] = 1,
	['__setters__'] = 1,
	['__parent__'] = 1,
	['__class__'] = 1,
	['__static__'] = 1,
	['__metadata__'] = 1,
}

local reservedProtoProps = {
	['__proto__'] = 1,
	['__holdingClass__'] = 1,
	['__getters__'] = 1,
	['__setters__'] = 1,
	['__parent__'] = 1,
}

local reservedStaticProps = {
	['__parent__'] = 1,
	['__proto__'] = 1,
	['__setters__'] = 1,
	['__getters__'] = 1,
	['__children__'] = 1,
	['__parents__'] = 1,
	['__metadata__'] = 1,
	['constructor'] = 1,
	['super'] = 1,
	['childClass'] = 1,
}

---------------------------------------------------------------------------------
-- Class structure
--
-- Static class
--  * Static metatable
--	* Static Parent metatable
--  * Class prototype
--	* Class prototype metatable
-- * Class instance
--  * Class instance metatable
-- * Parent class (repeat above)
---------------------------------------------------------------------------------
function helpers:overwriteProto(t, isOverwriting)
	if isOverwriting and #helpers.tableKeys(self.__prototype) > 0 then
		helpers.clearTable(self.__prototype)
		helpers.clearTable(self.__classGetters)
		helpers.clearTable(self.__classSetters)
		helpers.clearTable(self.__classMetamethods)
	end

	for k, v in pairs(t) do
		if k ~= 'constructor' then self.__protoMt.__fakeTable[k] = v
		elseif k == 'constructor' then 
			if type(v) == 'function' then
				self.__origConstructor = v
			else
				error(helpers.interpolate(MESSAGES.INVALID_CONSTRUCTOR_VALUE), 2)
			end
		end
	end
end

function helpers.parseAccessorTable(k, v, setters, getters, len, static)
	static = static ~= false

	len = len or #helpers.tableKeys(v)
	local get = v.get
	local set = v.set
	if len > 0 and len <= 2 and (set or get) then
		if set ~= nil then
			if type(set) == "function" then
				setters[k] = set
			else
				return helpers.interpolate(
					static and MESSAGES.INVALID_STATIC_CLASS_ACCESSOR or MESSAGES.INVALID_CLASS_ACCESSOR,
					{ accessor = "setter", property = k, type = type(set), value = set }
				)
			end
		end
		if get ~= nil then
			if type(get) == "function" then
				getters[k] = get
			else
				return helpers.interpolate(
					static and MESSAGES.INVALID_STATIC_CLASS_ACCESSOR or MESSAGES.INVALID_CLASS_ACCESSOR,
					{ accessor = "getter", property = k, type = type(get), value = get }
				)
			end
		end
	end
end

---------------------------------------------------------------------------------
-- Class metatable methods
---------------------------------------------------------------------------------
function classMtFuncs.__index(t, k)
	local self = instanceRegistry[t]
	local staticMt = self.__staticMt

	if k == 'super' then return self.__super end		
	if reservedProps[k] then
		if k == '__getters__' then return staticMt.__classGetters
		elseif k == '__setters__' then return staticMt.__classSetters
		elseif k == '__static__'
		or k == '__class__' then return staticMt.__class 
		elseif k == '__proto__' then return staticMt.__protoMt.__fakeTable
		elseif k == '__parent__' then return staticMt.__parentStaticMt.__class
		end
	end
	
	local __index = staticMt.__classMetamethods.__index
	local protoProp = staticMt.__prototype[k]
	local getter = staticMt.__classGetters[k]

	-- If requested value does not exist in current prototype, repeat same step on parent prototypes
	if staticMt.__hasParent and protoProp == nil and getter == nil then
		local cur = staticMt.__parentStaticMt
		local curGetters = cur.__classGetters
		local curProto = cur.__prototype

		while cur do
			local value = curProto[k]
			local foundGetter = curGetters[k]

			if foundGetter ~= nil then
				getter = foundGetter
				break
			elseif value ~= nil then
				protoProp = value
				break
			end

			if not cur.__parentStaticMt then
				break
			end

			cur = cur.__parentStaticMt
			curProto = cur.__prototype
			curGetters = cur.__classGetters
		end
	end

	if getter ~= nil then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(({ ["<class getter>"] = getter })["<class getter>"](t))

			if getter then return unpack(res, 1, res.n) end
		else
			return getter(t)
		end
	end

	if protoProp ~= nil then
		return protoProp
	else
		if __index then
			-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stack
			if _DEBUG then
				local res = helpers.pack(__index(t, k))
	
				if self then return unpack(res, 1, res.n) end
			else
				return __index(t, k)
			end
		end

		return nil
	end
end

function classMtFuncs.__newindex(t, k, v)
	if reservedProps[k] then error(helpers.interpolate(MESSAGES.CLASS_FIELD_RESERVED, { property = k }), 2) end

	local self = instanceRegistry[t]
	local staticMt = self.__staticMt
	local setter = staticMt.__classSetters[k]
	local getter = staticMt.__classGetters[k]
	local __newindex = staticMt.__classMetamethods.__newindex 

	-- Search for setter on parent classes
	if staticMt.__hasParent and setter == nil then
		local cur = staticMt.__parentStaticMt
		local curSetters = cur.__classSetters
		local curGetters = cur.__classGetters
		local curProto = cur.__prototype

		while cur do
			local foundSetter = curSetters[k]
			getter = curGetters[k]

			if foundSetter ~= nil then
				setter = foundSetter
				break
			end

			curProto = cur.__prototype
			curSetters = cur.__classSetters
			cur = cur.__parentStaticMt
		end
	end
	
	if getter and not setter then
		error(helpers.interpolate(MESSAGES.GETTER_ONLY_ASSIGNMENT, { property = k }), 3)
	elseif setter ~= nil then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(({ ["<class setter>"] = setter })["<class setter>"](t, v))

			if setter then return unpack(res, 1, res.n) end
		else
			return setter(t, v)
		end
	else
		if __newindex then
			-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
			if _DEBUG then
				local res = helpers.pack(__newindex(t, k, v))
	
				if self then return unpack(res, 1, res.n) end
			else
				return __newindex(t, k, v)
			end
		end

		return rawset(t, k, v)
	end
end

-- Override default pairs(), invoke class getters in the process
function classMtFuncs.__pairs(t)
	local self = instanceRegistry[t]
	local staticMt = self.__staticMt
	local getters = helpers.tableKeys(staticMt.__classGetters)
	local i = 0
	local onGetters = false
	local __pairs = staticMt.__classMetamethods.__pairs

	if __pairs then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(__pairs(t, k, v))

			if self then return unpack(res, 1, res.n) end
		else
			return __pairs(t, k, v)
		end
	end

	return function(t, k1)
		local k, v

		if not onGetters then
			k, v = next(t, k1)
		end

		-- Insert getters into results
		if k == nil then
			onGetters = true
			i = i + 1

			k, v = getters[i], t[getters[i]]
		end

		if k == nil then
			return nil, nil
		else
			return k, v
		end
	end, t, nil
end

function classMtFuncs.__ipairs(t)
	local self = instanceRegistry[t]
	local __ipairs = self.__staticMt.__classMetamethods.__ipairs

	if __ipairs then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(__ipairs(t, k, v))

			if self then return unpack(res, 1, res.n) end
		else
			return __ipairs(t, k, v)
		end
	end

	return function(t, i)
		i = i + 1

		local v = t[i]
		if v ~= nil then return i, v end
	end, t, 0
end


function classMtFuncs.__tostring(t)
	local self = instanceRegistry[t]
	local __tostring = self.__staticMt.__classMetamethods.__tostring

	if __tostring then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(__tostring(t, k, v))

			if self then return unpack(res, 1, res.n) end
		else
			return __tostring(t, k, v)
		end
	end


	return "class instance"
end

---------------------------------------------------------------------------------
-- Static class metamethods
---------------------------------------------------------------------------------
-- Construct class metadatatable
function helpers.createClassMt(staticMt)
	local classMt = {
		__staticMt = staticMt,
	}

	classMt.__metatable = { "instance metatable" }

	-- Set metamethods
	for k, v in pairs(classMtFuncs) do
		classMt[k] = v
	end

	return classMt
end

-- Called when actually constructing the class
function staticMtFuncs.__call(t, ...)
	local created = {}
	local self = classRegistry[t] or (instanceRegistry[t] and instanceRegistry[t].__staticMt) or error(helpers.interpolate(MESSAGES.INVALID_SELF_OBJECT, { name = helpers.getMethodName(), value = helpers.safetostring(t), type = type(t) }), 2)
	local classMt = helpers.createClassMt(self)
	local superCalled = false
	local done = false

	setmetatable(created, classMt)

	instanceRegistry[created] = classMt

	-- Set class properties
	for k, v in pairs(self.__initProps) do
		rawset(created, k, v)
	end

	-- set class metamethods
	for k, v in pairs(self.__classMetamethods) do
		if contextSafeProperties[k] or k == '__eq' or k == '__metatable' then
			classMt[k] = v
		elseif relationalOperators[k] then
			classMt[k] = v
		end
	end

	local i = 0
	local len = #self.__parents

	classMt.__constructorArgs = helpers.pack(...)

	-- TODO: Add super indexing support
	if self.__parentStaticMt then
		local curParent = self.__parents[1]

		local function superFunc(...)
			i = i + 1
			local passedSelf = ({ ... })[1]
			if not instanceRegistry[passedSelf] then error(MESSAGES.INVAID_SELF_SUPER, 2) end

			curParent = classRegistry[self.__parents[i]] or error(MESSAGES.INVALID_SUPER_CALL, 2)
			local constructor = curParent.__origConstructor
			
			local res = helpers.pack(constructor(...));
			
			if (res.n == 0 or res.n == 1) and res[1] ~= passedSelf then
				return passedSelf
			elseif res.n > 1 then
				if res[1] == nil then res[1] = passedSelf end
				
				return unpack(res, 1, res.n)
			else
				return passedSelf
			end
		end

		-- .super() is returned via `__index`
		classMt.__super = superFunc
	else
		classMt.__super = helpers.invalidSuperCall
	end
	local constructor = self.__origConstructor
	assert(constructor ~= staticMtFuncs.__call)
	local ret = helpers.pack(constructor(created, ...))

	if self.__parentStaticMt and i ~= len then
		error(MESSAGES.MUST_CALL_SUPER, 3);
	end

	if (ret.n == 0 or ret.n == 1) and ret[1] ~= created then
		return created
	elseif ret.n > 1 then
		if ret[1] ~= created then ret[1] = created end
		
		return unpack(ret, 1, ret.n)
	else
		return created
	end
end

function staticMtFuncs.__newindex(t, k, v)
	local self = classRegistry[t]
	if reservedStaticProps[k] then error(helpers.interpolate(MESSAGES.STATIC_CLASS_FIELD_RESERVED, { property = k }), 2) end

	-- If key is to Overwrite the prototype, clear any residual prototype keys and reset from new table
	if k == 'prototype' then
		if type(v) == 'table' then
			helpers.overwriteProto(self, v, true)

			return self.__protoMt.__fakeTable
		else
			error(helpers.interpolate(MESSAGES.INVALID_PROTO_VALUE, { type = type(v), value = helpers.safetostring(v) }), 2)
		end
	end

	local setter = self.__staticSetters[k]
	local __newindex = self.__staticMetamethods.__newindex

	-- Search for setter in parent classes
	if self.__hasParent and setter == nil then
		local cur = self.__parentStaticMt
		local curSetters = cur.__staticSetters
		local curGetters = cur.__staticGetters
		local curProto = cur.__prototype

		while cur do
			local foundSetter = curSetters[k]
			local foundGetter = curGetters[k]

			if foundSetter ~= nil or foundGetter ~= nil then
				setter = foundSetter
				getter = foundGetter
				break
			end

			cur = cur.__parentStaticMt

			if not cur then break end
			curProto = cur.__prototype
			curSetters = cur.__staticSetters
		end
	end

	-- If getter but not setter exists raise exception
	if getter and not setter then
		error(helpers.interpolate(MESSAGES.GETTER_ONLY_STATIC_ASSIGNMENT, { property = k }) , 3)
	elseif setter ~= nil then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(({ ["<static class setter>"] = setter })["<static class setter>"](t, v))
	
			if setter then return unpack(res, 1, res.n) end
		else
			return setter(t, v)
		end
	else
		if __newindex then
			-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
			if _DEBUG then
				local res = helpers.pack(__newindex(t))	
				
				if self then return unpack(res, 1, res.n) end
			else
				return __newindex(t)
			end
		end

		return rawset(t, k, v)
	end
end

function staticMtFuncs.__index(t, k)
	if util[k] then return util[k] end

	local self = classRegistry[t]

	if self.__indexDefaults[k] then return self.__indexDefaults[k] end

	local getter = self.__staticGetters[k]
	local value = rawget(t, k)
	local __index = self.__staticMetamethods.__index

	-- Search for getter/prototype property on parents
	if self.__hasParent and getter == nil and value == nil then
		local cur = self.__parentStaticMt
		local curGetters = cur.__staticGetters
		local curProto = cur.__prototype

		while cur do
			local foundGetter = curGetters[k]
			local foundValue = rawget(cur.__class, k)
			
			if foundValue ~= nil then
				value = foundValue
				break
			elseif foundGetter ~= nil then
				getter = foundGetter
				break
			end

			cur = cur.__parentStaticMt

			if not cur then break end
			curProto = cur.__prototype
			curGetters = cur.__staticGetters
		end
	end

	if getter ~= nil then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(({ ["<static class getter>"] = getter })["<static class getter>"](t))
	
			if getter then return unpack(res, 1, res.n) end
		else
			return getter(t)
		end
	else
		if __index then
			-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
			if _DEBUG then
				local res = helpers.pack(__index(t))	
				
				if self then return unpack(res, 1, res.n) end
			else
				return __index(t)
			end
		end

		return value
	end
end

function staticMtFuncs.__pairs(t)
	local self = classRegistry[t]
	local __pairs = self.__staticMetamethods.__pairs

	if __pairs then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(__pairs(t))	
			
			if self then return unpack(res, 1, res.n) end
		else
			return __pairs(t)
		end
	end

	local getters = helpers.tableKeys(self.__staticGetters)
	local i = 0
	local onGetters = false
	local protoDone = false

	return function(t, k1)
		local k, v

		-- Insert prototype into results
		if k1 == 'prototype' and not protoDone then
			protoDone = true
			return 'prototype', self.__protoMt.__fakeTable
		end

		if not onGetters then
			k, v = next(t, k1 ~= 'prototype' and k1 or nil)
		end

		-- Insert getters into results
		if k == nil then
			onGetters = true
			i = i + 1

			k, v = getters[i], t[getters[i]]
		end

		if k == nil then
			return nil, nil
		else
			return k, v
		end
	end, t, 'prototype'
end


function staticMtFuncs.__ipairs(t)
	local self = classRegistry[t]
	local __ipairs = self.__staticMetamethods.__ipairs

	if __ipairs then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(__ipairs(t))	
			
			if self then return unpack(res, 1, res.n) end
		else
			return __ipairs(t)
		end
	end

	return helpers.ipairsFunc, t, 0
end

function staticMtFuncs.__tostring(t)
	local self = classRegistry[t]
	local __tostring = self.__staticMetamethods.__tostring

	if __tostring then
		-- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks
		if _DEBUG then
			local res = helpers.pack(__tostring(t))	
			
			if self then return unpack(res, 1, res.n) end
		else
			return __tostring(t)
		end
	end

	return "class"
end

---------------------------------------------------------------------------------
-- Prototype metamethods
---------------------------------------------------------------------------------
function protoMtFuncs.__pairs(t)
	local self = prototypeRegistry[t]

	return function(t, k)
		local nextKey, value = next(self.__prototype, k);

		return nextKey, value
	end, self.__fakeTable, nil
end

function protoMtFuncs.__ipairs(t)
	return helpers.ipairsFunc, prototypeRegistry[t], 0
end

function protoMtFuncs.__newindex(t, k, v)
	local self = prototypeRegistry[t]
	local tp = type(v)

	if reservedProtoProps[k] then error(helpers.interpolate(MESSAGES.PROTO_FIELD_RESERVED, { property = k })) end
	if k == 'constructor' and tp == 'function' then
		-- Overwrite constructor
		local oldConstructor = self.__staticMt.__origConstructor
		local newConstructor = v

		self.__staticMt.__origConstructor = newConstructor or oldConstructor

		return self.__fakeTable
	elseif k == 'constructor' and tp ~= 'function' then
		error(helpers.interpolate(MESSAGES.INVALID_CONSTRUCTOR_VALUE))
	elseif tp == 'table' and next(v) ~= nil and k ~= '__metatable' then
		local err = helpers.parseAccessorTable(k, v, self.__staticMt.__classSetters, self.__staticMt.__classGetters, nil, false)

		if err then error(err, 2) end
		return
	else
		if metaProperties[k] then
			self.__staticMt.__classMetamethods[k] = v
		end
	end
	self.__prototype[k] = v
	
	return self.__fakeTable
end

function protoMtFuncs.__index(t, k)
	local self = prototypeRegistry[t]
	if self.__indexDefaults[k] then return self.__indexDefaults[k] end

	local value = self.__prototype[k]
	
	if value == nil then
		local parent = self.__parentMt
	
		while parent do
			local foundValue = parent.__prototype[k]

			if foundValue ~= nil then
				value = foundValue
				break 
			end
						
			parent = parent.__parentMt
		end
	end
	
	return value
end

function protoMtFuncs.__tostring()
	return "class prototype"
end

---------------------------------------------------------------------------------
-- Utilies, exposed on the static class
---------------------------------------------------------------------------------
function util:checkSelf(value, name)
	if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end 

	if not self:instanceof(value) then
		local methodName = name or helpers.getMethodName(1)

		error(helpers.interpolate(MESSAGES.INVALID_SELF_OBJECT, { name = methodName, value = helpers.safetostring(value), type = type(value) }), 3)
	end

	return self
end

function util:checkSelfStatic(value, name)
	if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

	if not self:isChildOrEqual(value) then
		local methodName = name or mw.text.split(debug.traceback(), "\n")[3]:match("in function [\'\"](.-)[\'\"]")

		error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = methodName, value = helpers.safetostring(value), type = type(value) }), 3)
	end

	return self
end

function util:isChildOrEqual(value)
	if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

	local cr = classRegistry[value]
	if not cr then return false end
	if value == self then return true end	
	
	local i = 1
	local parents = cr.__parents
	local class = parents[i]

	while class do
		if class == self then return true end

		i = i + 1
		class = parents[i]
	end
	
	return false
end

-- Check if value is an instance of class
function util:instanceof(value)
	if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end
	if not instanceRegistry[value] then return false end

	local class = value.__class__
	local parents = classRegistry[class].__parents

	if class == self then return true end

	local i = 1
	local parent = parents[i]

	while parent do
		if parent == self then return true end

		i = i + 1
		parent = parents[i]
	end

	return false
end

-- Check types of method arguments
function util:checkTypes(types, ...)
	if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

	local max = #types
	local values = helpers.pack(...)
	local i = 1
	local value = values[1]
	-- Rename error function to get proper name in stack
	local checkTypes = error

	while i <= max do
		local valueType = type(value)
		local targetType = types[i]

		if i > values.n then 
			checkTypes(helpers.interpolate(MESSAGES.INVALID_METHOD_ARG_TYPE, { name = helpers.getMethodName(1), type = "no value", pos = i, expected = helpers.getName(targetType) }), 3)
		end

		if p.isClass(targetType) then
			if not targetType:instanceof(value) then
				checkTypes(helpers.interpolate(MESSAGES.INVALID_METHOD_ARG_TYPE, { 
					name = helpers.getMethodName(1), 
					pos = i, 
					value = helpers.safetostring(value), 
					type = valueType, 
					expected = helpers.getName(targetType) 
				}), 3)
			end
		else
			if valueType ~= targetType then
				checkTypes(helpers.interpolate(MESSAGES.INVALID_METHOD_ARG_TYPE, { 
					name = helpers.getMethodName(1),
					pos = i,
					value = helpers.safetostring(value),
					type = helpers.getName(value),
					expected = targetType 
				}), 3)
			end
		end

		i = i + 1
		value = values[i]
	end

	return ...
end

---------------------------------------------------------------------------------
-- Package exports
---------------------------------------------------------------------------------
function p.isClass(v)
	return classRegistry[v] ~= nil
end

function p.isInstance(v)
	return instanceRegistry[v] ~= nil
end

function p.isPrototype(v)
	return prototypeRegistry[v] ~= nil
end

function p.makeClass(...)
	local args = { ... }
	local name, data

	if type(args[1]) == 'string' then
		data = args[2]
		name = args[1]
	else
		data = args[1]
	end

	data = data or {}

	if type(data) ~= 'table' then error(string.format('Argument #1 must be a table or a string (class name) or nil, recived %q instead', type(data)), 2) end
	if type(data) ~= 'table' then error(string.format('Argument #2 must be a table or nil, recived %q instead', type(data)), 2) end

	local parentClass = data.parentClass or data.parent
	local constructor = data.constructor or (parentClass and helpers.defaultInheritedConstructor or helpers.defaultConstructor)
	local staticData = data.static or {}
	local metadata = data.__metadata or {}
	local classFields = data.class or {}

	if type(staticData) ~= 'table' then error(helpers.interpolate(MESSAGES.STATIC_PROPERTIES_TYPE, { type = type(staticData) }), 2) end
	if type(classFields) ~= 'table' then error(helpers.interpolate(MESSAGES.PROPERTIES_TYPE, { type = type(classFields) }), 2) end

	local staticMt = {}

	local Class = {}
	local prototype = data.prototype or {}
	local initProps = {}

	local setters, getters = {}, {}

	classCount = classCount + 1

	if not classRegistry[parentClass] and parentClass ~= nil then
		error(MESSAGES.INVALID_PARENT_CLASS, 2)
	end

	-- Setup metadata
	metadata.name = name or metadata.name or "unnamed class " .. classCount
	metadata.id = classCount

	---------------------------------------------------------------------------------
	-- Set up static metatable
	---------------------------------------------------------------------------------
	helpers.merge(staticMt, {
		__prototype = prototype,
		__class = Class,
		__initProps = initProps,
		__origConstructor = constructor,

		__classSetters = setters,
		__classGetters = getters,
		__staticSetters = {},
		__staticGetters = {},
		__staticMetamethods = {},

		__parentClass = parentClass,
		__parentStaticMt = parentClass and classRegistry[parentClass],

		__isInstance = false,
		__hasParent = not not parentClass,
		__classMetamethods = {},
		__parents = setmetatable({}, { __mode = "v" }),
		__childClasses = setmetatable({}, { __mode = "v" }),
	});

	helpers.merge(staticMt, staticMtFuncs)

	---------------------------------------------------------------------------------
	-- Set static/class methods
	---------------------------------------------------------------------------------
	for k, v in pairs(data) do
		if k ~= 'constructor'
		and k ~= 'parent'
		and k ~= 'class'
		and k ~= 'prototype'
		and k ~= '__metadata'
		and k ~= 'static' then
			local tp = type(v)
			if tp == 'function' then
				rawset(prototype, k, v)
				if metaProperties[k] then
					staticMt.__classMetamethods[k] = v
				end
			elseif tp == 'table' then
				local len = #helpers.tableKeys(v)

				if (len <= 2 and len > 0) and (v.set or v.get) then
					local err = helpers.parseAccessorTable(k, v, staticMt.__classSetters, staticMt.__classGetters, len, false)
					if err then error(err, 2) end
				else
					rawset(initProps, k, v)
				end
			else
				rawset(initProps, k, v)
			end
		end
	end

	if next(classFields) ~= nil then
		for k, v in pairs(classFields) do
			initProps[k] = v
		end
	end

	if next(staticData) ~= nil then
		for k, v in pairs(staticData) do
			if reservedStaticProps[k] then error(helpers.interpolate(MESSAGES.STATIC_CLASS_FIELD_RESERVED, { property = k }), 2) end
			local tp = type(v)

			if tp == 'table' then
				local len = #helpers.tableKeys(v)
				if (len > 0 and len <= 2) and (v.get or v.set) then
					local err = helpers.parseAccessorTable(k, v, staticMt.__staticSetters, staticMt.__staticGetters, len, true)
					if err then error(err, 2) end
				else
					rawset(Class, k, v)
				end
			else
				rawset(Class, k, v)
			end

			if metaProperties[k] and tp == 'function' then
				if contextSafeProperties[k] or relationalOperators[k] or k == '__eq' or k == '__metatable' then
					staticMt[k] = v
				end

				staticMt.__staticMetamethods[k] = v
			-- TODO: Add metatable property verification
			-- elseif metaProperties[k] then
			-- 	error("Metatable properties must be functions.")
			end
		end
	end

	---------------------------------------------------------------------------------
	-- Set up prototype metatable
	---------------------------------------------------------------------------------
	local protoMt = {}
	local fakeProto = {}

	helpers.merge(protoMt, {
		__staticMt = staticMt,
		__class = Class,
		__prototype = staticMt.__prototype,
		__parentMt = staticMt.__hasParent and staticMt.__parentStaticMt.__protoMt or nil,
		__fakeTable = fakeProto,
		__indexDefaults = setmetatable({
			['constructor'] = staticMtFuncs.__call,
			['__setters__'] = staticMt.__classSetters,
			['__getters__'] = staticMt.__classGetters,
			['__proto__'] = staticMt.__hasParent and staticMt.__parentStaticMt.__protoMt.__fakeTable or nil,
			['__holdingClass__'] = staticMt.__class,
			['__parent__'] = staticMt.__hasParent and staticMt.__parentStaticMt.__class,
		}, { __mode = "v" }),
	});

	staticMt.__protoMt = protoMt
	staticMt.__metatable = {
		__isClass = true,
		__isInstance = false,
		__hasParent = staticMt.__hasParent,
		__class = Class,
		__parent = staticMt.__parentClass,
	}
	helpers.overwriteProto(staticMt, prototype, false)

	---------------------------------------------------------------------------------
	-- Set up final metadata for static metatable
	---------------------------------------------------------------------------------
	staticMt.__customMetadata = metadata
	staticMt.__indexDefaults = setmetatable({
		['__parent__'] = staticMt.__parentClass,
		['__proto__'] = staticMt.__parentClass,
		['prototype'] = protoMt.__fakeTable,
		['__setters__'] = staticMt.__staticSetters,
		['__getters__'] = staticMt.__staticGetters,
		['__children__'] = staticMt.__childClasses,
		['__parents__'] = staticMt.__parents,
		['__metadata__'] = staticMt.__customMetadata,
		['constructor'] = staticMtFuncs.__call,
		
		-- Utility methods
		['childClass'] = helpers.createChildClass,
	}, { __mode = "v" })

	-- Set prototype table metamethods
	for k, v in pairs(protoMtFuncs) do
		protoMt[k] = v
	end
	prototypeRegistry[fakeProto] = protoMt

	-- Merge parent initProps into target class and add child class to parent class list
	if parentClass then
		local parentMt = staticMt.__parentStaticMt

		table.insert(classRegistry[parentClass].__childClasses, Class)

		while parentMt do
			tinsert(staticMt.__parents, parentMt.__class)
			helpers.merge(initProps, staticMt.__initProps)

			parentMt = parentMt.__parentStaticMt
		end
	end

	-- Finalize setup
	setmetatable(fakeProto, protoMt)

	classRegistry[Class] = staticMt

	setmetatable(Class, staticMt)
	
	return Class
end
---------------------------------------------------------------------------------
-- Debug functions (Only use for debugging!)
---------------------------------------------------------------------------------
---------------------------------------------------------------------------------
-- Class instance debug functions
---------------------------------------------------------------------------------
function debug.instanceExists(value)
	return not not instanceRegistry[helpers.assertIsInstance(value)]
end

function debug.getInstanceMetatable(instance)
	return instanceRegistry[helpers.assertIsInstance(instance)]
end

function debug.hasInstanceMetamethod(instance, method)
	return not not debug.getInstanceMetamethod(helpers.assertIsInstance(instance), method)
end

function debug.getInstanceMetamethod(instance, method)
	return debug.getInstanceMetatable(helpers.assertIsInstance(instance)).__staticMt.__classMetamethods[method]
end

function debug.getInstanceSetters(instance)
	return helpers.assertIsInstance(instance).__setters__
end

function debug.getInstanceGetters(instance)
	return helpers.assertIsInstance(instance).__getters__
end

---------------------------------------------------------------------------------
-- Class debug functions
---------------------------------------------------------------------------------
function debug.classExists(value)
	return not not classRegistry[helpers.assertIsClass(value)]
end

function debug.getClassMetatable(class)
	return classRegistry[helpers.assertIsClass(class)]
end

function debug.hasClassMetamethod(class, method)
	return not not debug.getClassMetamethod(helpers.assertIsClass(class), method)
end

function debug.getClassMetamethod(class, method)
	return debug.getClassMetatable(helpers.assertIsClass(class)).__staticMetamethods[method]
end

function debug.getClassSetters(class)
	return helpers.assertIsClass(class).__setters__
end

function debug.getClassGetters(class)
	return helpers.assertIsClass(class).__getters__
end

function debug.getClassParents(value)
	return helpers.assertIsClass(value).__parent__
end

function debug.getClassName(class)
	return classRegistry[helpers.assertIsClass(class)].__customMetadata.name
end

function debug.getClassId(class)
	return classRegistry[helpers.assertIsClass(class)].__customMetadata.id
end

function debug.getClassByName(name)
	if type(name) ~= 'string' then error("Argument #1 must be a string", 2) end

	for k, v in pairs(classRegistry) do
		if v.__customMetadata.name == name then return k end	
	end
	
	return nil
end

---------------------------------------------------------------------------------
-- Class prototype functions
---------------------------------------------------------------------------------
function debug.getClassOfPrototype(proto)
	return helpers.assertIsPrototype(proto).__holdingClass__
end

function debug.getPrototypeMetatable(proto)
	return prototypeRegistry[helpers.assertIsPrototype(proto)]
end

function debug.getPrototypeTable(proto)
	return prototypeRegistry[helpers.assertIsPrototype(proto)].__prototype
end

function debug.getPrototypeConstructor(proto)
	return prototypeRegistry[helpers.assertIsPrototype(proto)].__prototype.constructor
end

---------------------------------------------------------------------------------
-- Class/Class instance functions
---------------------------------------------------------------------------------
function debug.getParentOf(value)
	return helpers.assertIsInstanceOrClassOrProto(value).__parent__
end

function debug.getPrototypeOf(value)
	return helpers.assertIsInstanceOrClassOrProto(value).__proto__
end

function debug.getNameOf(value)
	helpers.assertIsInstanceOrClassOrProto(value)
	
	if p.isClass(value) then
		return classRegistry[value].__customMetadata.name
	elseif p.isPrototype(value) then
		return prototypeRegistry[value].__staticMt.__customMetadata.name
	elseif p.isInstance(value) then
		return instanceRegistry[value].__staticMt.__customMetadata.name
	end
	
	error("Invalid program state entered")
end

return setmetatable(p, {
	__call = function(_, ...) return p.makeClass(...) end,
})