local K={}K.__index=K K.__newindex=function(K)error("\067\097\110\110\111\116 \097\115\115\105\103\110 \118\097\108\117\101 \116\111 "..tostring(K),2)end K.__tostring=function()return"\079\098\106\101\099\116\070\097\099\116\111\114\121"end local M={}function K.Get(K,D)return assert(M[D],tostring(D).." \105\115 \110\111\116 \105\110 \116\104\101 \114\101\103\105\115\116\114\121\033")end local function D(K)local M=newproxy(true)local D=("\083\121\109\098\111\108\040\037\115\041"):format(K);(getmetatable(M)).__tostring=function()return D end return M end local b=D("\067\108\097\115\115")local a=D("\073\110\104\101\114\105\116\097\110\099\101")local U=D("\073\110\115\116\097\110\099\101")local Z=D("\080\097\114\101\110\116\067\108\097\115\115")local A=D("\067\108\097\115\115\078\097\109\101")local m=D("\083\101\097\108\101\100\077\111\100\105\102\105\101\114")local k=D("\082\101\102")function K.Enum(M,D,b)local a=K:Extend(D,{sealed=true})function a.constructor(D,K,M)D.Name=K D.Value=M end function a.tostring(K)return"\069\110\117\109\091"..(D..("\064"..(K.Name.."\093")))end for K,M in next,b do a[M]=a.new(M,K)end a.new=nil return a end local function y(K,M)local D=getmetatable(K)if D[U]then for K,D in next,D[b][a]do if D[A]==M then return true end end return false else error("\067\097\110\110\111\116 \099\097\108\108 \073\115\065 \111\110 \097 \099\108\097\115\115 \116\121\112\101",2)end end local function J(K,M)local D=getmetatable(K)local b=type(M)=="\116\097\098\108\101"and getmetatable(M)if not D or D[U]then error("\096\073\115\096 \109\117\115\116 \098\101 \099\097\108\108\101\100 \111\110 \097 \099\108\097\115\115\033",2)end if b and b[U]then return M:IsType(K[A])else return false end end local function x(K,M,...)if type(M)~="\116\097\098\108\101"then error("\070\105\114\115\116 \097\114\103\117\109\101\110\116 \111\102 \115\117\112\101\114 \109\117\115\116 \098\101 \097\110 \111\098\106\101\099\116\033",2)end if K==(getmetatable(M)).__index then error("\067\097\108\108 \'\115\117\112\101\114\' \111\110 \116\104\101 \098\097\115\101 \099\108\097\115\115 \114\097\116\104\101\114 \116\104\097\110 \116\104\101 \115\097\109\101 \099\108\097\115\115\033",2)end if type(K.constructor)=="\102\117\110\099\116\105\111\110"then K.constructor(M,...)end end local function u()error("\067\097\110\110\111\116 \099\097\108\108 \110\101\119 \111\110 \111\098\106\101\099\116 \105\110\115\116\097\110\099\101",2)end local function z(K,...)error(K:format(...),3)end local f={__index=function()error("\065\116\116\101\109\112\116 \116\111 \105\110\100\101\120 \100\101\115\116\114\111\121\101\100 \111\098\106\101\099\116",2)end,__newindex=function()error("\065\116\116\101\109\112\116 \116\111 \115\101\116 \118\097\108\117\101 \111\110 \100\101\115\116\114\111\121\101\100 \111\098\106\101\099\116",2)end,__tostring=function()return"\068\101\115\116\114\111\121\101\100\079\098\106\101\099\116"end}function K.Destroy(M)if M==K then error("\067\097\110\110\111\116 \099\097\108\108 \100\101\115\116\114\111\121 \111\110 \079\098\106\101\099\116\070\097\099\116\111\114\121\033",2)end local D=getmetatable(M)if D[U]then if type(M.destructor)=="\102\117\110\099\116\105\111\110"then M:destructor()end for K in next,M do M[K]=nil end setmetatable(M,f)else error("\067\097\110\110\111\116 \099\097\108\108 \100\101\115\116\114\111\121 \111\110 "..tostring(M))end end local H=0 function K.Extend(u,K,D)H=H+1 assert(type(K)=="\115\116\114\105\110\103")assert(type(D)=="\116\097\098\108\101"or D==nil)D=D or{}local f=D.sealed or false local c=D.abstract or false local G=D.mutators or false local Q=D.operators or false local S=D.serializable or false if M[K]then z("\091\079\098\106\101\099\116\093 \068\117\112\108\105\099\097\116\101 \099\108\097\115\115 \096\037\115\096",K)end if f and c then z("\091\079\098\106\101\099\116\093 \037\115 \099\097\110\110\111\116 \098\101 \098\111\116\104 \115\101\097\108\101\100 \097\110\100 \097\098\115\116\114\097\099\116\046",K)end if u[m]then z("\091\079\098\106\101\099\116\093 \067\097\110\110\111\116 \101\120\116\101\110\100 \037\115 \102\114\111\109 \115\101\097\108\101\100 \099\108\097\115\115 \037\115 - \097\115 \105\116 \115\101\097\108\101\100\046",K,u[A])end local v={__index=u;__tostring=function(M)return"\091\099\108\097\115\115 "..(K.."\093")end}local e={}e.__index=e e[k]=H e[A]=K e.ClassName=K e[Z]=v e[a]={e,unpack(u[a]or{})}if f then e[m]=true end setmetatable(e,v)e.Is=J e.IsType=y e.super=x if G then e.get={}e.set={}end function e.instance(M)local D="\069\120\112\101\099\116\101\100 \037\115 \103\111\116 \037\115"local a=type(M)=="\116\097\098\108\101"and getmetatable(M)if a then local U=a[b]if U then return M:IsType(K),D:format(K,U[A])elseif M[A]then return false,"\069\120\112\101\099\116\101\100 "..(K..(" \103\111\116 \099\108\097\115\115 "..M[A]))else return false,"\069\120\112\101\099\116\101\100 "..(K..(" \103\111\116 "..tostring(M)))end else return false,D:format(K,type(M))end end if not c then if S then function e.serialize(K)assert(e.instance(K))return K:Serialize()end function e.deserialize(M)error(tostring(K).."\046\100\101\115\101\114\105\097\108\105\122\101 \110\111\116 \105\109\112\108\101\109\101\110\116\101\100\046")end function e.Serialize(K)if typeof(S)=="\116\097\098\108\101"then local M={}for D,b in next,S do if typeof(K["\071\101\116"..b])=="\102\117\110\099\116\105\111\110"then M[b]=K["\071\101\116"..b](K)elseif rawget(K,b)then M[b]=rawget(K,b)end end return M else error("\083\101\114\105\097\108\105\122\101 \109\117\115\116 \098\101 \111\118\101\114\114\105\100\101\044 \111\114 \097 \116\097\098\108\101 \111\102 \112\114\111\112\115 \116\111 \098\101 \115\101\114\105\097\108\105\122\101\100 \109\117\115\116 \098\101 \112\097\115\115\101\100 \116\111 \115\101\114\105\097\108\105\122\097\098\108\101")end end end function e.new(...)H=H+1 local M=true local D={[k]=H,[Z]=v,[b]=e;__index=e;[U]=true,__tostring=function(M)if type(M.tostring)=="\102\117\110\099\116\105\111\110"then return M:tostring()else return K..("\058 "..string.format("\037\046\049\054\088",(getmetatable(M))[k]))end end}if Q then D.__add=e.add or nil D.__sub=e.sub or nil D.__mul=e.mul or nil D.__div=e.div or nil D.__mod=e.mod or nil D.__lt=e.lessthan or nil D.__gt=e.greater or nil D.__eq=e.equal or nil end if G then D.__newindex=function(K,D,b)local a=rawget(e,"\115\101\116")local U=type(a)=="\116\097\098\108\101"and rawget(a,tostring(D))if M then rawset(K,D,b)elseif type(a)=="\102\117\110\099\116\105\111\110"then a(K,D,b)elseif type(U)=="\102\117\110\099\116\105\111\110"then U(K,b)else error("\067\097\110\110\111\116 \097\115\115\105\103\110 \110\101\119 \112\114\111\112\101\114\116\121 \111\117\116\115\105\100\101 \111\102 \099\111\110\115\116\114\117\099\116\111\114\058 "..tostring(D),2)end end D.__index=function(K,M)local D=rawget(e,"\103\101\116")local b=type(D)=="\116\097\098\108\101"and rawget(D,tostring(M))local a=type(D)=="\102\117\110\099\116\105\111\110"and D(K,M)or rawget(K,M)if a then return a elseif type(b)=="\102\117\110\099\116\105\111\110"then return b(K)else return e[M]end end end local a=setmetatable({},D)if type(e.constructor)=="\102\117\110\099\116\105\111\110"then e.constructor(a,...)end M=false return a end else function e.new()z("\091\037\115\046\110\101\119\093 \067\097\110\110\111\116 \099\114\101\097\116\101 \116\121\112\101 \100\101\102\105\110\101\100 \097\115 \097\098\115\116\114\097\099\116",K)end end M[K]=e return e end function K.typeIs(K)warn("\091\079\098\106\101\099\116\046\116\121\112\101\073\115\093 \116\121\112\101\073\115 \105\115 \100\101\112\114\101\099\097\116\101\100\044 \117\115\101 \067\108\097\115\115\046\105\110\115\116\097\110\099\101 \105\110\115\116\101\097\100\046")print(debug.traceback())if type(K)=="\116\097\098\108\101"then return function(M)local D=getmetatable(M)if not D then return false,"\101\120\112\101\099\116\101\100 \096"..(K[A]..("\096 \103\111\116  \096"..(typeof(M).."\096")))end return K:Is(M),"\101\120\112\101\099\116\101\100 \096"..(K[A]..("\096 \103\111\116  \096"..(D[b][A].."\096")))end else return function(M)local D=getmetatable(M)if D and D[U]then return M:IsType(K),"\101\120\112\101\099\116\101\100 \096"..(K..("\096 \103\111\116 \096"..(D[A].."\096")))end return false,"\101\120\112\101\099\116\101\100 \096"..(K..("\096 \103\111\116 \096"..(typeof(M).."\096")))end end end return setmetatable({},K)
local Object = {}
Object.__index = Object
Object.__newindex = function(self)
	error("Cannot assign value to " .. tostring(self), 2)
end
Object.__tostring = function()
	return "ObjectFactory"
end
local registry = {}

function Object.Get(_, name)
	return (assert(registry[name], tostring(name) .. " is not in the registry!"))
end

-- luacov: disable
local function symbol(name)
	local self = newproxy(true)
	local wrappedName = ("Symbol(%s)"):format(name)
	getmetatable(self).__tostring = function()
		return wrappedName
	end

	return self
end
-- luacov: enable

local ID_CLASS = symbol("Class")
local ID_INHERITANCE = symbol("Inheritance")
local ID_INSTANCE = symbol("Instance")
local ID_PARENT_CLASS = symbol("ParentClass")
local ID_CLASS_NAME = symbol("ClassName")
local ID_SEALED = symbol("SealedModifier")
local ID_REF = symbol("Ref")

--- Creates a sealed enum class
function Object.Enum(_, name, values)
	local enum = Object:Extend(name, {sealed = true})
	function enum:constructor(_name, _value)
		self.Name = _name
		self.Value = _value
	end

	function enum:tostring()
		return "Enum[" .. name .. "@" .. self.Name .. "]"
	end

	for idx, value in next, values do
		enum[value] = enum.new(value, idx)
	end

	enum.new = nil

	return enum
end

-- Used to check, like Rbx:IsA
local function class_IsType(object, className)
	local meta = getmetatable(object)
	if (meta[ID_INSTANCE]) then
		for _, baseClass in next, meta[ID_CLASS][ID_INHERITANCE] do
			if (baseClass[ID_CLASS_NAME] == className) then
				return true
			end
		end

		return false
	else
		error("Cannot call IsA on a class type", 2)
	end
end

local function class_Is(self, value)
	local selfmeta = getmetatable(self)
	local meta = type(value) == "table" and getmetatable(value)

	if not selfmeta or selfmeta[ID_INSTANCE] then
		error("`Is` must be called on a class!", 2)
	end

	if meta and meta[ID_INSTANCE] then
		return value:IsType(self[ID_CLASS_NAME])
	else
		return false
	end
end

-- Used to pass inheritance
local function class_super(_class, obj, ...)
	-- luacov: disable
	if (type(obj) ~= "table") then
		error("First argument of super must be an object!", 2)
	end

	if (_class == getmetatable(obj).__index) then
		error("Call 'super' on the base class rather than the same class!", 2)
	end
	-- luacov: enable

	if type(_class.constructor) == "function" then
		_class.constructor(obj, ...)
	end
end

local function throwOnNew()
	error("Cannot call new on object instance", 2)
end -- prevent creating object from another object

local function errorf(fmt, ...)
	error((fmt):format(...), 3)
end

local Destroyed = {
	__index = function()
		error("Attempt to index destroyed object", 2)
	end,
	__newindex = function()
		error("Attempt to set value on destroyed object", 2)
	end,
	__tostring = function()
		return "DestroyedObject"
	end
}

function Object:Destroy()
	-- luacov: disable
	if self == Object then
		error("Cannot call destroy on ObjectFactory!", 2)
	end
	-- luacov: enable

	local meta = getmetatable(self)
	if meta[ID_INSTANCE] then
		-- If the user wants to do their own cleanup, they can! :-)
		if type(self.destructor) == "function" then
			self:destructor()
		end

		for index in next, self do
			-- Set child values to nil
			self[index] = nil
		end

		setmetatable(self, Destroyed)
	else
		error("Cannot call destroy on " .. tostring(self))
	end
end

local refId = 0

--- Class:Extend(str) for class extending, default is Object:Extend( ) for base classes
function Object:Extend(name, options)
	refId = refId + 1
	assert(type(name) == "string")
	assert(type(options) == "table" or options == nil)

	options = options or {}
	local sealed = options.sealed or false
	local abstract = options.abstract or false
	local mutators = options.mutators or false
	local operators = options.operators or false
	local serializable = options.serializable or false

	if (registry[name]) then
		errorf("[Object] Duplicate class `%s`", name)
	end

	if sealed and abstract then
		errorf("[Object] %s cannot be both sealed and abstract.", name)
	end

	if (self[ID_SEALED]) then
		errorf("[Object] Cannot extend %s from sealed class %s - as it sealed.", name, self[ID_CLASS_NAME])
	end

	local super = {
		__index = self,
		__tostring = function(self)
			return "[class " .. name .. "]"
		end
	}

	local class = {}
	class.__index = class
	class[ID_REF] = refId
	class[ID_CLASS_NAME] = name
	class.ClassName = name
	class[ID_PARENT_CLASS] = super
	class[ID_INHERITANCE] = {class, unpack(self[ID_INHERITANCE] or {})}

	if (sealed) then
		class[ID_SEALED] = true
	end

	setmetatable(class, super)

	class.Is = class_Is
	class.IsType = class_IsType
	class.super = class_super

	if mutators then
		class.get = {}
		class.set = {}
	end

	-- static instance(value: unknown): value is {ClassName}
	function class.instance(value)
		local err = "Expected %s got %s"
		local meta = type(value) == "table" and getmetatable(value)

		if meta then
			local metaclass = meta[ID_CLASS]
			if metaclass then
				return value:IsType(name), err:format(name, metaclass[ID_CLASS_NAME])
			elseif value[ID_CLASS_NAME] then
				return false, "Expected " .. name .. " got class " .. value[ID_CLASS_NAME]
			else
				return false, "Expected " .. name .. " got " .. tostring(value)
			end
		else
			return false, err:format(name, type(value))
		end
	end

	-- Handler for X.new(...) - Calls X.constructor(...) internally
	if not abstract then
		if serializable then
			function class.serialize(obj)
				assert(class.instance(obj))
				return obj:Serialize()
			end

			function class.deserialize(tbl)
				error(tostring(name) .. ".deserialize not implemented.")
			end

			-- Default serializer
			function class:Serialize()
				if typeof(serializable) == "table" then
					local result = {}
					for _, prop in next, serializable do
						if typeof(self["Get" .. prop]) == "function" then
							result[prop] = self["Get" .. prop](self)
						elseif rawget(self, prop) then
							result[prop] = rawget(self, prop)
						end
					end
					return result
				else
					error("Serialize must be override, or a table of props to be serialized must be passed to serializable")
				end
			end
		end

		function class.new(...)
			refId = refId + 1
			local allowNewProperties = true
			local meta = {
				[ID_REF] = refId,
				[ID_PARENT_CLASS] = super,
				[ID_CLASS] = class,
				-- __newindex = ,
				__index = class,
				[ID_INSTANCE] = true,
				__tostring = function(self)
					if type(self.tostring) == "function" then
						return self:tostring()
					else
						return name .. ": " .. string.format("%.16X", getmetatable(self)[ID_REF])
					end
				end
			}

			-- luacov: disable
			-- TODO: Write tests
			if operators then
				meta.__add = class.add or nil
				meta.__sub = class.sub or nil
				meta.__mul = class.mul or nil
				meta.__div = class.div or nil
				meta.__mod = class.mod or nil
				meta.__lt = class.lessthan or nil
				meta.__gt = class.greater or nil
				meta.__eq = class.equal or nil
			end
			-- luacov: enable

			-- experimental mutators
			-- luacov: disable
			-- TODO: Write tests
			if mutators then
				meta.__newindex = function(self, index, value)
					local setterGlobal = rawget(class, "set")
					local setterFn = type(setterGlobal) == "table" and rawget(setterGlobal, tostring(index))
					if allowNewProperties then
						rawset(self, index, value)
					elseif type(setterGlobal) == "function" then
						setterGlobal(self, index, value)
					elseif type(setterFn) == "function" then
						setterFn(self, value)
					else
						error("Cannot assign new property outside of constructor: " .. tostring(index), 2)
					end
				end
				meta.__index = function(self, index)
					local getterGlobal = rawget(class, "get")
					local getterFn = type(getterGlobal) == "table" and rawget(getterGlobal, tostring(index))
					local child = type(getterGlobal) == "function" and getterGlobal(self, index) or rawget(self, index)
					if child then
						return child
					elseif type(getterFn) == "function" then
						return getterFn(self)
					else
						return class[index]
					end
				end
			end
			-- luacov: enable

			local obj = setmetatable({}, meta)
			-- obj.new = throwOnNew

			if type(class.constructor) == "function" then
				class.constructor(obj, ...)
			end

			allowNewProperties = false

			return obj
		end
	else
		function class.new()
			errorf("[%s.new] Cannot create type defined as abstract", name)
		end
	end

	registry[name] = class
	return class
end

-- luacov: disable
function Object.typeIs(t)
	warn("[Object.typeIs] typeIs is deprecated, use Class.instance instead.")
	print(debug.traceback())

	if (type(t) == "table") then
		return function(value)
			local meta = getmetatable(value)
			if not meta then
				return false, "expected `" .. t[ID_CLASS_NAME] .. "` got  `" .. typeof(value) .. "`"
			end

			return t:Is(value), "expected `" .. t[ID_CLASS_NAME] .. "` got  `" .. meta[ID_CLASS][ID_CLASS_NAME] .. "`"
		end
	else
		return function(value)
			local meta = getmetatable(value)
			if (meta and meta[ID_INSTANCE]) then
				return value:IsType(t), "expected `" .. t .. "` got `" .. meta[ID_CLASS_NAME] .. "`"
			end

			return false, "expected `" .. t .. "` got `" .. typeof(value) .. "`"
		end
	end
end
-- luacov: enable

return setmetatable({}, Object)
