local types = require(script.Parent.Parent.modules.types)
local watch = require(script.Parent.watch)

type SystemId = types.SystemId
type SystemSettingData = types.SystemSettingData
type SystemTag = types.SystemTag

type SystemData = types.SystemData
type ProcessingFrame = {
	started_at: number
}
type SystemFrame = types.SystemFrame

local MAX_BUFFER_SIZE = 50

local n = 0
local schedulers = {}

local function unit() end

local function create_scheduler()

	local count = 1
	local frames = 0

	local scheduler = {
		class_name = "Scheduler",
		name = "Scheduler",

		--- contains a map of valid system ids
		valid_system_ids = {} :: {[SystemId]: true},
		--- contains a list of static system data that is updated infrequently
		system_data = {} :: {[SystemId]: SystemData},
		--- list of system data that has updated
		system_data_updated = {} :: {[SystemId]: true},
		--- contains a buffer of the last couple frames of system data that is
		--- refreshed constantly
		system_frames = {} :: {[SystemId]: {SystemFrame}},
		--- stores the frames that have been updated
		system_frames_updated = {} :: {[SystemId]: {[SystemFrame]: true}},
		--- contains the current frame that a system is processing
		processing_frame = {} :: {[SystemId]: ProcessingFrame},
		--- contains a list of watches for each system
		system_watches = {} :: {[SystemId]: {{active: boolean, watch: types.SystemWatch}}}

	}

	local function ENABLE_WATCHES(id: SystemId)
		local watches = scheduler.system_watches[id]
		local cleanup = {}

		for i, system_watch in watches do
			if system_watch.active == false then continue end
			watch.step_watch(system_watch.watch)
			cleanup[i] = watch.track_watch(system_watch.watch)
		end

		return function()
			for _, stop in cleanup do
				stop()
			end
		end
	end

	local function ASSERT_SYSTEM_VALID(id: SystemId)
		assert(scheduler.valid_system_ids[id], `attempt to use unknown system with id #{id}`)
	end

	function scheduler:register_system(settings: types.SystemSettingData?)
		local id = count; count += 1
		scheduler.valid_system_ids[id] = true
		scheduler.system_data[id] = {
			name = "UNNAMED",
			phase = nil,
			layout_order = 0,
			paused = false
		}
		scheduler.system_frames[id] = {}
		scheduler.system_frames_updated[id] = {}

		if settings then
			scheduler:set_system_data(id, settings)
		end

		return id
	end

	function scheduler:set_system_data(id: SystemId, settings: types.SystemSettingData)
		ASSERT_SYSTEM_VALID(id)

		for key, value in settings do
			scheduler.system_data[id][key] = value
		end
		scheduler.system_data_updated[id] = true
	end

	function scheduler:get_system_data(id: SystemId)
		ASSERT_SYSTEM_VALID(id)
		return scheduler.system_data[id]
	end

	function scheduler:remove_system(id: SystemId)
		scheduler.valid_system_ids[id] = nil
		scheduler.system_data[id] = nil
		scheduler.system_frames[id] = nil
		scheduler.system_frames_updated[id] = nil
		scheduler.system_data_updated[id] = true
		scheduler.system_watches[id] = nil
	end

	function scheduler:_mark_system_frame_start(id: SystemId)
		ASSERT_SYSTEM_VALID(id)

		scheduler.processing_frame[id] = {
			started_at = os.clock()
		}
	end

	function scheduler:_mark_system_frame_end(id: SystemId, s: number?)
		ASSERT_SYSTEM_VALID(id)
		local now = os.clock()
		local pending_frame_data = scheduler.processing_frame[id]
		assert(pending_frame_data ~= nil, "no processing frame")
		local frame = {
			i = frames,
			s = now - pending_frame_data.started_at
		}

		frames += 1

		scheduler.processing_frame[id] = nil
		scheduler.system_frames_updated[id][frame] = true
		local last_frame = scheduler.system_frames[id][MAX_BUFFER_SIZE]
		if last_frame then
			scheduler.system_frames_updated[id][last_frame] = nil
		end

		table.insert(scheduler.system_frames[id], 1, frame)
		table.remove(scheduler.system_frames[id], MAX_BUFFER_SIZE + 1)
	end

	function scheduler:append_extra_frame_data(id: SystemId, label: {})
		--todo
		error("todo")
	end

	function scheduler:run<T...>(id: SystemId, system: (T...) -> (), ...: T...)
		ASSERT_SYSTEM_VALID(id)
		local system_data = scheduler.system_data[id]

		if system_data.paused then return end

		local watches = scheduler.system_watches[id]
		local cleanup_watches = unit

		if watches then
			cleanup_watches = ENABLE_WATCHES(id)
		end

		scheduler:_mark_system_frame_start(id)
		system(...)
		scheduler:_mark_system_frame_end(id)

		cleanup_watches()
	end

	function scheduler:create_watch_for_system(id: SystemId)
		ASSERT_SYSTEM_VALID(id)

		local new_watch = watch.create_watch()
		local watch_data
		scheduler.system_watches[id] = scheduler.system_watches[id] or {} :: never

		local function untrack()
			local idx = table.find(scheduler.system_watches[id], watch_data)
			table.remove(scheduler.system_watches[id], idx)
		end

		watch_data = {active = false, watch = new_watch, untrack = untrack}
		table.insert(scheduler.system_watches[id], watch_data)

		return watch_data
	end

	schedulers[n + 1] = scheduler
	n = n + 1

	return scheduler

end

return {

	create = create_scheduler,
	schedulers = schedulers

}


local jecs = require(script.Parent.Parent.Parent.Parent.jecs)
local lon = require(script.Parent.Parent.Parent.modules.lon)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local query_parser = require(script.Parent.Parent.query_parser)

local entity_index_try_get = jecs.entity_index_try_get
local IS_PAIR = jecs.IS_PAIR
local pair = jecs.pair
local pair_first = jecs.pair_first
local pair_second = jecs.pair_second
local empty_table = {}

local function get_all_components(world, entity): {}
	local record = entity_index_try_get(world.entity_index, entity)

	if not record then return empty_table end
	local archetype = record.archetype
	if not archetype then return empty_table end

	local components = {}
	for _, ty in archetype.types do
		table.insert(components, ty)
	end
	return components
end

local function convert_component(world, debug, entity): string
 	if IS_PAIR(entity) then
		local left = convert_component(world, debug, pair_first(world, entity))
		local right = convert_component(world, debug, pair_second(world, entity))
		return `({left}, {right})`
	else
		return world:get(entity, debug) or `${tostring(entity)}`
	end
end

--- Indicates a value is a tag
local TAG = newproxy()
--- Indicates a value is set to nil; this is not allowed in 0.3.0
local BAD_VALUE = newproxy()

local function get_component(ctype_name: string, map_components: {[string]: number})

	local function get_entity(ctype: query_parser.PureComponent)
		local value = ctype.value

		if value.type == "Entity" then
			return value.entity
		elseif value.type == "Name" then
			return map_components[value.name]
		end
		error("bad")
	end

	local entity_to_set
	local parse = query_parser(ctype_name)[1]
	if parse.type == "Component" then
		entity_to_set = get_entity(parse)
	elseif parse.type == "Relationship" then
		local left, right = jecs.Wildcard, jecs.Wildcard

		if parse.left.type == "Component" then
			left = get_entity(parse.left)
		end

		if parse.right.type == "Component" then
			right = get_entity(parse.right)
		end

		entity_to_set = pair(left, right)
	end

	return entity_to_set
end

return function()

	local inspect_entity = queue(remotes.inspect_entity)
	local update_inspect_settings = queue(remotes.update_inspect_settings)
	local stop_inspect_entity = queue(remotes.stop_inspect_entity)
	local update_entity = queue(remotes.update_entity)
	local delete_entity = queue(remotes.delete_entity)

	local validate_entity = queue(remotes.validate_entity_component)

	local inspectors = {}

	return function()

		for incoming, world_id, ctype_name in validate_entity:iter() do
			local world: types.World = public[world_id]
			local outgoing = reverse_connector(incoming)

			if not traffic_check.check_no_wl(incoming.host) then continue end
			if not world or world.class_name ~= "World" then continue end

			local map_components = {}
			for id, name in world.world:query(jecs.Name):iter() do
				map_components[name] = id
			end

			local ok, reason = pcall(get_component, ctype_name, map_components)

			remotes.validate_entity_component_result:fire(
				outgoing, world_id, ctype_name, ok, not ok and reason or nil
			)
		end

		for incoming, world_id, entity, inspect_id in inspect_entity:iter() do
			local world: types.World = public[world_id]
			local outgoing = reverse_connector(incoming)

			if not traffic_check.check_no_wl(incoming.host) then continue end
			if not world or world.class_name ~= "World" then continue end

			inspectors[inspect_id] = {
				outgoing = outgoing,
				world = world,
				entity = entity,
				paused = false,

				new_values = {},
				old_values = {}
			}
		end

		for incoming, inspect_id in stop_inspect_entity:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			inspectors[inspect_id] = nil
		end

		for incoming, inspect_id in delete_entity:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local inspect_data = inspectors[inspect_id]
			local world_data = inspect_data.world
			local world = world_data.world
			local entity = inspect_data.entity

			world:delete(entity)
		end

		for incoming, inspect_id, settings in update_inspect_settings:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local inspect_data = inspectors[inspect_id]
			if not inspect_data then continue end
			inspect_data.paused = settings.paused
		end

		for incoming, inspect_id, changes in update_entity:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local inspect_data = inspectors[inspect_id]
			local world_data = inspect_data.world
			local world = world_data.world
			local entity = inspect_data.entity

			local map_components = {}
			for id, name in world:query(jecs.Name):iter() do
				map_components[name] = id
			end

			for ctype_name, value in changes do

				-- get the component we need to set
				local ok, entity_to_set = pcall(get_component, ctype_name, map_components)

				if not ok then
					warn("attempted to set", ctype_name, "to", value)
					warn(entity_to_set)
					continue
				end

				local ok, result = pcall(lon.parse, value)

				if not ok then
					warn("attempted to set", ctype_name, "to", value)
					warn(result)
					continue
				end

				local ok, data = pcall(
					lon.compile,
					result,
					{tag = TAG}
				)

				if not ok then
					warn("attempted to set", ctype_name, "to", value)
					warn(data)
					continue
				end

				if data == nil then
					world:remove(entity, entity_to_set)
				elseif data == TAG then
					-- tag is handled separately, as its still just "nil"
					-- the difference is that entities used as tags arent allowed to store data
					world:set(entity, entity_to_set)
				else
					world:set(entity, entity_to_set, data)
				end
			end
		end

		for inspect_id, inspector_data in inspectors do
			local world = inspector_data.world.world
			local entity = inspector_data.entity

			if inspector_data.paused then continue end
			if world:contains(entity) == false then continue end

			local new_values = inspector_data.new_values
			local old_values = inspector_data.old_values

			local components = get_all_components(world, entity)

			local function is_tag(id: jecs.Entity<any>)
				return if IS_PAIR(id) then not world:has(jecs.pair_first(world, id), jecs.Component)
					else not world:has(id, jecs.Component)

			end

			for _, component in components do
				local name = convert_component(world, jecs.Name, component)
				local is_tag = is_tag(component)

				if is_tag then
					new_values[name] = TAG
				else
					local value = world:get(entity, component)
					new_values[name] = if value == nil then BAD_VALUE else value
				end
			end

			for name, new_value in new_values do
				local old_value = old_values[name]

				if old_value == new_value and typeof(new_value) ~= "table" then continue end
				remotes.inspect_entity_update:fire(
					inspector_data.outgoing,
					inspect_id,
					name,
					if new_value == TAG then "tag"
					--todo: figure out a better way to say that you are not allowed to store nil in a component
					elseif new_value == BAD_VALUE then "nil (not allowed)"
					else lon.output(new_value, true)
				)
			end

			for name, value in old_values do
				local new_value = new_values[name]

				if new_value ~= nil then continue end
				remotes.inspect_entity_update:fire(
					inspector_data.outgoing,
					inspect_id,
					name,
					nil
				)
			end

			table.clear(old_values)

			inspector_data.new_values = old_values
			inspector_data.old_values = new_values
		end
	end
end


local jecs = require(script.Parent.Parent.Parent.Parent.jecs)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)

local entity_index_try_get = jecs.entity_index_try_get
local IS_PAIR = jecs.IS_PAIR
local pair_first = jecs.pair_first
local pair_second = jecs.pair_second
local empty_table = {}

local function convert_component(world, debug, entity): string
	if IS_PAIR(entity) then
	   local left = convert_component(world, debug, pair_first(world, entity))
	   local right = convert_component(world, debug, pair_second(world, entity))
	   return `({left}, {right})`
   else
	   return world:get(entity, debug) or `${tostring(entity)}`
   end
end

local function get_type(t: { [any]: any }): string
	local key, value = next(t)
	if key == nil then return "" end
	return `[{typeof(key)}]: {typeof(value)}`
end

local function get_string_keys(t: { [any]: any }): ({ string }, boolean)
	local keys = {}
	local i = 0
	for key in t do
		if i > 3 then return keys, true end
		if typeof(key) ~= "string" then continue end
		table.insert(keys, key)
		i += 1
	end
	return keys, false
end

local function is_tag(world: jecs.World, id: jecs.Entity<any>)
	return not
		if IS_PAIR(id) then
			world:has(pair_first(world, id), jecs.Component) or
			world:has(pair_second(world, id), jecs.Component)
		else
			world:has(id, jecs.Component)
end

local function get_all_components(world, entity): {}
	local record = entity_index_try_get(world.entity_index, entity)

	if not record then return empty_table end
	local archetype = record.archetype
	if not archetype then return empty_table end

	local components = {}
	for _, ty in archetype.types do
		table.insert(components, ty)
	end

	table.sort(components, function(a, b)
		return if is_tag(world, a) and is_tag(world, b) then a < b
			elseif is_tag(world, a) then true
			elseif is_tag(world, b) then false
			else a < b
	end)

	return components
end

local function obtain_string(entity: jecs.Entity<any>, world: jecs.World)
	local MAX_SIZE = 840
	local has_more = false
	local entity_name = world:get(entity, jecs.Name)
	local strings = {`<b>{if entity_name then `{entity_name} #` else "#"}{entity}</b>\n`}
	local n = #strings[1]

	for _, id in get_all_components(world, entity) do
		if id == jecs.Name then continue end
		local name = convert_component(world, jecs.Name, id)
		local value = if is_tag(world, id) then nil else world:get(entity, id)
		local to_append

		if typeof(value) == "table" then
			local string_keys = get_string_keys(value)

			if #string_keys > 0 then
				local temp_b = {`<b>{name}</b>:`}
				local temp_n = #temp_b[1]

				for key, value in pairs(value) do
					if #temp_b > 0 then
						table.insert(temp_b, "\n")
					end

					local str_of_v = if type(value) == "string" then `{value}`
						elseif typeof(value) == "table" then get_type(value)
						else tostring(value)
					if #str_of_v > 32 then
						str_of_v = `{string.sub(str_of_v, 1, 30)}..`
					end

					local str = `{key}: {str_of_v}`

					if temp_n + #str + 2 > 32 then
						table.insert(temp_b, "...")
						break
					else
						table.insert(temp_b, str)
					end
				end

				to_append = `{table.concat(temp_b)}`
			else
				to_append = `<b>{name}</b>: {get_type(value)}`
			end
		elseif is_tag(world, id) then
			to_append = `<b>{name}</b>`
		else
			local value = tostring(value)
			if #value > 32 then
				to_append = `<b>{name}</b>: {string.sub(value, 1, 30)}..`
			else
				to_append = `<b>{name}</b>: {value}`
			end
		end

		if MAX_SIZE > n + #to_append then
			n += #to_append
			table.insert(strings, to_append)
		else
			has_more = true
		end
	end

	local str = table.concat(strings, "\n")
	if has_more then str = str .. "..." end

	return str
end

return function()

	local send_mouse_pointer = queue(remotes.send_mouse_pointer)

	return function()

		for incoming, world_id, pos, dir in send_mouse_pointer:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local world_data: types.World = public[world_id]
			local world = world_data.world
			local outgoing = reverse_connector(incoming)

			if world_data.entities == nil and world_data.get_entity_from_part == nil then continue end
			if not world_data or world_data.class_name ~= "World" then continue end

			local result = workspace:Raycast(pos, dir * 1000)

			if not result then
				remotes.send_mouse_entity:fire(
					outgoing,
					world_id
				)
				continue
			end

			local part = result.Instance
			local entity

			-- no way to obtain the entity
			if world_data.get_entity_from_part == nil and world_data.entities == nil then
				remotes.send_mouse_entity:fire(
					outgoing,
					world_id
				)
				continue
			end

			if world_data.get_entity_from_part == nil then
				entity = world_data.entities[part]

				while entity == nil and part.Parent ~= game do
					part, entity = part.Parent, world_data.entities[part]
				end
			else
				entity, part = world_data.get_entity_from_part(part)
			end

			if not entity then
				remotes.send_mouse_entity:fire(
					outgoing,
					world_id
				)
				continue
			end

			local str = obtain_string(entity, world, jecs.Name)

			remotes.send_mouse_entity:fire(
				outgoing,
				world_id,
				part,
				entity,
				str
			)

		end

	end
end


local Players = game:GetService("Players")

local net = require(script.Parent.Parent.Parent.modules.net)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)

return function()

	local ping = queue(remotes.ping)

	for _, player in Players:GetPlayers() do
		if traffic_check.communication_is_allowed(net.local_host, player, true) then
			remotes.new_server_registered:fire({
				host = player,
			})
		end
	end

	return function()
		for connector in ping:iter() do
			local outgoing = reverse_connector(connector)
			remotes.new_server_registered:fire(outgoing)
		end
	end
end


local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local public = require(script.Parent.Parent.public)

return function()

	local connected = {}

	local bind_to_core = queue(remotes.bind_to_server_core)

	return function()

		for connector in bind_to_core:iter() do
			local outgoing = reverse_connector(connector)

			if not traffic_check.check_no_wl(connector.host) then continue end
			-- print("help")

			table.insert(connected, outgoing)
			local schedulers = {}
			local worlds = {}

			for idx, data in ipairs(public) do

				if data.class_name == "Scheduler" then
					table.insert(schedulers, {
						name = data.name :: string,
						id = idx
					})
				elseif data.class_name == "World" then
					table.insert(worlds, {
						name = data.name :: string,
						id = idx
					})
				end

			end

			remotes.update_server_data:fire(outgoing, {
				schedulers = schedulers,
				worlds = worlds
			})
		end

		if public.updated == false then return end
		public.updated = false

		local schedulers = {}
		local worlds = {}

		for idx, data in ipairs(public) do

			if data.class_name == "Scheduler" then
				table.insert(schedulers, {
					name = data.name :: string,
					id = idx
				})
			elseif data.class_name == "World" then
				table.insert(worlds, {
					name = data.name :: string,
					id = idx
				})
			end

		end

		local fired_to = {}
		for _, connector in connected do
			if fired_to[connector] then return end
			fired_to[connector] = true
			remotes.update_server_data:fire(connector, {
				schedulers = schedulers,
				worlds = worlds
			})
		end

	end
end


local jecs = require(script.Parent.Parent.Parent.Parent.jecs)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local query_parser = require(script.Parent.Parent.query_parser)

type Connection = {

	outgoing: types.OutgoingConnector,
	query_id: number,
	frame: number,

	paused: boolean,
	refresh: boolean,

	world: types.World,

	include: {jecs.Entity<any>},
	exclude: {jecs.Entity<any>},
	with: {jecs.Entity<any>},

	new_columns: {{any}},
	old_columns: {{any}},

	from: number,
	upto: number

}

local NIL = newproxy() -- NULL is displayed if the value exists, buth as no value

local function clear_columns(columns: {{any}})
	for _, column in columns do
		local name = column[1]
		table.clear(column)
		column[1] = name
		assert(column[1] == name)
	end
	return columns
end

local function reverse_columns(columns: {{any}}, size: number)
	for _, column in columns do
		for i = 0, size // 2 - 1 do
			column[i + 2], column[(size + 1) - i] = column[(size + 1) - i], column[i + 2]
		end
	end
	return columns
end

return function()

	local processing_queries: {[number]: Connection} = {}

	local validate_query = queue(remotes.validate_query)
	local request_query = queue(remotes.request_query)
	local disconnect_query = queue(remotes.disconnect_query)
	local advance_query_page = queue(remotes.advance_query_page)
	local pause_query = queue(remotes.pause_query)
	local refresh_query = queue(remotes.refresh_results)

	local function check_if_query_valid(world: types.World, query: string): (boolean, string)
		local map_components = {}
		local ok, result = pcall(query_parser, query)
		local msg = nil

		if not ok then
			return ok, result :: any
		end

		for id, name in world.world:query(jecs.Name):iter() do
			map_components[name] = id
		end

		local total_to_query = 0

		for _, ctype in result do
			if not ok then break end

			if ctype.query and not ctype.exclude then
				total_to_query += 1
			end

			if ctype.type == "Component" then
				if ctype.value.type == "Entity" then
					if world.world:contains(ctype.value.entity) then continue end
					return false, "entity does not exist"
				elseif ctype.value.type == "Name" then
					if map_components[ctype.value.name] then continue end
					return false, `unknown component called {ctype.value.name}`
				else
					return false, "what"
				end
			elseif ctype.type == "Relationship" then
				local both_wildcard = ctype.left.type == "Wildcard" and ctype.right.type == "Wildcard"
				if both_wildcard then
					return false, `(*, *) is not a valid relationship`
				end

				local left = ctype.left
				local right = ctype.right

				if left.type == "Component" then
					if left.value.type == "Entity" then
						if world.world:contains(left.value.entity) then continue end
						return false, "entity does not exist"
					elseif left.value.type == "Name" then
						if map_components[left.value.name] then continue end
						return false, `unknown component called {left.value.name}`
					else
						return false, "what"
					end
				end

				if right.type == "Component" then
					if right.value.type == "Entity" then
						if world.world:contains(right.value.entity) then continue end
						return false, "entity does not exist"
					elseif right.value.type == "Name" then
						if map_components[right.value.name] then continue end
						return false, `unknown component called {right.value.name}`
					else
						return false, "what"
					end
				end
			end
		end

		if total_to_query > 26 then
			warn("attempting to observe too many values")
			return false, "attempting to observe too many entities"
		end

		return ok, msg
	end

	--fixme: contains is missing from types
	local function check_if_still_valid(world: any, entities: {any})
		for _, id in entities do
			if jecs.IS_PAIR(id) then
				if not (world:contains(jecs.pair_first(world, id) and jecs.pair_second(world, id))) then
					return false
				end
			elseif not world:contains(id) then
				return false
			end
		end
		return true
	end

	return function()

		for incoming, world_id, query in validate_query:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end

			local world: types.World = public[world_id]
			local outgoing = reverse_connector(incoming)

			if not world or world.class_name ~= "World" then
				remotes.validate_result:fire(outgoing, world_id, query, false, "world does not exist")
				continue
			end

			local ok, message = check_if_query_valid(world, query)
			remotes.validate_result:fire(outgoing, world_id, query, ok, message)
		end

		for incoming, query_id in disconnect_query:iter() do
			processing_queries[query_id] = nil
		end

		for incoming, world_id, query_id, query in request_query:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end

			local world: types.World = public[world_id]
			local outgoing = reverse_connector(incoming)

			if not world or world.class_name ~= "World" then continue end

			local ok = check_if_query_valid(world, query)

			if not ok then continue end

			local result = query_parser(query)
			local include = {}
			local exclude = {}
			local with = {}

			local new_columns = {}
			local old_columns = {}

			table.insert(new_columns, {})
			table.insert(old_columns, {})

			local map_components = {}
			local map_entity: {[any]: any} = {}
			for id, name in world.world:query(jecs.Name):iter() do
				map_components[name] = id
			end

			local function get_entity(ctype: query_parser.PureComponent)
				local value = ctype.value

				if value.type == "Entity" then
					return value.entity
				elseif value.type == "Name" then
					return map_components[value.name]
				end
				error("bad")
			end

			for _, ctype in result do
				if ctype.type == "Component" then
					map_entity[ctype] = get_entity(ctype)
				elseif ctype.type == "Relationship" then
					local left, right = jecs.Wildcard, jecs.Wildcard

					if ctype.left.type == "Component" then
						left = get_entity(ctype.left)
					end

					if ctype.right.type == "Component" then
						right = get_entity(ctype.right)
					end

					local pair = jecs.pair(left, right)
					map_entity[ctype] = pair
				end
			end

			for _, ctype in result do
				local entity = map_entity[ctype]
				if ctype.exclude then
					table.insert(exclude, entity)
				elseif ctype.query then
					-- local name = if ctype.type == "Component" then ctype.name else `({ctype.left.name}, {ctype.right.name})`
					table.insert(include, entity)
					table.insert(new_columns, {})
					table.insert(old_columns, {})
				else
					table.insert(with, entity)
				end
			end

			if processing_queries[query_id] then
				local connection = processing_queries[query_id]

				connection.outgoing = outgoing
				connection.query_id = query_id
				connection.world = world
				connection.refresh = true

				connection.include = include
				connection.exclude = exclude
				connection.with = with

				connection.new_columns = new_columns
				connection.old_columns = old_columns
				connection.from = 1
				connection.upto = 25
			else
				local connection: Connection = {

					outgoing = outgoing,
					query_id = query_id,
					frame = 0,

					world = world,

					paused = false,
					refresh = false,

					include = include,
					exclude = exclude,
					with = with,

					new_columns = new_columns,
					old_columns = old_columns,

					from = 1,
					upto = 25
				}

				processing_queries[query_id] = connection
			end
		end

		for incoming, query_id in refresh_query:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local query = processing_queries[query_id]
			if not query then continue end
			query.refresh = true
		end

		for incoming, query_id, state in pause_query:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local query = processing_queries[query_id]
			if not query then continue end
			query.paused = state
		end

		for incoming, query_id, from, to in advance_query_page:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local query = processing_queries[query_id]
			if not query then continue end

			query.refresh = true
			query.from = from
			query.upto = to
		end

		for query_id, query_data in processing_queries do
			if query_data.paused and query_data.refresh ~= true then continue end
			debug.profilebegin("process query")
			query_data.refresh = false
			local world_data = query_data.world
			local world = world_data.world
			local debug_trait = jecs.Name

			if
				not (check_if_still_valid(world, query_data.include)
				and check_if_still_valid(world, query_data.exclude)
				and check_if_still_valid(world, query_data.with))
			then
				-- query is no longer valid!
				--todo: query is invalid, notify the client about this
				debug.profileend()
				continue
			end

			local query = world:query(unpack(query_data.include))

			if #query_data.exclude > 0 then
				query = query:without(unpack(query_data.exclude))
			end

			if #query_data.with > 0 then
				query = query:with(unpack(query_data.with))
			end

			local from = query_data.from
			local upto = query_data.upto

			local new_columns = query_data.new_columns
			local old_columns = query_data.old_columns
			-- set the names of each column
			--todo: fix type
			local function get_name(entity: any)
				if jecs.IS_PAIR(entity) then
					local left = jecs.pair_first(world, entity)
					local right = jecs.pair_second(world, entity)
					return `({get_name(left)}, {get_name(right)})`
				elseif entity == jecs.Wildcard :: any then
					return "*"
				elseif world:has(entity, debug_trait) then
					return world:get(entity, debug_trait)
				else
					return `${entity}`
				end
			end

			-- set column names
			for index, column in new_columns do
				local e = query_data.include[index - 1]

				if e then
					column[1] = get_name(e)
				else
					column[1] = "id"
				end
			end

			-- process the data into columns
			-- we inline the query here, as jecs queries are in reverse to prevent iterator invalidation
			-- this is usually fine, but it's annoying, as now entities are added to the first page.
			--todo: pause button
			local total_entities = 0
			local archetypes = query:archetypes()

			for _, archetype: jecs.Archetype in archetypes do
				total_entities += #archetype.entities
			end

			local entities = table.create(total_entities)
			local at = total_entities
			local row_entity = 1

			for _, archetype: jecs.Archetype in archetypes do
				for row = #archetype.entities, 1, -1 do
					local entity = archetype.entities[row]
					table.insert(entities, entity)
				end
			end

			table.sort(entities)

			for i = from, upto do
				row_entity += 1
				local entity = entities[i]
				if not entity then continue end
				new_columns[1][row_entity] = entity
				for idx, ctype in query_data.include do
					local value = world:get(entity, ctype)
					new_columns[idx + 1][row_entity] = if value == nil then NIL else value
				end
			end

			--- reverse the order of each array

			remotes.count_total_entities:fire(
				query_data.outgoing,
				query_id,
				total_entities
			)

			-- diff the columns and replicate any new values
			for column = 1, math.max(#new_columns, #old_columns) do
				for row = 1, upto do
					local new_value = new_columns[column][row]
					local old_value = old_columns[column][row]

					if new_value ~= old_value or typeof(new_value) == "table" then
						-- todo: improve replication of the new value
						-- ideally, we would figure out if the value is a certain type and needs special replication
						-- if we for example determine a value is a string, or table, we cap it at MAX_CHARACTERS
						-- or we tostring a couple keys of the table until we reach MAX_CHARACTERS.
						-- we wanna be able to replicate every single. value

						local MAX_CHARS = 750
						local str

						if typeof(new_value) == "string" then
							str = `"{string.sub(new_value, 1, MAX_CHARS-2)}"`
						elseif typeof(new_value) == "table" then
							local temp_n = 0
							local temp_b = {}

							for key, value in new_value do
								if #temp_b > 0 then
									table.insert(temp_b, "; ")
								end

								local str_of_v = if type(value) == "string" then `"{value}"` else tostring(value)
								local str = `{key}: {str_of_v}`

								if temp_n + #str + 2 > MAX_CHARS then
									table.insert(temp_b, "...")
									break
								else
									table.insert(temp_b, str)
								end
							end

							str = `\{{table.concat(temp_b)}\}`
						elseif new_value == NIL then
							str = "" -- important distinction, this is still a valid component
						elseif new_value == nil then
							str = nil -- but this isnt
						else
							str = string.sub(tostring(new_value), 1, MAX_CHARS-2)
						end

						if row == 1 then str = new_value end
						remotes.update_query_result:fire(
							query_data.outgoing,
							query_id,
							query_data.frame,
							column,
							row,
							str
						)
					end
				end
			end

			query_data.new_columns = clear_columns(old_columns)
			query_data.old_columns = new_columns
			query_data.frame += 1
			debug.profileend()
		end

	end

end


--!nolint LocalShadow
local hash = require(script.Parent.Parent.Parent.modules.hash_connector)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)

return function()

	local connected = {}

	local request_scheduler = queue(remotes.request_scheduler)
	local disconnect_scheduler = queue(remotes.disconnect_scheduler)
	local schedule_pause = queue(remotes.scheduler_system_pause)

	return function()

		for incoming, id in request_scheduler:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end

			local scheduler = public[id]

			if scheduler.class_name ~= "Scheduler" then continue end
			if scheduler == nil then continue end

			local outgoing = reverse_connector(incoming)
			connected[id] = connected[id] or {}
			table.insert(connected[id], outgoing)
			-- print("connected")

			for system_id, data in scheduler.system_data do
				remotes.scheduler_system_static_update:fire(outgoing, id, system_id, data)
			end

			for system_id, frames in scheduler.system_frames do
				local frame = frames[1]
				if not frame then continue end
				remotes.scheduler_system_update:fire(outgoing, id, system_id, frame.i, frame.s)
			end
		end

		for incoming, id in disconnect_scheduler:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end

			if not connected[id] then continue end

			local scheduler_connected = connected[id]

			for i = #scheduler_connected, 1, -1 do
				local connector = scheduler_connected[i]
				if connector.host ~= incoming.host then continue end
				if connector.to_vm ~= incoming.from_vm then continue end
				scheduler_connected[i] = scheduler_connected[#scheduler_connected]
				scheduler_connected[#scheduler_connected] = nil
				break
			end
		end

		for incoming, id, system, paused in schedule_pause:iter() do
			if not traffic_check.check_no_wl(incoming.host) then continue end
			local scheduler: types.Scheduler = public[id]

			if not scheduler then return end

			scheduler:set_system_data(system, {
				paused = paused
			})
		end

		for id, connected in connected do
			local scheduler: types.Scheduler = public[id]
			if #connected == 0 then continue end

			for system_id in scheduler.system_data_updated do
				local map = {}
				local data = scheduler.system_data[system_id]

				for _, connector in connected do
					if map[hash(connector)] then continue end
					map[hash(connector)] = true
					remotes.scheduler_system_static_update:fire(connector, id, system_id, data)
				end

				scheduler.system_data_updated[system_id] = nil
			end

			for system_id, frames in scheduler.system_frames_updated do
				local map = {}
				for frame in frames do
					for _, connector in connected do
						if map[hash(connector)] then continue end
						map[hash(connector)] = true
						remotes.scheduler_system_update:fire(connector, id, system_id, frame.i, frame.s)
					end
				end
				table.clear(frames)
			end

		end

	end
end


--[[

Handles the API for the system watch.
Users will be able to add watches to their systems to track changes.

Users will be able to learn about what actions a system performs on a jecs world
through this.

Hooked API's:

component()
entity()
remove()
clear()
delete()
add()
set()

]]

local jecs = require(script.Parent.Parent.Parent.Parent.jecs)
local hash_connector = require(script.Parent.Parent.Parent.modules.hash_connector)
local lon = require(script.Parent.Parent.Parent.modules.lon)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local watch = require(script.Parent.Parent.watch)

local NIL = watch.NIL

return function()

	local stored_watches = {}
	local connected_watches = {}

	local function create_watch_for_id(
		scheduler: types.Scheduler,
		system: types.SystemId,
		watch_id: number
	)
		local watch = scheduler:create_watch_for_system(system)
		stored_watches[watch_id] = watch
	end

	local function send_watch_data_to(host: types.OutgoingConnector, watch_id: number, frame: number)
		local map_worlds_to_name = {}
		local watch = stored_watches[watch_id]
		local frames = watch.watch.frames
		local data = frames[frame]

		if not data then
			remotes.update_watch_data:fire(host, watch_id, frame, nil)
			return
		end

		for _, world in ipairs(public) do
			if world.world == nil then continue end
			map_worlds_to_name[world.world] = jecs.Name
		end

		local to_send = {
			types = data.types,
			entities = data.entities,
			component = table.clone(data.component),
			values = table.clone(data.values)
		}

		for idx, ctype in to_send.component do
			local world = data.worlds[idx]
			to_send.component[idx] = world:get(ctype, map_worlds_to_name[world]) or ctype
		end

		for idx, value in to_send.values do
			if value == NIL then to_send.values[idx] = "" end
			to_send.values[idx] = lon.output(value, false)
		end

		remotes.update_watch_data:fire(host, watch_id, frame, to_send :: any)
	end

	local function remove_watch_id(watch_id: number)
		if not stored_watches[watch_id] then return end
		stored_watches[watch_id].untrack()
		stored_watches[watch_id] = nil
		connected_watches[watch_id] = nil
	end

	local function start_record_watch(watch_id: number)
		local watch = stored_watches[watch_id]
		watch.active = true
	end

	local function stop_record_watch(watch_id: number)
		local watch = stored_watches[watch_id]
		watch.active = false
	end

	local function connect_watch(host: types.OutgoingConnector, watch_id: number)
		connected_watches[watch_id] = connected_watches[watch_id] or {}
		connected_watches[watch_id][hash_connector(host)] = host

		local watch = stored_watches[watch_id]
		for i, frame in watch.watch.frames do
			remotes.update_overview:fire(host, watch_id, i, #frame.types)
		end
	end

	local function disconnect_watch(host: types.OutgoingConnector, watch_id: number)
		if not connected_watches[watch_id] then return end
		connected_watches[watch_id][hash_connector(host)] = nil
	end

	local request_create_watch = queue(remotes.create_watch)
	local request_remove_watch = queue(remotes.remove_watch)
	local request_watch_data = queue(remotes.request_watch_data)
	local request_stop_watch = queue(remotes.stop_watch)
	local request_record_watch = queue(remotes.start_record_watch)
	local request_connect_watch = queue(remotes.connect_watch)
	local request_disconnect_watch = queue(remotes.disconnect_watch)
	-- local set_lon_enabled = queue(remotes.set_lon_enabled)

	return function()

		for from, scheduler_id, system, watch_id in request_create_watch:iter() do
			local scheduler = public[scheduler_id]

			if scheduler.class_name ~= "Scheduler" then continue end
			if scheduler == nil then continue end

			create_watch_for_id(scheduler, system, watch_id)
		end

		for from, watch_id in request_stop_watch:iter() do
			stop_record_watch(watch_id)
		end

		for from, watch_id in request_remove_watch:iter() do
			remove_watch_id(watch_id)
		end

		for from, watch_id, frame in request_watch_data:iter() do
			send_watch_data_to(
				reverse_connector(from),
				watch_id,
				frame
			)
		end

		for from, watch_id in request_record_watch:iter() do
			start_record_watch(watch_id)
		end

		for from, watch_id in request_connect_watch:iter() do
			connect_watch(reverse_connector(from), watch_id)
		end

		for from, watch_id in request_disconnect_watch:iter() do
			disconnect_watch(from, watch_id)
		end

		for watch_id, hosts in connected_watches do
			local watch = stored_watches[watch_id]
			local current_frame = watch.watch.frame
			local frame_data = watch.watch.frames[current_frame] or {types = {}}
			local changes = #frame_data.types

			for _, host in hosts do
				remotes.update_overview:fire(host, watch_id, current_frame, changes)
			end
		end

	end
end


local types = require(script.Parent.Parent.modules.types)
local world_hook = require(script.Parent.world_hook)

local NIL = newproxy()

type ChangeTypes = "remove" | "clear" | "delete" | "add" | "set" | "entity" | "component"
type Changes = types.WatchLoggedChanges

export type SystemWatch = {
	--- enables Lua Object Notation.
	--- incurs a significant performance penalty.
	enable_lon: boolean,
	--- the current frame to process
	frame: number,

	frames: {[number]: Changes}
}

local function create_changes()
	return {
		types = {},
		entities = {},
		component = {},
		values = {},
		worlds = {}
	}
end

local function step_watch(watch: SystemWatch)
	watch.frame += 1
	watch.frames[watch.frame] = create_changes()
end

local function track_watch(watch: SystemWatch)

	local hooks = {

		world_hook.hook_onto("remove", function(self, id, component)
			local frame = watch.frames[watch.frame]

			table.insert(frame.types, "remove")
			table.insert(frame.entities, id)
			table.insert(frame.component, component)
			table.insert(frame.values, NIL)
			table.insert(frame.worlds, self)
		end),

		world_hook.hook_onto("clear", function(self, id)
			local frame = watch.frames[watch.frame]

			table.insert(frame.types, "clear")
			table.insert(frame.entities, id)
			table.insert(frame.component, NIL)
			table.insert(frame.values, NIL)
			table.insert(frame.worlds, self)
		end),

		world_hook.hook_onto("delete", function(self, id)
			local frame = watch.frames[watch.frame]

			table.insert(frame.types, "delete")
			table.insert(frame.entities, id)
			table.insert(frame.component, NIL)
			table.insert(frame.values, NIL)
			table.insert(frame.worlds, self)
		end),

		world_hook.hook_onto("add", function(self, id, component)
			local frame = watch.frames[watch.frame]

			table.insert(frame.types, "add")
			table.insert(frame.entities, id)
			table.insert(frame.component, component)
			table.insert(frame.values, NIL)
			table.insert(frame.worlds, self)
		end),

		world_hook.hook_onto("set", function(self, entity, component, value)
			if self:has(entity, component) then
				local frame = watch.frames[watch.frame]

				table.insert(frame.types, "change")
				table.insert(frame.entities, entity)
				table.insert(frame.component, component)
				table.insert(frame.values, value)
				table.insert(frame.worlds, self)
			else
				local frame = watch.frames[watch.frame]

				table.insert(frame.types, "move")
				table.insert(frame.entities, entity)
				table.insert(frame.component, component)
				table.insert(frame.values, value)
				table.insert(frame.worlds, self)
			end
		end)

	}

	--- stops all hooks
	local function stop_hook()
		for _, destroy in hooks do
			destroy()
		end
	end

	return stop_hook
end

local function create_watch()
	local watch: SystemWatch = {
		enable_lon = false,

		frame = 0,
		frames = {}
	}

	return watch
end

return {

	create_watch = create_watch,
	track_watch = track_watch,
	step_watch = step_watch,

	NIL = NIL

}


local jecs = require(script.Parent.Parent.Parent.jecs)
local world = jecs.World.new()

local function i_hook_onto(key: string, hooks: {(...any) -> ()})

	local method: any = world[key]

	assert(typeof(method) == "function", "can only hook onto functions")

	-- create a new wrapper function
	local function run_hook(...)
		for _, hook in hooks do
			hook(...)
		end
		return method(...)
	end

	-- print(debug.info(world[key], "s"))
	world.__index[key] = run_hook

	local function unhook()
		world.__index[key] = method
	end

	return unhook
end

local hooks = {}

local function find_swap_pop<T>(list: {T}, value: T)
	local idx = table.find(list, value)
	if not idx then return end
	list[idx] = list[#list]
	list[#list] = nil
end

local function hook_onto(key: string, hook: (...any) -> ())

	if hooks[key] == nil then
		local callbacks = {}
		local cleanup = i_hook_onto(key, callbacks)
		hooks[key] = {
			cleanup = cleanup,
			callbacks = callbacks
		}
	end

	local hook_info = hooks[key]
	local dead = false
	table.insert(hook_info.callbacks, hook)

	local function unhook()
		if dead then return end
		dead = true
		find_swap_pop(hook_info.callbacks, hook)

		if hook_info.callbacks[1] == nil then
			hook_info.cleanup()
			hooks[key] = nil
		end
	end

	return unhook
end

return {
	hook_onto = hook_onto
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local start = require(ReplicatedStorage.std.start)

start(script.Parent:WaitForChild("systems"):GetChildren())


--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std       = ReplicatedStorage.std
local world     = require(std.world)
local cts       = require(std.components)
local scheduler = require(std.scheduler)
local phases    = require(std.phases)

local Mob        = cts.Mob
local Transform  = cts.Transform
local Locomotion = cts.Locomotion
local Size       = cts.Size

-- mobs we want to steer (need locomotion)
local moving = world
    :query(Transform, Locomotion, Size)
    :with(Mob)
    :cached()

-- neighbors pool (all mobs with positions + size)
local neighbors = world
    :query(Transform, Size)
    :with(Mob)
    :cached()

local EPS = 1e-4

-- Tunables:
-- Size is HitboxSize.Magnitude, so it's "overall scale".
--  radiusFactor converts that into a collision radius in studs.
local RADIUS_FACTOR       = 0.25  -- tweak: bigger -> they avoid from farther away
local SEPARATION_STRENGTH = 8     -- how aggressively to push

local SEP_HZ   = 10
local SEP_TICK = 1 / SEP_HZ
local sepAcc   = 0

local function separationStep(dt: number)
    for e, transform, loco, sizeSelf in moving do
        local pos = transform.new.Position
        local baseDir = loco.dir

        local selfRadius = (sizeSelf or 0) * RADIUS_FACTOR
        local separation = Vector3.zero

        for other, otherTransform, sizeOther in neighbors do
            if other == e then
                continue
            end

            local otherPos = otherTransform.new.Position
            local offset = pos - otherPos
            local dist = offset.Magnitude
            if dist < EPS then
                continue
            end

            local otherRadius = (sizeOther or 0) * RADIUS_FACTOR
            local targetDist = selfRadius + otherRadius

            if dist < targetDist and targetDist > EPS then
                -- how deep we are inside each other's "bubble"
                local penetration = targetDist - dist
                local pushStrength = penetration / targetDist -- [0..1]

                separation += offset.Unit * pushStrength
            end
        end

        if separation.Magnitude > EPS then
            local sepDir = separation.Unit

            -- blend separation into current direction
            local combined = baseDir + sepDir * (SEPARATION_STRENGTH * dt)
            if combined.Magnitude > EPS then
                combined = combined.Unit
            end

            loco.dir = combined
            world:set(e, Locomotion, loco)
        end
    end
end

local function sepWrapper(dt: number)
    sepAcc += dt
    if sepAcc < SEP_TICK then return end

    local step_dt = sepAcc
    sepAcc = 0
    separationStep(step_dt)
end

-- Runs after AIBehavior (brain), before AIMovement (movement)
scheduler.SYSTEM(sepWrapper, phases.AIStateMachine)

return 0


-- ServerScriptService/systems/mob_move.luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace    = game:GetService("Workspace")

local std       = ReplicatedStorage.std
local world     = require(std.world)
local cts       = require(std.components)
local scheduler = require(std.scheduler)
local phases    = require(std.phases)

local Mob        = cts.Mob
local Transform  = cts.Transform
local Locomotion = cts.Locomotion
local Hitbox     = cts.Hitbox

local moving_mobs = world
    :query(Transform, Locomotion, Hitbox)
    :with(Mob)
    :cached()

local MIN_REPL_DELTA = 0.05

local EPS = 1e-4
local GROUND_CHECK_DIST = 30

local GROUND_ROOT = workspace:WaitForChild("Map")

local function makeRayParams(ignore: {Instance}): RaycastParams
    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Include
    params.FilterDescendantsInstances = { GROUND_ROOT, workspace.Terrain }
    return params
end

local function mobMoveStep(dt: number)
    for _e, transform, loco, hitbox in moving_mobs do
        local cf = transform.new
        local pos = cf.Position

        local dir = loco.dir
        local speed = loco.speed or 0

        -- horizontal only
        dir = Vector3.new(dir.X, 0, dir.Z)
        if dir.Magnitude > EPS and speed > 0 then
            dir = dir.Unit
        else
            dir = Vector3.zero
            speed = 0
        end

        local move = dir * speed * dt
        local desiredPos = pos + move

        local params = makeRayParams({ hitbox })

        -- 1) forward wall check (optional, but good to keep)
        if move.Magnitude > EPS then
            local forwardResult = Workspace:Raycast(
                pos + Vector3.new(0, hitbox.Size.Y * 0.5, 0),
                move,
                params
            )

            if forwardResult then
                local hitDist = (forwardResult.Position - pos).Magnitude
                if hitDist > 1 then
                    desiredPos = pos + dir * (hitDist - 1)
                else
                    desiredPos = pos -- blocked immediately
                end
            end
        end

        -- 2) Edge / ground check UNDER desiredPos
        local groundResult = Workspace:Raycast(
            desiredPos + Vector3.new(0, GROUND_CHECK_DIST * 0.5, 0),
            Vector3.new(0, -GROUND_CHECK_DIST, 0),
            params
        )

        local halfHeight = hitbox.Size.Y * 0.5
        local stepUpLimit = pos.Y + math.min(halfHeight, 3)

         if groundResult then
            local hitY = groundResult.Position.Y

            if hitY <= stepUpLimit then
                desiredPos = Vector3.new(
                    desiredPos.X,
                    hitY + halfHeight,
                    desiredPos.Z
                )
            else
                -- treat as ceiling, keep current Y
                desiredPos = Vector3.new(
                    desiredPos.X,
                    pos.Y,
                    desiredPos.Z
                )
            end
        else
            -- no ground under new step -> edge
            desiredPos = pos
        end

        pos = desiredPos

        -- 3) Facing direction (if idle, keep old forward)
        local forward = if dir.Magnitude > EPS
            then dir
            else cf.LookVector

        local oldPos = transform.new.Position
        local newCF = CFrame.new(pos, pos + forward)

        transform.new = newCF

        if (pos - oldPos).Magnitude > MIN_REPL_DELTA then
            hitbox.CFrame = newCF
        end
    end
end

local MOVE_HZ = 20          -- 20 Hz movement
local MOVE_TICK = 1 / MOVE_HZ
local moveAcc = 0

local function moveWrapper(dt: number)
    moveAcc += dt
    if moveAcc < MOVE_TICK then return end

    local step_dt = moveAcc
    moveAcc = 0
    mobMoveStep(step_dt)
end

scheduler.SYSTEM(moveWrapper, phases.AIMovement)
return 0


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local std = ReplicatedStorage.std
local ref = require(std.ref)

local cts = require(std.components)
local world = require(std.world)
local Player = cts.Player
local Character = cts.Character

local conn = {}

local function playersAdded(player: Player)
	local e = ref(player.UserId)
	world:set(e, Player, player)
	local characterAdd = player.CharacterAdded
	conn[e] = characterAdd:Connect(function(rig)
		while rig.Parent ~= workspace do
			task.wait()
		end
		world:set(e, Character, rig)
	end)
end

local function playersRemoved(player: Player)
	local e = ref(player.UserId)
	world:clear(e)
	local connection = conn[e]
	connection:Disconnect()
	conn[e] = nil
end

local scheduler = require(std.scheduler)
local phases = require(std.phases)
scheduler.SYSTEM(playersAdded, phases.PlayerAdded)
scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved)

return 0


local label = script.Parent
local replicated = game:GetService("ReplicatedStorage")
local count = replicated:WaitForChild("Count")

count.Changed:Connect(function(value)
	label.Text = "Mob Count :" .. value
end)


local Lighting = game:GetService("Lighting")
local RunService = game:GetService("RunService")

local Sky = Lighting:WaitForChild("Sky") :: Sky

local TOTAL_TIME = 600-- 5 minutes in seconds
local ROTATION_SPEED = 360 / TOTAL_TIME -- 1.2 degrees per second

RunService.RenderStepped:Connect(function(dt)
	local ori = Sky.SkyboxOrientation
	Sky.SkyboxOrientation = Vector3.new(
    ori.X,
    (ori.Y + ROTATION_SPEED * dt) % 360,
    ori.Z)
end)


--!strict
local RunService = game:GetService("RunService")

local NPC_HITBOXES = workspace:WaitForChild("NPC_Hitboxes")

type Visual = {
    range: BasePart?,
    ray: BasePart?,
    gui: BillboardGui,
    label: TextLabel,
}

local VISUALS: {[BasePart]: Visual} = {}

local function makeBillboard(hitbox: BasePart): Visual
    local gui = Instance.new("BillboardGui")
    gui.Name = "MobStateBillboard"
    gui.Size = UDim2.fromScale(4, 1)
    gui.AlwaysOnTop = true
    gui.LightInfluence = 0
    gui.StudsOffset = Vector3.new(0, hitbox.Size.Y + hitbox.Size.Y/2, 0)
    gui.Adornee = hitbox
    gui.Parent = hitbox  -- auto-cleanup when hitbox is destroyed
    gui.MaxDistance = 100

    local label = Instance.new("TextLabel")
    label.Name = "StateLabel"
    label.Size = UDim2.fromScale(1, 1)
    label.BackgroundTransparency = 0.35
    label.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
    label.BorderSizePixel = 0
    label.TextColor3 = Color3.new(1, 1, 1)
    label.Font = Enum.Font.GothamBold
    label.TextScaled = true
    label.Text = "..."
    label.Parent = gui

    return { gui = gui, label = label }
end

local function stateColor(state: string): Color3
    if state == "chase" then
        return Color3.fromRGB(255, 80, 80)      -- red
    elseif state == "flee" then
        return Color3.fromRGB(80, 160, 255)     -- blue
    elseif state == "circle" then
        return Color3.fromRGB(255, 200, 80)     -- yellow
    elseif state == "wander" then
        return Color3.fromRGB(80, 255, 120)     -- green
    else
        return Color3.fromRGB(180, 180, 180)    -- gray / unknown
    end
end

local function createRangePart(range: number): BasePart
    local p = Instance.new("Part")
    p.Shape = Enum.PartType.Ball
    p.Size = Vector3.new(range*2, range*2, range*2)
    p.Color = Color3.fromRGB(0, 180, 255)
    p.Material = Enum.Material.ForceField
    p.Transparency = 0.85
    p.CanCollide = false
    p.CanQuery = false
    p.CanTouch = false
    p.Anchored = true
    p.CastShadow = false
    p.Name = "DetectRangeVisual"
    p.Parent = workspace.Terrain
    return p
end

local function createRayPart(): BasePart
    local p = Instance.new("Part")
    p.Size = Vector3.new(0.1, 0.1, 1)
    p.Color = Color3.fromRGB(255, 80, 80)
    p.Material = Enum.Material.Neon
    p.Transparency = 0.2
    p.CanCollide = false
    p.CanQuery = false
    p.CanTouch = false
    p.Anchored = true
    p.Name = "VisionRay"
    p.Parent = workspace.Terrain
    return p
end

local function getVisual(hitbox: BasePart): Visual
    local v = VISUALS[hitbox]
    if not v then
        v = { range = nil, ray = nil }
        VISUALS[hitbox] = v
    end
    return v
end

local function updateStateVisual(hitbox: BasePart)
    local v = getVisual(hitbox)
    local stateAttr = hitbox:GetAttribute("MobState")
    if typeof(stateAttr) ~= "string" then
        return
    end

    if not v.gui then
        local Billboard = makeBillboard(hitbox)
        v.gui = Billboard.gui
        v.label = Billboard.label
    end

    v.label.Text = stateAttr
    v.label.BackgroundColor3 = stateColor(stateAttr)
end

local function updateRangeVisual(hitbox: BasePart)
    local v = getVisual(hitbox)
    local rangeAttr = hitbox:GetAttribute("DetectRange")
    if typeof(rangeAttr) ~= "number" or rangeAttr <= 0 then
        if v.range then
            v.range:Destroy()
            v.range = nil
        end
        return
    end

    if not v.range then
        v.range = createRangePart(rangeAttr)
    end
    v.range.Size = Vector3.new(rangeAttr*2, rangeAttr*2, rangeAttr*2)
end

local function updateRayVisual(hitbox: BasePart)
    local v = getVisual(hitbox)

    local hasTarget = hitbox:GetAttribute("HasTarget")
    local targetPos = hitbox:GetAttribute("TargetPos")

    if hasTarget ~= true or typeof(targetPos) ~= "Vector3" then
        if v.ray then
            v.ray.Transparency = 1
        end
        return
    end

    if not v.ray then
        v.ray = createRayPart()
    end

    local origin = hitbox.Position
    local toTarget = targetPos - origin
    local dist = toTarget.Magnitude
    if dist < 0.1 then
        v.ray.Transparency = 1
        return
    end

    local dir = toTarget.Unit
    v.ray.Transparency = 0.2
    v.ray.Size = Vector3.new(0.1, 0.1, dist)
    v.ray.CFrame = CFrame.new(origin + dir * (dist * 0.5), targetPos)
end

-- listen for new hitboxes
NPC_HITBOXES.ChildAdded:Connect(function(child)
    if not child:IsA("BasePart") then return end

    task.defer(function()
        updateRangeVisual(child)
        updateRayVisual(child)
        updateStateVisual(child)
    end)

    child.AttributeChanged:Connect(function(attr)
        if attr == "DetectRange" then
            updateRangeVisual(child)
        elseif attr == "HasTarget" or attr == "TargetPos" then
            updateRayVisual(child)
        elseif attr == "MobState" then
            updateStateVisual(child)
        end
    end)
end)

-- cleanup + follow hitboxes
RunService.RenderStepped:Connect(function()
    for hitbox, v in pairs(VISUALS) do
        if hitbox.Parent == nil then
            if v.range then v.range:Destroy() end
            if v.ray then v.ray:Destroy() end
            VISUALS[hitbox] = nil

            v.gui:Destroy()
            VISUALS[hitbox] = nil
        else
            if v.range then
                v.range.CFrame = hitbox.CFrame
            end
            -- ray visual is updated in AttributeChanged + here for motion
            if v.ray then
                updateRayVisual(hitbox)
            end
        end
    end
end)

return 0


--StarterPlayer/StarterPlayerScripts/systems/move.luau
--!optimize 2
--!native
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local world = require(std.world)
local scheduler = require(std.scheduler)

local cts = require(std.components)
local Model = cts.Model
local Transform = cts.Transform
local Hitbox = cts.Hitbox

local moved_mobs = world:query(Model, Transform,Hitbox):with(cts.Mob):cached()

-- ---------- Smooth interpolation state ----------
-- For each entity we keep a small state machine:
-- prev -> next over 'span' seconds, with alpha advancing each frame.
type Smooth = {
    prev: CFrame,
    next: CFrame,
    rendered: CFrame,  -- last frame’s output
    alpha: number,     -- [0..1]
    span: number,      -- seconds to complete the blend
}

local smooth: { [number]: Smooth } = {}

-- Pick a blend time ~ network period (12 Hz ~= 0.083s).
-- A bit longer is nicer; tweak to taste.
local TARGET_SPAN = 0.10   -- 100 ms
local MIN_SPAN    = 1/120  -- never 0; avoids huge alpha steps
local MAX_SPAN    = 0.25   -- don't smear too long

local function beginBlend(e: number, serverCF: CFrame)
    local s = smooth[e]
    if not s then
        -- first time: start at server pose (no pop)
        s = {
            prev = serverCF,
            next = serverCF,
            rendered = serverCF,
            alpha = 1,
            span = TARGET_SPAN,
        }
        smooth[e] = s
        return
    end

    -- Start new blend FROM what we actually rendered last frame (no rewind)
    local start = s.rendered
    s.prev = start
    s.next = serverCF

    -- Optional: dynamic span based on travel distance (longer jump = slightly longer smoothing)
    local dist = (s.next.Position - s.prev.Position).Magnitude
    local span = TARGET_SPAN
    if dist > 10 then
        span = math.clamp(TARGET_SPAN * 1.5, MIN_SPAN, MAX_SPAN)
    end

    s.span  = math.clamp(span, MIN_SPAN, MAX_SPAN)
    s.alpha = 0
end

local function stepBlend(e: number, dt: number): CFrame?
    local s = smooth[e]
    if not s then return nil end
    -- advance
    if s.alpha < 1 then
        -- normalize dt by span, clamp so big hitches don’t overshoot badly
        local da = math.clamp(dt / s.span, 0, 0.5)
        s.alpha = math.min(1, s.alpha + da)
    end
    -- CFrame:Lerp blends pos+rot (slerp-like)
    local out = s.prev:Lerp(s.next, s.alpha)
    s.rendered = out
    return out
end

-- ---------- 60 Hz presentation ----------
local RENDER_HZ = 60
local RENDER_DT = 1 / RENDER_HZ
local acc = 0

local function present(dt: number)
	acc += dt
	if acc < RENDER_DT then return end
	acc = 0
	for e, model, transform, hitbox in moved_mobs do
		if model.PrimaryPart and hitbox then
			-- target from Roblox replication
			local serverCF = hitbox.CFrame
			transform.new = serverCF

			-- drive smoothing
			beginBlend(e, serverCF)
			local cf = stepBlend(e, RENDER_DT)
			if cf then
				model:PivotTo(cf)
			end
		end
	end
end

scheduler.SYSTEM(present)

return 0


--StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau May move to replicated later
--!optimize 2
--!native
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local ref = require(std.ref)
local world = require(std.world)
local cts = require(std.components)

local MobsFolder = Instance.new("Folder")
MobsFolder.Name = "Mobs"
MobsFolder.Parent = workspace

local ReplicatedAnimals = ReplicatedStorage:WaitForChild("Animals")
local Animals = {} :: {[string]: BasePart}

for _ , child in pairs(ReplicatedAnimals:GetChildren()) do
	if child:IsA("BasePart") then
		if Animals[child.Name] == nil then
			Animals[child.Name] = child
		end
	end
end

warn("Animals ", Animals)

local function spawnVisualForHitbox(hitbox: BasePart)

	local mobType = hitbox:GetAttribute("MobType")
	local size = hitbox:GetAttribute("MobSize") or 1
	if not mobType or not Animals[mobType] then
		return
	end

	local part = Animals[mobType]:Clone()
	part.Anchored = true
	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false

	local model = Instance.new("Model")
	model.Name = `Mob-{mobType}`
	model.PrimaryPart = part
	part.Parent = model
	model.Parent = MobsFolder

	model:PivotTo(hitbox.CFrame)
	part.Size *= size

	local e = ref(hitbox)
	world:set(e, cts.Transform, { new = hitbox.CFrame, old = hitbox.CFrame })
	world:set(e, cts.Model, model)
	world:set(e, cts.Size, part.Size.Magnitude)
	world:set(e, cts.Hitbox, hitbox)
	world:add(e, cts.Mob)

end

local NPC_Hitboxes = workspace:WaitForChild("NPC_Hitboxes")

NPC_Hitboxes.ChildAdded:Connect(function(child)
	if child:IsA("BasePart") then
		spawnVisualForHitbox(child)
	end
end)

for _, child in ipairs(NPC_Hitboxes:GetChildren()) do
	if child:IsA("BasePart") then
		spawnVisualForHitbox(child)
	end
end

return 0


local ReplicatedStorage = game:GetService("ReplicatedStorage")
return require(ReplicatedStorage.Packages.jecs)


--!optimize 2
--!native

-- original author @centau

local FAILURE = -1
local RUNNING = 0
local SUCCESS = 1

local function SEQUENCE(nodes)
	return function(...)
		for _, node in nodes do
			local status = node(...)
			if status <= RUNNING  then
				return status
			end
		end
		return SUCCESS
	end
end

local function FALLBACK(nodes)
	return function(...)
		for _, node in nodes do
			local status = node(...)
			if status > FAILURE then
				return status
			end
		end
		return FAILURE
	end
end

local bt = {
	SEQUENCE = SEQUENCE,
	FALLBACK = FALLBACK,
	RUNNING = RUNNING,
}

return bt


--!nonstrict

--[[
	local signal = Signal.new() :: Signal.Signal<string, string>
	local events = collect(signal)
	local function system(world)
		for id, str1, str2 in events do
			--
		end
	end
]]

--[[
original author by @memorycode

MIT License

Copyright (c) 2024 Michael

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]

type Signal<T...> = { [any]: any }
local function collect<T...>(event: Signal<T...>)
	local storage = {}
	local mt = {}
	local iter = function()
		local n = #storage
		return function()
			if n <= 0 then
				mt.__iter = nil
				return nil
			end

			n -= 1
			return n + 1, unpack(table.remove(storage, 1) :: any)
		end
	end

	local disconnect = event:Connect(function(...)
		table.insert(storage, { ... })
		mt.__iter = iter
	end)

	setmetatable(storage, mt)
	return (storage :: any) :: () -> (number, T...), function()
		disconnect()
	end
end

return collect


local function interval(s)
	local pin

	local function throttle()
		if not pin then
			pin = os.clock()
		end

		local elapsed = os.clock() - pin > s
		if elapsed then
			pin = os.clock()
		end

		return elapsed
	end
	return throttle
end

return interval


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
-- Mailbox.lua (singleton)
local world = require(std.world)
local C     = require(std.components)

local M = { sys = nil, bufByComp = {} }  -- one global mailbox

local function ensureSYS(comp)
    if M.sys then return M.sys end
    for e in world:query(comp):cached() do
        M.sys = e;
        return e
    end
    -- (optional) create it:
    local e = world:entity()
    M.sys = e
    return e
end

function M.getBuf(comp)
    local buf = M.bufByComp[comp]
    if buf then return buf end
    local sys = ensureSYS(comp)
    buf = world:get(sys, comp)

    if not buf then
        buf = table.create(128);
        world:set(sys, comp, buf)
    end

    M.bufByComp[comp] = buf
    return buf
end

function M.push(comp, item)
    local b = M.getBuf(comp)
    b[#b+1] = item
end

function M.drain(comp, fn)
    local b = M.getBuf(comp)
    for i=1,#b do
        fn(b[i])
    end
    table.clear(b)
end

function M.invalidate()  -- for hot-reload
    M.sys = nil
    table.clear(M.bufByComp)
end

return M


local std = game:GetService("ReplicatedStorage").std
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local scheduler = require(std.scheduler)
local PHASE = scheduler.PHASE

-- ===== Phase Creation with Validation =====
local phases = {}

local function createPhase(name: string, config: { after: any?, event: RBXScriptSignal? }?)
    if phases[name] then
        warn(`Phase '{name}' already exists! Skipping...`)
        return phases[name]
    end

    local phase = PHASE(config or {})
    phases[name] = phase

    if _G.__DEV__ then  -- Only in development
        print(`✓ Created phase: {name}`)
    end

    return phase
end

-- ===== Core Phases (Never Change) =====
phases.Heartbeat = scheduler.phases.Heartbeat
phases.PreSimulation = scheduler.phases.PreSimulation
phases.PreAnimation = scheduler.phases.PreAnimation
phases.PreRender = scheduler.phases.PreRender

-- ===== Player Events =====
createPhase("PlayerAdded", { event = Players.PlayerAdded })
createPhase("PlayerRemoved", { event = Players.PlayerRemoving })

-- ===== Game Phases (Dependency Chain) =====
-- This order is the contract - changing it breaks everything
createPhase("EarlyUpdate", { after = phases.PreSimulation })
createPhase("Input", { after = phases.EarlyUpdate })
createPhase("GameLogic", { after = phases.Input })

-- Physics chain (order matters!)
createPhase("Physics", { after = phases.GameLogic })
createPhase("PhysicsKnockback", { after = phases.Physics })
createPhase("PhysicsGravity", { after = phases.PhysicsKnockback })
createPhase("PhysicsTerrain", { after = phases.PhysicsGravity })
createPhase("PhysicsCollision", { after = phases.PhysicsTerrain })

-- AI chain (can run in parallel within same phase)
createPhase("AIBehavior", { after = phases.PhysicsCollision })
createPhase("AIStateMachine", { after = phases.AIBehavior })
createPhase("AIMovement", { after = phases.AIStateMachine })  -- Chase/Patrol/Wander run here
createPhase("AICombat", { after = phases.AIMovement })

-- Game Systems
createPhase("Combat", { after = phases.AICombat })
createPhase("Health", { after = phases.Combat })
createPhase("Inventory", { after = phases.Health })
createPhase("Economy", { after = phases.Inventory })

-- Networking (order matters for batching)
createPhase("NetworkDelta", { after = phases.Economy })
createPhase("NetworkReliable", { after = phases.NetworkDelta })

-- Cleanup
createPhase("Cleanup", { after = phases.NetworkReliable })
createPhase("LateUpdate", { after = phases.Cleanup })

-- Independent chains (run on Heartbeat)
createPhase("WorldManagement", { after = phases.Heartbeat })
createPhase("Spawning", { after = phases.WorldManagement })
createPhase("Environment", { after = phases.WorldManagement })  -- Weather, day/night

-- Client-only phases
if RunService:IsClient() then
    createPhase("ClientInput", { after = phases.PreRender })
    createPhase("ClientUI", { after = phases.ClientInput })
    createPhase("ClientEffects", { after = phases.ClientUI })
    createPhase("ClientSound", { after = phases.ClientEffects })
    createPhase("ClientRendering", { after = phases.ClientSound })
end

-- Debug phase (always last)
createPhase("Debug", { after = phases.LateUpdate })

-- Freeze to prevent accidental modification
table.freeze(phases)

-- ===== Validation & Documentation =====
if _G.__DEV__ then
    print("=== Phase Execution Order ===")
    print("PreSimulation → EarlyUpdate → Input → GameLogic")
    print("→ Physics → PhysicsKnockback → PhysicsGravity → PhysicsTerrain → PhysicsCollision")
    print("→ AIBehavior → AIStateMachine → AIMovement → AICombat")
    print("→ Combat → Health → Inventory → Economy")
    print("→ NetworkDelta → NetworkReliable → Cleanup → LateUpdate → Debug")
    print("\nHeartbeat Branch: WorldManagement → Spawning/Environment")
    print("PreRender Branch (Client): ClientInput → ClientUI → ClientEffects → ClientSound → ClientRendering")
    print("========================")
end

return phases


local world = require(script.Parent.world)
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local refs: {[any]: jecs.Entity} = {}

local function fini(key): () -> ()
	return function()
		refs[key] = nil
	end
end

local function noop() end

local function ref(key): (jecs.Entity, () -> ())
	if not key then
		return world:entity(), noop
	end
	local e = refs[key]
	if not e then
		e = world:entity()
		refs[key] = e
	end
	-- Cannot cache handles because they will get invalidated
	return e, fini(key)
end

return ref


--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
local pair = jecs.pair
local Name = jecs.Name

type World = jecs.World
type Entity<T = nil> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>

type System = {
	callback: (world: World) -> (),
	id: number,
}

type Systems = { System }

type Events = {
	RenderStepped: Systems,
	Heartbeat: Systems,
}

local world = require(script.Parent.world)
local Disabled = world:entity()
local System = world:component() :: Id<{ callback: (any) -> (), name: string}>
local DependsOn = world:entity()
local Event = world:component() :: Id<RBXScriptSignal>
local Phase = world:entity()

local PreRender = world:entity()
local Heartbeat = world:entity()
local PreAnimation = world:entity()
local PreSimulation = world:entity()

local sys: System
local dt: number

local jabby_scheduler = jabby.scheduler.create("Scheduler")

local a, b, c, d
local function run()
	local id = sys.id
	jabby_scheduler:run(id, sys.callback, a, b, c, d)
	return nil
end

world:add(Heartbeat, Phase)
world:set(Heartbeat, Event, RunService.Heartbeat)

world:add(PreSimulation, Phase)
world:set(PreSimulation, Event, RunService.PreSimulation)

world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)

jabby.register({
	applet = jabby.applets.world,
	name = "MyWorld",
	configuration = {
	world = world,
	},
})

jabby.register({
	applet = jabby.applets.scheduler,
	name = "Scheduler",
	configuration = {
		scheduler = jabby_scheduler,
	},
})

if RunService:IsClient() then
	world:add(PreRender, Phase)
	world:set(PreRender, Event, (RunService :: RunService).PreRender)
end

local function begin(events: { [RBXScriptSignal]: Systems })
	local connections = {}
	for event, systems in events do
		if not event then
			continue
		end
		local event_name = tostring(event)
		connections[event] = event:Connect(function(...)
			debug.profilebegin(event_name)
			for _, s in systems do
				sys = s
				a, b, c, d = ...

				for _ in run do
					break
				end

			end
			debug.profileend()
		end)
	end
	return connections
end

local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity)
	local phase_name = world:get(phase, Name)
	for _, s in world:query(System):with(pair(DependsOn, phase)) do
		table.insert(systems, {
			id = jabby_scheduler:register_system({
				name = s.name,
				phase = phase_name,
			} :: any),
			callback = s.callback,
		})
	end
	for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do
		scheduler_collect_systems_under_phase_recursive(systems, after)
	end
end

local function scheduler_collect_systems_under_event(event)
	local systems = {}
	scheduler_collect_systems_under_phase_recursive(systems, event)
	return systems
end

local function scheduler_collect_systems_all()
	local events = {}
	for phase, event in world:query(Event):with(Phase) do
		events[event] = scheduler_collect_systems_under_event(phase)
	end
	return events
end

local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? })
	local phase = world:entity()
	world:add(phase, Phase)
	local after = d.after
	if after then
		local dependency = pair(DependsOn, after :: Entity)
		world:add(phase, dependency)
	end

	local event = d.event
	if event then
		world:set(phase, Event, event)
	end
	return phase
end

local function scheduler_systems_new(callback: (any) -> (), phase: Entity?)
	local system = world:entity()
	world:set(system, System, { callback = callback, name = debug.info(callback, "n") })
	local depends_on = DependsOn :: jecs.Entity
	local p: Entity = phase or Heartbeat
	world:add(system, pair(depends_on, p))

	return system
end

return {
	SYSTEM = scheduler_systems_new,
	BEGIN = begin,
	PHASE = scheduler_phase_new,
	COLLECT = scheduler_collect_systems_all,
	phases = {
		Heartbeat = Heartbeat,
		PreSimulation = PreSimulation,
		PreAnimation = PreAnimation,
		PreRender = PreRender
	}
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local jabby = require(ReplicatedStorage.Packages.jabby)
local std = ReplicatedStorage.std
local scheduler = require(std.scheduler)
local world = require(std.world)

local function start(modules)
	for _, module in modules do
		require(module)
	end
	local events = scheduler.COLLECT()
	scheduler.BEGIN(events)
	--[[jabby.set_check_function(function(player)
		return true
	end)
	if RunService:IsClient() then
		local player = game:GetService("Players").LocalPlayer
		local playergui = player:WaitForChild("PlayerGui")
		local client = jabby.obtain_client()
		UserInputService.InputBegan:Connect(function(input)
			if input.KeyCode == Enum.KeyCode.F4 then
				local home = playergui:FindFirstChild("Home")
				if home then
					home:Destroy()
				end
				client.spawn_app(client.apps.home)
			end
		end)
	end]]
end

return start


local jecs = require(game:GetService("ReplicatedStorage").ecs)

-- I like the idea of only having the world be a singleton.
local world = jecs.World.new() :: jecs.World
return world


local Prefix = require('./Prefix')
local sysPath = require('./sys/path')

local MAIN_SEPARATOR_STR = sysPath.MAIN_SEPARATOR_STR

type Prefix = Prefix.Prefix

export type PrefixComponent = {
    type: 'prefix',
    raw: string,
    parsed: Prefix,
}
type NormalComponent = { type: 'normal', value: string }

type ComponentEnum =
    NormalComponent
    | { type: 'rootDir' }
    | { type: 'curDir' }
    | { type: 'parentDir' }
    | PrefixComponent

type ComponentCommon = {
    toString: (self: Component) -> string,
    equal: (self: Component, other: Component) -> boolean,
}

export type Component = ComponentEnum & ComponentCommon

type ComponentStatic = Component & {
    normal: (value: string) -> Component,
    rootDir: () -> Component,
    curDir: () -> Component,
    parentDir: () -> Component,
    prefix: (raw: string, prefix: Prefix) -> Component,
}

local Component: ComponentStatic = {} :: any
local ComponentMetatable = {
    __index = Component,
    __tostring = function(self)
        return self:toString()
    end,
    __eq = function(self: Prefix, other: Prefix): boolean
        return self:equal(other)
    end,
}

function Component.normal(value: string): Component
    local self: ComponentEnum = { type = 'normal', value = value }

    return setmetatable(self, ComponentMetatable) :: any
end

function Component.rootDir(): Component
    local self: ComponentEnum = { type = 'rootDir' }

    return setmetatable(self, ComponentMetatable) :: any
end

function Component.curDir(): Component
    local self: ComponentEnum = { type = 'curDir' }

    return setmetatable(self, ComponentMetatable) :: any
end

function Component.parentDir(): Component
    local self: ComponentEnum = { type = 'parentDir' }

    return setmetatable(self, ComponentMetatable) :: any
end

function Component.prefix(raw: string, prefix: Prefix): Component
    local self: PrefixComponent = { type = 'prefix', raw = raw, parsed = prefix }

    return setmetatable(self, ComponentMetatable) :: any
end

function Component:toString(): string
    local componentType = self.type
    return if componentType == 'normal'
        then (self :: NormalComponent & ComponentCommon).value
        elseif componentType == 'curDir' then '.'
        elseif componentType == 'parentDir' then '..'
        elseif componentType == 'rootDir' then MAIN_SEPARATOR_STR
        else (self :: PrefixComponent & ComponentCommon).raw
end

function Component:equal(other: Component): boolean
    local selfType = self.type

    if selfType ~= other.type then
        return false
    end

    if selfType == 'normal' then
        return (self :: NormalComponent & ComponentCommon).value
            == (other :: NormalComponent & ComponentCommon).value
    elseif selfType == 'prefix' then
        return (self :: PrefixComponent & ComponentCommon).parsed:equal(
            (other :: PrefixComponent & ComponentCommon).parsed
        )
    end

    return true
end

return Component


export type PrefixEnum =
    { type: 'Verbatim', value: string }
    | { type: 'VerbatimUNC', hostName: string, shareName: string }
    | { type: 'VerbatimDisk', value: string }
    | { type: 'DeviceNS', value: string }
    | { type: 'UNC', hostName: string, shareName: string }
    | { type: 'Disk', value: string }

export type Prefix = PrefixEnum & {
    len: (self: Prefix) -> number,
    isVerbatim: (self: Prefix) -> boolean,
    isDrive: (self: Prefix) -> boolean,
    hasImplicitRoot: (self: Prefix) -> boolean,

    clone: (self: Prefix) -> Prefix,
    equal: (self: Prefix, other: Prefix) -> boolean,
}

type PrefixStatic = Prefix & {
    Verbatim: (value: string) -> Prefix,
    VerbatimUNC: (hostName: string, shareName: string) -> Prefix,
    VerbatimDisk: (value: string) -> Prefix,
    DeviceNS: (value: string) -> Prefix,
    UNC: (hostName: string, shareName: string) -> Prefix,
    Disk: (value: string) -> Prefix,
}

local Prefix: PrefixStatic = {} :: any
local PrefixMetatable = {
    __index = Prefix,
    __eq = function(self: Prefix, other: Prefix): boolean
        return self:equal(other)
    end,
}

local function new(self: PrefixEnum)
    return setmetatable(self, PrefixMetatable) :: any
end

function Prefix.Verbatim(value: string): Prefix
    return new({ type = 'Verbatim', value = value })
end

function Prefix.VerbatimUNC(hostName: string, shareName: string): Prefix
    return new({ type = 'VerbatimUNC', hostName = hostName, shareName = shareName })
end

function Prefix.VerbatimDisk(value: string): Prefix
    return new({ type = 'VerbatimDisk', value = value })
end

function Prefix.DeviceNS(value: string): Prefix
    return new({ type = 'DeviceNS', value = value })
end

function Prefix.UNC(hostName: string, shareName: string): Prefix
    return new({ type = 'UNC', hostName = hostName, shareName = shareName })
end

function Prefix.Disk(value: string): Prefix
    return new({ type = 'Disk', value = value })
end

function Prefix:len(): number
    local selfType = self.type
    if selfType == 'Verbatim' then
        return 4 + string.len((self :: any).value)
    elseif selfType == 'VerbatimUNC' then
        local data: { hostName: string, shareName: string } = self :: any
        local shareNameLen = string.len(data.shareName)
        return 8 + string.len(data.hostName) + if shareNameLen > 0 then 1 + shareNameLen else 0
    elseif selfType == 'VerbatimDisk' then
        return 6
    elseif selfType == 'UNC' then
        local data: { hostName: string, shareName: string } = self :: any
        local shareNameLen = string.len(data.shareName)
        return 2 + string.len(data.hostName) + if shareNameLen > 0 then 1 + shareNameLen else 0
    elseif selfType == 'DeviceNS' then
        return 4 + string.len((self :: any).value)
    elseif selfType == 'Disk' then
        return 2
    elseif _G.DEV then
        error('unreachable')
    end
    return 0
end

function Prefix:isVerbatim(): boolean
    local selfType = self.type
    return selfType == 'Verbatim' or selfType == 'VerbatimDisk' or selfType == 'VerbatimUNC'
end

function Prefix:isDrive(): boolean
    return self.type == 'Disk'
end

function Prefix:hasImplicitRoot(): boolean
    return self.type ~= 'Disk'
end

function Prefix:clone(): Prefix
    local self = table.clone(self)

    return setmetatable(self, PrefixMetatable) :: any
end

function Prefix:equal(other: Prefix): boolean
    local selfType = self.type

    if selfType ~= other.type then
        return false
    end

    if selfType == 'Verbatim' then
        return (self :: any).value == (other :: any).value
    end
    if selfType == 'VerbatimUNC' then
        return (self :: any).hostName == (other :: any).hostName
            and (self :: any).shareName == (other :: any).shareName
    end
    if selfType == 'VerbatimDisk' then
        return (self :: any).value == (other :: any).value
    end
    if selfType == 'DeviceNS' then
        return (self :: any).value == (other :: any).value
    end
    if selfType == 'UNC' then
        return (self :: any).hostName == (other :: any).hostName
            and (self :: any).shareName == (other :: any).shareName
    end
    if selfType == 'Disk' then
        return (self :: any).value == (other :: any).value
    end

    return true
end

return Prefix


local Disk = require('@pkg/luau-disk')

local Array = Disk.Array

export type Rev<T> = {
    next: (self: Rev<T>) -> T?,
    collect: (self: Rev<T>) -> { T },
    rev: (self: Rev<T>) -> Rev<T>,
    -- DoubleEndedIterator
    nextBack: (self: Rev<T>) -> T?,

    clone: (self: Rev<T>) -> Rev<T>,
}

type DoubleEndedIterator<T> = {
    next: (self: DoubleEndedIterator<T>) -> T?,
    nextBack: (self: DoubleEndedIterator<T>) -> T?,

    clone: (self: DoubleEndedIterator<T>) -> DoubleEndedIterator<T>,
}

type Private<T> = {
    _iter: DoubleEndedIterator<T>,
}
type PrivateRev<T> = Rev<T> & Private<T>

type RevStatic = {
    new: <T>(iter: DoubleEndedIterator<T>) -> Rev<T>,

    next: <T>(self: Rev<T>) -> T?,
    collect: <T>(self: Rev<T>) -> { T },
    rev: <T>(self: Rev<T>) -> Rev<T>,
    -- DoubleEndedIterator
    nextBack: <T>(self: Rev<T>) -> T?,

    clone: <T>(self: Rev<T>) -> Rev<T>,
}

local Rev: RevStatic = {} :: any
local RevMetatable = {
    __index = Rev,
    __iter = function<T>(self: PrivateRev<T>)
        local items = self:collect()
        return next, items
    end,
}

function Rev.new<T>(iter: DoubleEndedIterator<T>): Rev<T>
    local self: Private<T> = {
        _iter = iter,
    }

    return setmetatable(self, RevMetatable) :: any
end

function Rev:next<T>(): T?
    local self: PrivateRev<T> = self :: any

    return self._iter:nextBack()
end

function Rev:collect<T>(): { T }
    local self: PrivateRev<T> = self :: any

    return Array.fromFn(function()
        return self:next()
    end)
end

function Rev:nextBack<T>(): T?
    local self: PrivateRev<T> = self :: any

    return self._iter:next()
end

function Rev:clone<T>(): Rev<T>
    local self: PrivateRev<T> = self :: any

    return Rev.new(self._iter:clone())
end

return Rev


-- additional tests that were reported

local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Path = require('../Path')

local expect = jestGlobals.expect
local it = jestGlobals.it
local describe = jestGlobals.describe

describe('issue #3', function()
    it(`set empty extension on 'src/abc' gives 'src/abc'`, function()
        local path = Path.new('src/abc')

        path:setExtension('')

        expect(path:toString()).toEqual(Path.new('src'):join('abc'):toString())
        expect(path:equal(Path.new('src/abc'))).toBe(true)
    end)
end)


local Disk = require('@pkg/luau-disk')
local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Path = require('../Path')
local Prefix = require('../Prefix')
local sysPath = require('../sys/path')

local Array = Disk.Array
local Map = Disk.Map

type Prefix = Prefix.Prefix

-- When running tests in Roblox Studio, the output filters strings that look
-- like urls or paths, so the test names are awful. To work around that, this
-- function inserts an invisible space between each character.
local function display(content: string?): string?
    return content and table.concat(string.split(content, ''), '\u{200F}\u{200F}\u{200E}\u{200E}')
end

local expect = jestGlobals.expect
local it = jestGlobals.it
local describe = jestGlobals.describe

local IS_WINDOWS = sysPath.MAIN_SEPARATOR_STR == '\\'

describe('Prefix', function()
    if not IS_WINDOWS then
        return
    end

    local function getPathPrefix(s: string): Prefix
        local path = Path.new(s)

        local component = path:components():next()

        assert(component ~= nil, 'expected at least one component')
        assert(component.type == 'prefix', 'expected prefix component')

        return (component :: any).parsed
    end

    local cases: { [string]: Prefix } = {
        [ [[\\?\pictures\kittens]] ] = Prefix.Verbatim('pictures'),
        [ [[\\?\UNC\server\share]] ] = Prefix.VerbatimUNC('server', 'share'),
        [ [[\\?\c:\]] ] = Prefix.VerbatimDisk('C'),
        [ [[\\.\BrainInterface]] ] = Prefix.DeviceNS('BrainInterface'),
        [ [[\\server\share]] ] = Prefix.UNC('server', 'share'),
        [ [[C:\Users\Rust\Pictures\Ferris]] ] = Prefix.Disk('C'),
    }

    for path, expectPrefix in cases do
        it(`parses the prefix in '{display(path)}'`, function()
            expect(expectPrefix).toEqual(getPathPrefix(path))
        end)
    end
end)

describe('isVerbatim', function()
    local cases: { [Prefix]: boolean } = {
        [Prefix.Verbatim('pictures')] = true,
        [Prefix.VerbatimUNC('server', 'share')] = true,
        [Prefix.VerbatimDisk('C')] = true,
        [Prefix.DeviceNS('BrainInterface')] = false,
        [Prefix.UNC('server', 'share')] = false,
        [Prefix.Disk('C')] = false,
    }

    for prefix, expected in cases do
        local formatted = `\{ {table.concat(
            Array.map(Map.entries(prefix), function(entry)
                return `{entry[1]} = {entry[2]}`
            end),
            ', '
        )} }`

        it(`tests if {formatted} is verbatim = {expected}`, function()
            expect(prefix:isVerbatim()).toEqual(expected)
        end)
    end
end)


local Path = require('./Path')

export type Path = Path.Path
export type AsPath = Path.AsPath

return {
    Path = Path,
}


local Component = require('./Component')

type Component = Component.Component

type ComponentIterator = { next: (self: ComponentIterator) -> Component? }
type Clone<T> = { clone: (T) -> T }

local function iterAfter<T>(
    iter: T & ComponentIterator & Clone<ComponentIterator>,
    prefix: T & ComponentIterator
): (T & ComponentIterator)?
    while true do
        local iterNext = iter:clone()
        local x = iterNext:next()
        local y = prefix:next()

        if x ~= nil and y ~= nil then
            if not x:equal(y) then
                return nil
            end
        elseif y == nil then
            return iter
        else
            return nil
        end
        iter = iterNext :: any
    end
end

return iterAfter


local PrefixParserSlice = require('./PrefixParserSlice')

type PrefixParserSlice = PrefixParserSlice.PrefixParserSlice

export type PrefixParser = {
    asSlice: (self: PrefixParser) -> PrefixParserSlice,
}

type Private = {
    _path: string,
    _prefix: string,
    _length: number,
}
type PrivatePrefixParser = PrefixParser & Private

type PrefixParserStatic = PrefixParser & Private & {
    new: (path: string, length: number) -> PrefixParser,
    getPrefix: (path: string, length: number) -> string,
}

local PrefixParser: PrefixParserStatic = {} :: any
local PrefixParserMetatable = {
    __index = PrefixParser,
}

function PrefixParser.new(path: string, length: number): PrefixParser
    local self: Private = {
        _path = path,
        _prefix = PrefixParser.getPrefix(path, length),
        _length = length,
    }

    return setmetatable(self, PrefixParserMetatable) :: any
end

function PrefixParser.getPrefix(path: string, length: number): string
    local prefix = string.gsub(string.sub(path, 1, length), '/', '\\')
    return prefix
end

function PrefixParser:asSlice(): PrefixParserSlice
    local self: PrivatePrefixParser = self :: any

    return PrefixParserSlice.new(self._path, string.sub(self._prefix, 1, math.min(#self._path, 8)))
end

return PrefixParser


export type PrefixParserSlice = {
    stripPrefix: (self: PrefixParserSlice, prefix: string) -> PrefixParserSlice?,
    prefixBytes: (self: PrefixParserSlice) -> string,
    finish: (self: PrefixParserSlice) -> string,
}

type Private = {
    _path: string,
    _prefix: string,
    _index: number,
}
type PrivatePrefixParserSlice = PrefixParserSlice & Private

type PrefixParserSliceStatic = PrefixParserSlice & Private & {
    new: (path: string, prefix: string) -> PrefixParserSlice,
}

local PrefixParserSlice: PrefixParserSliceStatic = {} :: any
local PrefixParserSliceMetatable = {
    __index = PrefixParserSlice,
}

function PrefixParserSlice.new(path: string, prefix: string): PrefixParserSlice
    local self: Private = {
        _path = path,
        _prefix = prefix,
        _index = 1,
    }

    return setmetatable(self, PrefixParserSliceMetatable) :: any
end

function PrefixParserSlice:stripPrefix(prefix: string): PrefixParserSlice?
    local self: PrivatePrefixParserSlice = self :: any

    local prefixLength = #prefix
    if string.sub(self._prefix, self._index, self._index + prefixLength - 1) == prefix then
        local self: Private = {
            _path = self._path,
            _prefix = self._prefix,
            _index = self._index + prefixLength,
        }

        return setmetatable(self, PrefixParserSliceMetatable) :: any
    end
    return nil
end

function PrefixParserSlice:prefixBytes(): string
    local self: PrivatePrefixParserSlice = self :: any

    return string.sub(self._path, 1, self._index)
end

function PrefixParserSlice:finish(): string
    local self: PrivatePrefixParserSlice = self :: any

    return string.sub(self._path, self._index)
end

return PrefixParserSlice


local Prefix = require('../../Prefix')
local parsePrefixWindows = require('./parsePrefix')

type Prefix = Prefix.Prefix

local MAIN_SEPARATOR_STR = if _G.SYS_PATH_SEPARATOR == '\\'
        or _G.SYS_PATH_SEPARATOR == '/'
    then _G.SYS_PATH_SEPARATOR
    elseif _G.LUA_ENV == 'lune' and (require :: any)('@lune/process').os == 'windows' then '\\'
    else '/'

local IS_WINDOWS = MAIN_SEPARATOR_STR == '\\'

local function isSepByteUnix(b: string): boolean
    return b == '/'
end

local function isSepByteWindows(b: string): boolean
    return b == '/' or b == '\\'
end

local function isVerbatimSepUnix(b: string): boolean
    return b == '/'
end

local function isVerbatimSepWindows(b: string): boolean
    return b == '\\'
end

local isSepByte: (string) -> boolean = if IS_WINDOWS then isSepByteWindows else isSepByteUnix
local isVerbatimSep: (string) -> boolean = if IS_WINDOWS
    then isVerbatimSepWindows
    else isVerbatimSepUnix

local function parsePrefixUnix(_path: string): Prefix?
    return nil
end

local parsePrefix: (path: string) -> Prefix? = if IS_WINDOWS
    then parsePrefixWindows
    else parsePrefixUnix

return {
    MAIN_SEPARATOR_STR = MAIN_SEPARATOR_STR,
    isSepByte = isSepByte,
    isVerbatimSep = isVerbatimSep,
    parsePrefix = parsePrefix,
}