------------------------------------------------ LevelOOPer Library 2.2.6 ------------------------------------------------
function _G.pairs(t)
	local mt = getmetatable(t)
	if mt and type(mt.__pairs) == "function" then
		return mt.__pairs(t)
	else
		return next, t, nil
	end
end

local function fread(file)
	local f = fs.open(file,"r")
	local txt = f.readAll()
	f.close()
	return txt
end

local env = _ENV
local oop = {}

local classCache = {}
local privCache = {}
local publicCache = {}
local objectCache = {}

local dir = fs.getDir(({...})[1] or shell.getRunningProgram())

local function typeRestriction(index, sType, value)
	if not oop.isType(value, sType) then
		return false, "cannot convert "..sType.." '"..index.."' to '"..oop.type(value, true).."'"
	else
		return true
	end
end

local function getNumberType(number)
	if number%1 == 0 then
		return "int"
	else
		return "double"
	end
end

oop.setEnv = function(newEnv)
	env = newEnv
end

oop.setDir = function(newDir)
	oop.expect(1, newDir, "string")
	dir = newDir
end

oop.getDir = function()
	return dir
end

local default = {
	int = 0,
	double = 0,
	number = 0,
	string = "",
	["function"] = function() end,
	table = {},
	boolean = false,
}

local varTypes = {
	int = "int",
	double = "double",
	num = "number",
	str = "string",
	func = "function",
	tbl = "table",
	bool = "boolean",
}

local newVar = {
	var = function(name)
		local tbl = {name=name}
		setmetatable(tbl, {
			__call = function(self, value)
				self.value = value
				return self
			end
		})
		return tbl
	end,
	obj = function(class)
		oop.expect(1, class, "class")
		local var = {type=classCache[class].name}
		setmetatable(var, {
			__call = function(self, name)
				self.name = name
				setmetatable(var, {
					__call = function(self, value)
						local ok,err = typeRestriction(name, var.type, value)
						if not ok then
							error(err, 2)
						else
							self.value = value
							return self
						end
					end,
				})
				return self
			end,
		})
		return var
	end,
}

local const = function(name)
	if type(name) == "function" then
		return function(realname)
			local var = name(realname)
			var.const = true
			return var
		end
	elseif type(name) == "string" then
		local var = {name=name, const=true}
		setmetatable(var, {
			__call = function(self, value)
				self.value = value
				return self
			end,
		})
		return var
	end
end

for k,v in pairs(varTypes) do
	newVar[k] = function(name)
		local tbl = {name=name, type=varTypes[k]}
		setmetatable(tbl, {
			__call = function(self, value)
				local stat,err = typeRestriction(name, tbl.type, value)
				if not stat then
					error(err, 2)
				else
					self.value = value
					return self
				end
			end,
		})
		return tbl
	end
end

oop.injectEnv = function(env)
	env.oop = oop
	env.class = oop.class
	env.const = const
	env.import = oop.import
	env.type = oop.type
	env.getPublicObject = oop.getPublicObject
	for k,v in pairs(newVar) do
		env[k] = v
	end
end

local metamethods = {
	["__add"] = true,
	["__sub"] = true,
	["__mul"] = true,
	["__div"] = true,
	["__mod"] = true,
	["__pow"] = true,
	["__unm"] = true,
	["__concat"] = true,
	["__len"] = true,
	["__eq"] = true,
	["__lt"] = true,
	["__le"] = true,
	["__index"] = true,
	["__newindex"] = true,
	["__call"] = true,
	["__tostring"] = true,
	["__metatable"] = true,
	["__pairs"] = true,
	["__ipairs"] = true
}

local function sanitizeArgs(...)
	local args = table.pack(...)
	for k,v in pairs(args) do
		if publicCache[v] then
			args[k] = publicCache[v]
		end
	end
	return table.unpack(args, nil, args.n)
end

local function instantiate(orig)
	local function deepCopy(o, seen)
		seen = seen or {}
		if seen[o] then
			return seen[o]
		end
		local copy
		if type(o) == 'table' then
			copy = {}
			seen[o] = copy
			for k,v in pairs(o) do
				copy[deepCopy(k, seen)] = deepCopy(v, seen)
			end
			setmetatable(copy, deepCopy(getmetatable(o), seen))
		else
			copy = o
		end
		return copy
	end
	return deepCopy(orig)
end

local function makeClass(name, classTbl, inherit)
	local class = {
		static = {
		},
		private = {
		},
		public = {
		},
		properties = {
			static = {
			},
			private = {
			},
			public = {
			},
			types = {
			},
			consts = {
			},
		},
		metatable = {
			public = {
			},
			private = {
			},
		},
		name = name,
	}

	class.metatable.static = {
		__index = class.static,
		__newindex = function(tbl, k, v)
			if class.properties.static[k] then
				if class.properties.consts[k] then
					error("cannot modify const '"..tostring(k).."'", 2)
				elseif class.properties.types[k] then
					local stat,err = typeRestriction(k, class.properties.types[k], v)
					if not stat then
						error(err, 2)
					else
						class.static[k] = v
					end
				end
			elseif class.properties.private[k] then
				error("cannot modify private property '"..tostring(k).."' outside of class", 2)
			elseif class.properties.public[k] then
				error("cannot modify public property '"..tostring(k).."' outside of object", 2)
			end
		end,
		__call = function(self, ...)
			local privObj = {}
			local obj = {}

			privCache[obj] = privObj
			publicCache[privObj] = obj

			local properties = {}
			local privProperties = {}

			setmetatable(properties, {__index=class.public})
			setmetatable(privProperties, {
				__index=function(t, k)
					return properties[k] or class.private[k]
				end
			})

			local mt = {
				__index = function(t, k)
					local p = properties
					if t == privObj then
						p = privProperties
					end
					local v = p[k]
					if type(v) == "function" then
						local tEnv = {self=privObj}
						oop.injectEnv(tEnv)
						setmetatable(tEnv, {__index=env})
						setfenv(v, tEnv)
					end
					return v
				end
			}
			
			local inConstructor = false

			mt.__pairs = function(tbl)
				local loopThroughTables = {properties, class.public, class.static}
				if tbl == privObj then
					table.insert(loopThroughTables, 2, class.private)
				end
				local cls = class
				while cls.parent do -- this WONT WORK
					cls = cls.parent
					if tbl == privObj then
						table.insert(loopThroughTables, cls.private)
					end
					table.insert(loopThroughTables, cls.public)
					table.insert(loopThroughTables, cls.static)
				end
				
				local currentIndex = 1

				local had = {}
				
				local function customIterator(_, prevKey)
					local key, value
					
					while currentIndex <= #loopThroughTables do
						key, value = next(loopThroughTables[currentIndex], prevKey)
						while key ~= nil and had[key] do -- skip keys that were already found
							key, value = next(loopThroughTables[currentIndex], key)
						end
						
						if key ~= nil then
							had[key] = true
							return key, tbl[key]
						else
							prevKey = nil
							currentIndex = currentIndex + 1
						end
					end
					
					return nil
				end
				
				return customIterator, tbl, nil
			end
			
			mt.__newindex = function(tbl, k, v)
				if class.properties.consts[k] and not inConstructor then
					error("cannot modify const '"..tostring(k).."'", 2)
				end
				if tbl ~= privObj and class.properties.private[k] then
					error("cannot modify private property '"..tostring(k).."' outside of class", 2)
				end
				if class.properties.types[k] then
					local stat,err = typeRestriction(k, class.properties.types[k], v)
					if not stat then
						error(err, 2)
					end
				end
				if class.properties.static[k] then
					-- change in whole class
					class.static[k] = v
				elseif class.properties.private[k] then
					privProperties[k] = v
				else
					properties[k] = v
				end
			end

			local mt2 = {}

			for k,v in pairs(mt) do
				mt2[k] = v
			end

			for k,v in pairs(class.metatable.public) do
				mt[k] = function(...)
					local tEnv = {self=privObj}
					oop.injectEnv(tEnv)
					setmetatable(tEnv, {__index=env})
					setfenv(v, tEnv)
					return v(sanitizeArgs(...))
				end
			end

			for k,v in pairs(class.metatable.private) do
				mt2[k] = function(...)
					local tEnv = {self=privObj}
					oop.injectEnv(tEnv)
					setmetatable(tEnv, {__index=env})
					setfenv(v, tEnv)
					return v(sanitizeArgs(...))
				end
			end

			setmetatable(obj, mt)
			setmetatable(privObj, mt2)

			local constructors = {class.constructor}
			local cls = class
			while cls.parent do
				cls = cls.parent
				if cls.constructor then
					table.insert(constructors, 1, cls.constructor)
				end
			end
			for t=1,#constructors do
				inConstructor = true
				local tEnv = {self=privCache[obj] or obj}
				oop.injectEnv(tEnv)
				setmetatable(tEnv, {__index=env})
				setfenv(constructors[t], tEnv)
				local status,tObj = pcall(constructors[t],sanitizeArgs(...))
				if not status then
					error(tObj, 2)
				end
				if tObj == false then
					return false
				elseif tObj then
					obj = publicCache[tObj] or tObj
				end
				inConstructor = false
			end

			objectCache[obj] = class
			objectCache[privObj] = class

			for k,v in pairs(privObj) do
				if type(v) == "table" and v == class.private[k] then
					privObj[k] = instantiate(v)
				end
			end

			return obj
		end,
	}
	
	local iClass
	if inherit then
		iClass = classCache[inherit]
		class.parent = iClass
		setmetatable(class.static, {__index=iClass.static})
		setmetatable(class.public, {
			__index = function(tbl, key)
				if iClass.public[key] ~= nil then
					return iClass.public[key]
				else
					return class.static[key]
				end
			end,
		})
		
		setmetatable(class.private, {
			__index = function(tbl, key)
				if iClass.private[key] ~= nil then
					return iClass.private[key]
				else
					return class.public[key]
				end
			end,
		})
		
		for k,v in pairs(class.properties) do
			setmetatable(v, {__index=iClass.properties[k]})
		end
		
		setmetatable(class, {__index=iClass})
	else
		setmetatable(class.public, {__index=class.static})
		setmetatable(class.private, {__index=class.public})
	end
	
	loopThrough = {"static","private","public"}
	for i,l in ipairs(loopThrough) do
		if classTbl[l] then
			for k,v in pairs(classTbl[l]) do
				if type(k) == "number" then
					if type(v) == "table" and v.name then
						if v.const then
							class.properties.consts[v.name] = true
						end
						if v.type then
							class.properties.types[v.name] = v.type
						end
						if v.value or v.type then
							class[l][v.name] = v.value or default[v.type]
						end
						class.properties[l][v.name] = true -- now values can be generic typed starting with nil but still recognized
					elseif type(v) == "string" then
						class.properties[l][v] = true
					end
				elseif type(k) == "string" then
					if l == "public" and k == name then -- constructor
						if type(v) ~= "function" then
							error("invalid constructor: expected function, got "..type(v),2)
						end
						class.constructor = v
					elseif metamethods[k] then
						class.metatable[l][k] = v
					else
						class[l][k] = v
						class.properties[l][k] = true
					end
				end
			end
		end
	end

	local classObj = {}

	classCache[classObj] = class
	class.obj = classObj

	setmetatable(classObj, class.metatable.static)

	env[name] = classObj
end

function oop.class(name)
	return function(class)
		if classCache[class] then -- a class object was passed meaning we're gonna inherit
			return function(realclass)
				makeClass(name, realclass, class)
			end
		else
			makeClass(name, class)
		end
	end
end

local oType = type
function oop.type(value, numberType)
	if oType(value) == "table" then
		if objectCache[value] then
			return objectCache[value].name
		elseif classCache[value] then
			return "class"
		else
			return "table"
		end
	elseif numberType and oType(value) == "number" then
		return getNumberType(value)
	else
		return oType(value)
	end
end

function oop.getClassName(class)
	oop.expect(1, class, "class")
	return classCache[class].name
end

function oop.isType(value, ...)
	local types = {oop.type(value), oType(value), oop.type(value, true)}
	if types[1] ~= types[2] and types[3] ~= "class" then
		types[3] = "object"
	end
	local allowedTypes = {...}
	for i,t in ipairs(types) do
		for i=1, #allowedTypes do
			if allowedTypes[i] == t or (allowedTypes[i] == "double" and t == "number") then
				return true
			end
		end
	end
	return false
end

function oop.expect(index, value, ...)
	local level = 3
	local allowedTypes = {...}
	if type(allowedTypes[1]) == "number" then
		level = allowedTypes[1]
		table.remove(allowedTypes, 1)
	end
	if type(allowedTypes[1]) ~= "string" then
		error("bad argument #3: expected string", 2)
	end
	if oop.isType(value, ...) then
		return value
	end

	local expectlist
	local numbertype = false
	for t=1,#allowedTypes do
		if allowedTypes[t] == "int" then
			numbertype = true
		end
	end
	if #allowedTypes > 1 then
		local lastType = allowedTypes[#allowedTypes]
		allowedTypes[#allowedTypes] = nil
		expectlist = table.concat(allowedTypes, ", ").." or "..lastType
	else
		expectlist = allowedTypes[1]
	end
	local t = oop.type(value, numbertype)

	error("bad argument #"..index..": expected "..expectlist.." got "..t.." ("..tostring(value)..")", level)
end

function oop.getPublicObject(privateObject)
	return publicCache[privateObject] or privateObject
end

function oop.import(filepath)
	oop.expect(1, filepath, "string")

	local makePackage = require("cc.require")
	local tEnv = setmetatable({shell=shell, multishell=multishell},{__index=env})
	tEnv.require, tEnv.package = makePackage.make(tEnv, dir)
	tEnv.oop = oop

	oop.injectEnv(tEnv)

	local pathlog = filepath
	local filepath2 = filepath..".lua"
	pathlog = pathlog.."\n"..filepath2

	if filepath:sub(1,1) ~= "/" then
		pathlog = pathlog.."\n"..fs.combine(dir, filepath)
		pathlog = pathlog.."\n"..fs.combine(dir, filepath2)
		if fs.exists(fs.combine(dir, filepath)) then
			filepath = fs.combine(dir, filepath)
		elseif fs.exists(fs.combine(dir, filepath2)) then
			filepath = fs.combine(dir, filepath2)
		end
	end

	if fs.exists(filepath2) and not fs.exists(filepath) then
		filepath = filepath2
	end

	if not fs.exists(filepath) then
		error("Could not find file:"..pathlog,2)
	end

	local f = fread(filepath)

	local func, err = load(f, "@"..filepath, nil, tEnv)

	if not func then
		error(err, 2)
	else
		local ok,err = pcall(func)
		if not ok then
			error(err, 2)
		end
	end
end

return oop