local g=require(script.Parent.Tables)local C=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")local Q=require(C:WaitForChild("\078\101\118\101\114\109\111\114\101"))local D=(Q("\083\116\100")).t local q={}function q.makeClass(C,Q)assert(D.string(C),"\067\108\097\115\115 \110\097\109\101 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")assert((D.optional(D.callback))(Q),"\067\108\097\115\115 \099\111\110\115\116\114\117\099\116\111\114 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110 \111\114 \110\105\108")Q=Q or function()return{}end local S={name=C}function S.new(...)local g=Q(...)setmetatable(g,{__index=S,__tostring=S.toString})if g._init then g:_init(...)end g.Class=S return g end function S.isInstance(g)local Q=q.isA(g,S)return Q,not Q and string.format("\078\111\116 \097 \037\115 \105\110\115\116\097\110\099\101",C)or nil end function S.extend(Q,g,C)local D=q.makeClass(g,C or S.new)setmetatable(D,{__index=Q})return D end function S.extendWithInterface(D,C,Q)local function S(g)if g==nil then return function()return{}end elseif type(g)=="\102\117\110\099\116\105\111\110"then return g else return function()return g end end end local W=D.interface local z=function(C)return g.assign({},(S(Q))(C),(S(W))(C))end local X=q.makeClassWithInterface(C,z)setmetatable(X,{__index=D})return X end function S.toString(g)return g.name end return S end function q.makeClassWithInterface(C,Q)local function S(g)local Q,q=(D.values(D.callback))(g)assert(Q,string.format("\067\108\097\115\115 \037\115 \100\111\101\115 \110\111\116 \104\097\118\101 \097 \118\097\108\105\100 \105\110\116\101\114\102\097\099\101\n\037\115",C,tostring(q)))return D.strictInterface(g)end local W local z=q.makeClass(C,function(Q)Q=Q or{}local D,q=W(Q)assert(D,string.format("\067\108\097\115\115 \037\115 \099\097\110\110\111\116 \098\101 \105\110\115\116\097\110\116\105\097\116\101\100\n\037\115",C,tostring(q)))return g.mapKeys(Q,function(g,C)return"\095"..C end)end)W=type(Q)=="\102\117\110\099\116\105\111\110"and S(Q(z))or S(Q)z.interface=Q return z end function q.makeEnum(C)local Q=g.keyBy(C,function(g)assert(g:match("\094\091\065-\090\095\093\043\036"),"\069\110\117\109 \107\101\121\115 \109\117\115\116 \098\101 \100\101\102\105\110\101\100 \097\115 \117\112\112\101\114 \115\110\097\107\101 \099\097\115\101")return g end)setmetatable(Q,{__index=function(g,C)error(string.format("\065\116\116\101\109\112\116 \116\111 \097\099\099\101\115\115 \107\101\121 \037\115 \119\104\105\099\104 \105\115 \110\111\116 \097 \118\097\108\105\100 \107\101\121 \111\102 \116\104\101 \101\110\117\109",C))end;__newindex=function(g,C)error(string.format("\065\116\116\101\109\112\116 \116\111 \115\101\116 \107\101\121 \037\115 \111\110 \101\110\117\109",C))end})return Q end function q.applySwitchStrategyForEnum(C,Q,S,...)assert(q.isA(Q,C),"\101\110\117\109\086\097\108\117\101 \109\117\115\116 \098\101 \097\110 \105\110\115\116\097\110\099\101 \111\102 \101\110\117\109")assert(g.deepEquals(g.sort(g.values(C)),g.sort(g.keys(S))),"\107\101\121\115 \102\111\114 \115\116\114\097\116\101\103\105\101\115 \109\117\115\116 \109\097\116\099\104 \118\097\108\117\101\115 \102\111\114 \101\110\117\109")assert((D.values(D.callback))(S),"\115\116\114\097\116\101\103\105\101\115 \118\097\108\117\101\115 \109\117\115\116 \098\101 \102\117\110\099\116\105\111\110\115")return S[Q](...)end function q.makeFinal(g)local C=getmetatable(g)local Q={__index=function(g,C)error(string.format("\065\116\116\101\109\112\116 \116\111 \097\099\099\101\115\115 \107\101\121 \037\115 \119\104\105\099\104 \105\115 \109\105\115\115\105\110\103 \105\110 \102\105\110\097\108 \111\098\106\101\099\116",C))end,__newindex=function(g,C)error(string.format("\065\116\116\101\109\112\116 \116\111 \115\101\116 \107\101\121 \037\115 \111\110 \102\105\110\097\108 \111\098\106\101\099\116",C))end}if C then setmetatable(Q,C)end setmetatable(g,Q)return g end function q.isA(g,C)local Q=type(g)=="\115\116\114\105\110\103"if Q then local Q=type(C[g])=="\115\116\114\105\110\103"return Q elseif type(g)=="\116\097\098\108\101"then if g.__symbol and C[g.__symbol]==g then return true end local Q=getmetatable(g)while Q do if Q.__index==C then return true end Q=getmetatable(Q.__index)end end return false end function q.makeSymbol(g)local C={__symbol=g}setmetatable(C,{__tostring=function()return"\083\121\109\098\111\108\040"..(g.."\041")end})return C end return q
local Tables = require(script.Parent.Tables)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local require = require(ReplicatedStorage:WaitForChild("Nevermore"))
local t = require("Std").t

local Classes = {}

function Classes.makeClass(name, constructor)
	assert(t.string(name), "Class name must be a string")
	assert(t.optional(t.callback)(constructor), "Class constructor must be a function or nil")
	constructor = constructor or function()
			return {}
		end
	local Class = {
		name = name
	}
	function Class.new(...)
		local instance = constructor(...)
		setmetatable(instance, {__index = Class, __tostring = Class.toString})
		if instance._init then
			instance:_init(...)
		end
		instance.Class = Class
		return instance
	end
	function Class.isInstance(value)
		local ok = Classes.isA(value, Class)
		return ok, not ok and string.format("Not a %s instance", name) or nil
	end
	function Class:extend(name, subConstructor)
		local SubClass = Classes.makeClass(name, subConstructor or Class.new)
		setmetatable(SubClass, {__index = self})
		return SubClass
	end
	function Class:extendWithInterface(name, interface)
		local function getComposableInterface(input)
			if input == nil then
				return function()
					return {}
				end
			elseif type(input) == "function" then
				return input
			else
				return function()
					return input
				end
			end
		end
		local inheritedInterface = self.interface
		-- NOTE: Sub interfaces can at present override super interfaces, so this should be avoided
		-- to provide better validation detection / true field type inheritence.
		local compositeInterface = function(Class)
			return Tables.assign({}, getComposableInterface(interface)(Class), getComposableInterface(inheritedInterface)(Class))
		end
		local SubClass = Classes.makeClassWithInterface(name, compositeInterface)
		setmetatable(SubClass, {__index = self})
		return SubClass
	end
	function Class:toString()
		return self.name
	end
	return Class
end

function Classes.makeClassWithInterface(name, interface)
	local function getImplementsInterface(currentInterface)
		local ok, problem = t.values(t.callback)(currentInterface)
		assert(ok, string.format([[Class %s does not have a valid interface
%s]], name, tostring(problem)))
		return t.strictInterface(currentInterface)
	end
	local implementsInterface
	local Class =
		Classes.makeClass(
		name,
		function(data)
			data = data or {}
			local ok, problem = implementsInterface(data)
			assert(ok, string.format([[Class %s cannot be instantiated
%s]], name, tostring(problem)))
			return Tables.mapKeys(
				data,
				function(_, key)
					return "_" .. key
				end
			)
		end
	)
	implementsInterface =
		type(interface) == "function" and getImplementsInterface(interface(Class)) or getImplementsInterface(interface)
	Class.interface = interface
	return Class
end

function Classes.makeEnum(keys)
	local enum =
		Tables.keyBy(
		keys,
		function(key)
			assert(key:match("^[A-Z_]+$"), "Enum keys must be defined as upper snake case")
			return key
		end
	)

	setmetatable(
		enum,
		{
			__index = function(t, key)
				error(string.format("Attempt to access key %s which is not a valid key of the enum", key))
			end,
			__newindex = function(t, key)
				error(string.format("Attempt to set key %s on enum", key))
			end
		}
	)

	return enum
end

function Classes.applySwitchStrategyForEnum(enum, enumValue, strategies, ...)
	assert(Classes.isA(enumValue, enum), "enumValue must be an instance of enum")
	assert(
		Tables.deepEquals(Tables.sort(Tables.values(enum)), Tables.sort(Tables.keys(strategies))),
		"keys for strategies must match values for enum"
	)
	assert(t.values(t.callback)(strategies), "strategies values must be functions")

	return strategies[enumValue](...)
end

function Classes.makeFinal(object)
	local backend = getmetatable(object)
	local proxy = {
		__index = function(t, key)
			error(string.format("Attempt to access key %s which is missing in final object", key))
		end,
		__newindex = function(t, key)
			error(string.format("Attempt to set key %s on final object", key))
		end
	}
	if backend then
		setmetatable(proxy, backend)
	end

	setmetatable(object, proxy)

	return object
end

function Classes.isA(instance, classOrEnum)
	local isEnum = type(instance) == "string"
	if isEnum then
		local isEnumKeyDefined = type(classOrEnum[instance]) == "string"
		return isEnumKeyDefined
	elseif type(instance) == "table" then
		if instance.__symbol and classOrEnum[instance.__symbol] == instance then
			return true
		end
		local metatable = getmetatable(instance)
		while metatable do
			if metatable.__index == classOrEnum then
				return true
			end
			metatable = getmetatable(metatable.__index)
		end
	end
	return false
end

function Classes.makeSymbol(name)
	local symbol = {
		__symbol = name
	}
	setmetatable(
		symbol,
		{
			__tostring = function()
				return "Symbol(" .. name .. ")"
			end
		}
	)
	return symbol
end

return Classes