local collectPackages = require("@lune-lib/utils/collect-packages")
local fs = require("@lune/fs")
local path = require("@lune-lib/utils/path")
local process = require("@lune/process")
local roblox = require("@lune/roblox")
local rootConfig = require("@lune-lib/root-config")
local selectPackages = require("@lune-lib/utils/select-packages")
local serde = require("@lune/serde")
local types = require("@lune-lib/types")

local packages = selectPackages(collectPackages(process.cwd, rootConfig.packageDir))
local _, nameToPackage, toBuild = packages.names, packages.nameToPackage, packages.selected

local distPath = path(process.cwd, "dist")
local distPesdePath = path(distPath, "pesde")
local distWallyPath = path(distPath, "wally")
local distNpmPath = path(distPath, "npm")
local distModelPath = path(distPath, "models")
local distTempPath = path(distPath, "temp")
local distBundlePath = path(distPath, "bundle.rbxm")
local distBundlePlacePath = path(distPath, "bundle.rbxl")

if fs.isDir(distPath) then
	fs.removeDir(distPath)
end

fs.writeDir(distPath)
fs.writeDir(distPesdePath)
fs.writeDir(distWallyPath)
fs.writeDir(distNpmPath)
fs.writeDir(distModelPath)
fs.writeDir(distTempPath)

local function copyPackageDir(package: types.Package, destinationDir: string)
	for _, file in fs.readDir(package.absolutePath) do
		if fs.isFile(file) then
			if not file:match("%.luau$") or file == "prvd.config.luau" then
				continue
			end
		end
		fs.copy(path(package.absolutePath, file), path(destinationDir, file))
	end
end

local function generateLinkerFile(package: types.Package, requirePath: string)
	local linkerSource = { `local DEPENDENCY = require("{requirePath}")` }

	local packageTypes = package.config.types
	if packageTypes then
		for type, value in packageTypes do
			table.insert(linkerSource, `export type {type} = DEPENDENCY.{if value == true then type else value}`)
		end
	end

	table.insert(linkerSource, "return DEPENDENCY\n")

	return table.concat(linkerSource, "\n")
end

local function packageLicensePath(package: types.Package)
	return if package.config.license then path(package.absolutePath, "LICENSE.md") else path(process.cwd, "LICENSE.md")
end

-- Pesde

local packageForPesde
do
	type PesdeEnvironment = "roblox" | "roblox_server" | "luau" | "lune"

	type PesdeTarget = {
		environment: PesdeEnvironment,
		lib: string,
		build_files: { string },
	}

	type PesdeIndices = {
		default: string,
	}

	type PesdeDependency = {
		name: string,
		version: string,
		index: string?,
		target: PesdeEnvironment?,
	}

	type PesdeManifest = {
		name: string,
		version: string,
		description: string,
		license: string,
		authors: { string },
		repository: string,

		indices: {
			default: string,
		},

		includes: { string },

		target: PesdeTarget,

		dependencies: { [string]: PesdeDependency }?,
	}

	local DEFAULT_TARGETS: { [PesdeEnvironment]: true } = {
		roblox = true,
		luau = true,
		lune = true,
	}

	local SYBAU_PESDE = '\
		[scripts]\
		roblox_sync_config_generator = ".pesde/scripts/roblox_sync_config_generator.luau"\
		sourcemap_generator = ".pesde/scripts/sourcemap_generator.luau"\
		\
		[dev_dependencies]\
		scripts = { name = "pesde/scripts_rojo", version = "^0.1.0", target = "lune" }\
		rojo = { name = "pesde/rojo", version = "^7.4.4", target = "lune" }\
	'

	local function pesdeify(name: string)
		return (string.gsub(name, "%-", "_"))
	end

	function packageForPesde(package: types.Package)
		local targets: { [PesdeEnvironment]: true } = package.config.pesdeTargets or DEFAULT_TARGETS

		local baseManifest: PesdeManifest = {
			name = pesdeify(`{rootConfig.publishers.pesde.scope}/{package.config.name}`),
			version = package.config.version or rootConfig.defaults.version,
			description = package.config.description,
			license = package.config.license or rootConfig.defaults.license,
			authors = package.config.authors or rootConfig.defaults.authors,
			repository = rootConfig.repository,

			indices = {
				default = rootConfig.publishers.pesde.registry,
			},

			includes = {
				"src/**/*.luau",
				"LICENSE.md",
				"README.md",
				"pesde.toml",
			},

			target = {
				environment = nil :: any,
				lib = "src/init.luau",
				build_files = {
					"src",
				},
			},
		}

		-- NOTE: serde is ass, we serializing this by hand
		local dependencies = { "[dependencies]" }
		local linkerFiles: { [string]: string } = {}
		if package.config.dependencies then
			for dependency in package.config.dependencies do
				local dependentPackage = nameToPackage[dependency]

				-- I love stylua
				table.insert(
					dependencies,
					`{dependency} = \{ workspace = "{pesdeify(
						`{rootConfig.publishers.pesde.scope}/{dependentPackage.config.name}`
					)}", `
						.. `version = "{dependentPackage.config.version or rootConfig.defaults.version}" }`
				)

				linkerFiles[dependency] = generateLinkerFile(dependentPackage, `./$TARGET_PACKAGES/{dependency}`)
			end
		end

		local dependenciesFooter = table.concat(dependencies, "\n")

		local licenseToCopy = packageLicensePath(package)

		for subtarget in targets do
			local distPackagePath = path(distPesdePath, subtarget, package.config.name)
			fs.writeDir(distPackagePath)

			local src = path(distPackagePath, "src")
			fs.writeDir(src)
			copyPackageDir(package, src)

			baseManifest = table.clone(baseManifest)
			baseManifest.target.environment = subtarget :: any

			for fileName, source in linkerFiles do
				local linkerFile = `{fileName}.luau`
				table.insert(baseManifest.includes, linkerFile)
				table.insert(baseManifest.target.build_files, linkerFile)
				fs.writeFile(
					path(distPackagePath, linkerFile),
					(string.gsub(source, "$TARGET_PACKAGES", `{subtarget}_packages`))
				)
			end

			local manifest = serde.encode("toml", baseManifest, true)
			manifest ..= dependenciesFooter
			manifest ..= "\n"

			if subtarget == "roblox" then
				-- why is pesde so overcomplicated
				manifest ..= SYBAU_PESDE
				fs.copy(path(process.cwd, ".lune", "pesde-scripts"), path(distPackagePath, ".pesde", "scripts"))
			end

			fs.writeFile(path(distPackagePath, "pesde.toml"), manifest)

			fs.copy(licenseToCopy, path(distPackagePath, "LICENSE.md"))
		end
	end
end

-- Wally

local packageForWally
do
	type WallyManifest = {
		package: {
			name: string,
			description: string,
			version: string,
			license: string,
			authors: { string },
			realm: "shared" | "server",
			registry: string,
			homepage: string,
			repository: string,
			private: boolean,
			dependencies: { [string]: string },
		},
	}

	function packageForWally(package: types.Package)
		local distPackagePath = path(distWallyPath, package.config.name)
		fs.writeDir(distPackagePath)

		local manifest: WallyManifest = {
			package = {
				name = `{rootConfig.publishers.wally.scope}/{package.config.name}`,
				description = package.config.description,
				version = package.config.version or rootConfig.defaults.version,
				license = package.config.license or rootConfig.defaults.license,
				authors = package.config.authors or rootConfig.defaults.authors,
				realm = "shared",
				registry = rootConfig.publishers.wally.registry,
				homepage = rootConfig.homepage,
				repository = rootConfig.repository,
				private = false,
				dependencies = {},
			},
		}

		if package.config.dependencies then
			for dependency in package.config.dependencies do
				local dependentPackage = nameToPackage[dependency]

				-- I love stylua
				manifest.package.dependencies[dependency] =
					`{rootConfig.publishers.wally.scope}/{dependentPackage.config.name}@{dependentPackage.config.version or rootConfig.defaults.version}`

				fs.writeFile(
					path(distPackagePath, `{dependency}.luau`),
					generateLinkerFile(dependentPackage, `./Packages/{dependency}`)
				)
			end
		end

		local src = path(distPackagePath, "src")
		fs.writeDir(src)
		copyPackageDir(package, src)

		fs.copy(packageLicensePath(package), path(distPackagePath, "LICENSE.md"))
		fs.writeFile(path(distPackagePath, "Wally.toml"), serde.encode("toml", manifest, true))
	end
end

-- NPM

local packageForNpm
do
	type NpmManifest = {
		name: string,
		version: string,
		description: string,
		main: string,
		types: string,
		files: { string },
		author: string,
		repository: {
			type: "git",
			url: string,
		},
		bugs: {
			url: string,
		},
		dependencies: { [string]: string },
	}

	local function copyPackageDir(package: types.Package, destinationDir: string)
		for _, file in fs.readDir(package.absolutePath) do
			if file:match("%.luau$") and file ~= "prvd.config.luau" then
				fs.copy(path(package.absolutePath, file), path(destinationDir, file))
			elseif file:match("%.ts$") then
				-- fixing import paths
				local contents = fs.readFile(path(package.absolutePath, file))
				contents = string.gsub(contents, 'from "%.%./%w+"', function(importPath)
					return string.gsub(importPath, "%.%./", `@{rootConfig.publishers.npm.scope}/`)
				end)
				fs.writeFile(path(destinationDir, file), contents)
			end
		end
	end

	function packageForNpm(package: types.Package)
		local distPackagePath = path(distNpmPath, package.config.name)
		fs.writeDir(distPackagePath)

		local manifest: NpmManifest = {
			name = `@{rootConfig.publishers.npm.scope}/{package.config.name}`,
			version = package.config.version or rootConfig.defaults.version,
			description = package.config.description,
			main = "src/init.luau",
			types = "src/init.luau",
			files = {
				"src",
				"LICENSE.md",
				"package.json",
			},
			author = (package.config.authors or rootConfig.defaults.authors)[1],
			repository = {
				type = "git",
				url = `git+{rootConfig.repository}.git`,
			},
			bugs = {
				url = `{rootConfig.repository}/issues`,
			},
			dependencies = {},
		}

		if package.config.dependencies then
			for dependency in package.config.dependencies do
				local dependentPackage = nameToPackage[dependency]

				-- I love stylua
				manifest.dependencies[`@{rootConfig.publishers.npm.scope}/{dependentPackage.config.name}`] = dependentPackage.config.version
					or rootConfig.defaults.version

				fs.writeFile(
					path(distPackagePath, `{dependency}.luau`),
					generateLinkerFile(dependentPackage, `./Packages/{dependency}`)
				)
			end
		end

		local src = path(distPackagePath, "src")
		fs.writeDir(src)
		copyPackageDir(package, src)

		fs.copy(packageLicensePath(package), path(distPackagePath, "LICENSE.md"))
		fs.writeFile(path(distPackagePath, "package.json"), serde.encode("json", manifest, true))
	end
end

local models: { [types.Package]: string } = {}

local function buildModel(package: types.Package)
	local distTempModelPath = path(distTempPath, "models", package.config.name)
	fs.writeDir(distTempModelPath)
	local src = path(distTempModelPath, "src")
	fs.writeDir(src)
	copyPackageDir(package, src)
	fs.writeFile(
		path(distTempModelPath, "default.project.json"),
		serde.encode("json", {
			name = package.config.name,
			tree = {
				["$path"] = "src",
			},
		}, true)
	)
	local result = process.exec("rojo", { "build", "-o", "model.rbxm", "default.project.json" }, {
		cwd = distTempModelPath,
	})
	assert(result.ok, result.stderr)
	local modelPath = path(distModelPath, `{package.config.name}.rbxm`)
	fs.move(path(distTempModelPath, "model.rbxm"), modelPath)
	models[package] = modelPath
	fs.removeDir(distTempModelPath)
end

for _, name in toBuild do
	print("Building package", name)
	local package = nameToPackage[name]

	packageForPesde(package)
	packageForWally(package)
	packageForNpm(package)

	buildModel(package)
end

fs.removeDir(distTempPath)
print("Building bundle")

local bundle = roblox.Instance.new("Folder")
bundle.Name = "prvdmwrong"

for _, modelPath in models do
	for _, instance in roblox.deserializeModel(fs.readFile(modelPath)) do
		instance.Parent = bundle
	end
end

fs.writeFile(distBundlePath, roblox.serializeModel({ bundle }))

local game = roblox.Instance.new("DataModel")
bundle.Parent = game:GetService("ReplicatedStorage")

fs.writeFile(distBundlePlacePath, roblox.serializePlace(game))


-- Repository

export type RootPublisher = {
	scope: string,
	registry: string,
}

local function publisher(x: RootPublisher)
	return table.freeze(x)
end

export type RepoConfig = {
	repository: string,
	homepage: string,
	packageDir: string,
	defaults: {
		authors: { string },
		license: string,
		version: string,
	},
	publishers: {
		pesde: RootPublisher,
		wally: RootPublisher,
		npm: RootPublisher,
	}
}

local function root(x: RepoConfig)
	return table.freeze(x)
end

-- Packages

export type PackageName =
	| "prvdmwrong"
	| "runtime"
	| "logger"
	| "dependencies"
	| "providers"
	| "lifecycles"
	| "roots"
	| "rbx-components"
	| "rbx-lifecycles"

export type PackageTarget = "roblox"

export type PackageDependencies = { [PackageName]: true? }

local function dependencies(x: PackageDependencies)
	return table.freeze(x)
end

export type PackageConfig = {
	name: string,
	description: string,

	authors: { string }?,
	license: string?,
	version: string?,

	pesdeTargets: { [PesdeEnvironment]: true }?,

	dependencies: PackageDependencies?,

	types: { [string]: boolean | string }?,

	unreleased: boolean?
}

local function package(x: PackageConfig)
	return table.freeze(x)
end

export type PesdeEnvironment = "roblox" | "roblox_server" | "luau" | "lune"

return table.freeze({
	root = root,
	publisher = publisher,
	package = package,
	dependencies = dependencies,
})


-- From dphfox/tiniest, licenced under BSD
--!strict

local tiniest_plugin = require("./tiniest_plugin")

type Context = DescribeContext | RunContext

type DescribeContext = {
	type: "describe",
	labels: { string },
	add_test: (Test) -> (),
}

type RunContext = {
	type: "run",
}

export type ErrorReport = {
	type: "tiniest.ErrorReport",
	message: string,
	trace: string,
	code: {
		snippet: string,
		line: string,
	}?,
}

export type Test = {
	labels: { string },
	run: () -> (),
}

export type RunResult = {
	tests: { Test },
	status_tally: {
		pass: number,
		fail: number,
	},
	individual: { [Test]: TestRunResult },
}

export type TestRunResult = PassTestRunResult | FailTestRunResult

export type PassTestRunResult = {
	status: "pass",
}

export type FailTestRunResult = {
	status: "fail",
	error: ErrorReport,
}

export type Options = {
	plugins: nil | { tiniest_plugin.Plugin },
}

export type RunOptions = {}

local function catch_errors(func: any, ...)
	local outer_trace
	local function handler(message)
		local report: ErrorReport
		if typeof(message) == "table" and message.type == "tiniest.ErrorReport" then
			report = message :: ErrorReport
		else
			report = {
				type = "tiniest.ErrorReport",
				message = tostring(message),
				trace = debug.traceback(nil, 2),
			}
		end
		local from, to = string.find(report.trace, outer_trace, 1, true)
		if from ~= nil and to ~= nil then
			report.trace = report.trace:sub(1, from - 1) .. report.trace:sub(to + 1)
		end
		return report
	end
	outer_trace = debug.traceback()
	return xpcall(func, handler, ...)
end

local tiniest = {}

function tiniest.configure(options: Options)
	local plugins = tiniest_plugin.configure(options.plugins)

	local get_context, with_root_context, with_inner_context
	do
		local current_context: Context? = nil

		function get_context(): Context
			assert(current_context ~= nil, "This function can only be called from inside of a test suite")
			return current_context
		end

		function with_root_context(root_context: Context, inner: (Context) -> ()): ()
			assert(current_context == nil, "This function can't be called from inside of a test suite")
			current_context = root_context
			local ok, err = catch_errors(inner :: any, root_context)
			current_context = nil
			if not ok then
				-- bro
				-- error(err.message, 0)
				error(err, 0)
			end
		end

		function with_inner_context(make_context: (Context) -> Context, inner: (Context) -> ()): ()
			local outer_context = get_context()
			local inner_context = make_context(outer_context)
			current_context = inner_context
			local ok, err = catch_errors(inner :: any, inner_context)
			current_context = outer_context
			if not ok then
				error(err.message, 0)
			end
		end
	end

	local self = {}

	function self.describe(label: string, inner: () -> ()): ()
		with_inner_context(function(outer_context: Context): Context
			assert(outer_context.type == "describe", "This function can only be called outside of tests")
			local context = table.clone(outer_context)
			context.labels = table.clone(context.labels)
			table.insert(context.labels, label)
			return context
		end, inner)
	end

	function self.test(label: string, run: () -> ()): ()
		local context = get_context()
		assert(context.type == "describe", "This function can only be called outside of tests")
		local labels = table.clone(context.labels)
		table.insert(labels, label)
		local test: Test = {
			labels = labels,
			run = run,
		}
		context.add_test(test)
	end

	function self.collect_tests(inner: () -> ()): { Test }
		local tests = {}
		local context: Context = {
			type = "describe",
			labels = {},
			add_test = function(test)
				table.insert(tests, test)
			end,
		}
		with_root_context(context, inner)
		return tests
	end

	function self.run_tests(tests: { Test }, run_options: RunOptions): RunResult
		plugins.notify("before_run", tests, run_options)

		local context: Context = {
			type = "run",
		}

		local individual = {}
		for _, test in tests do
			plugins.notify("before_test", test, run_options)
			local _, test_run_result = xpcall(function()
				with_root_context(context, test.run)
				local pass: TestRunResult = {
					status = "pass",
				}
				return pass
			end, function(message)
				if typeof(message) == "table" and message.type == "tiniest.ErrorReport" then
					local fail: TestRunResult = {
						status = "fail",
						error = message :: ErrorReport,
					}
					return fail
				else
					local error: ErrorReport = {
						type = "tiniest.ErrorReport",
						message = tostring(message),
						trace = debug.traceback(),
					}
					local fail: TestRunResult = {
						status = "fail",
						error = error,
					}
					return fail
				end
			end)
			plugins.notify("after_test", test, test_run_result, run_options)
			individual[test] = test_run_result
		end

		local status_tally = {
			pass = 0,
			fail = 0,
		}

		for _, result in individual do
			status_tally[result.status] += 1
		end

		local result: RunResult = {
			tests = tests,
			status_tally = status_tally,
			individual = individual,
		}
		plugins.notify("after_run", result, run_options)

		return result
	end

	return self
end

return tiniest


-- From dphfox/tiniest, licenced under BSD
--!strict

local tiniest_quote = require("./tiniest_quote")

local tiniest_expect = {}

type Context = {
	target: unknown,
	test: string,
	params: { unknown },
}

local context: Context = {
	target = nil,
	test = "",
	params = {},
}

local function fail(message: string?): never
	local quoted = {}
	for index, param in context.params do
		quoted[index] = tiniest_quote(param)
	end
	local param_list = table.concat(quoted, ",")
	error({
		type = "tiniest.ErrorReport",
		message = "Expectation not met" .. if message == nil then "" else `, because:\n{message}`,
		trace = debug.traceback(nil, 4),
		code = {
			snippet = `expect({tiniest_quote(context.target)}).{context.test}({param_list})`,
			line = debug.info(4, "l"),
		},
	}, 0)
end

local function check(condition: boolean, cause: string): ()
	if not condition then
		error(`Bad usage of expect().{context.test}()\nCause: {cause}`, 0)
	end
end

local function is_indexable(x: unknown): boolean
	return type(x) == "table" or type(x) == "userdata"
end

function tiniest_expect.expect(a: any)
	local tests = {}

	function tests.exists()
		return if a ~= nil then tests else fail()
	end

	function tests.never_exists()
		return if a == nil then tests else fail()
	end

	function tests.is(b: any)
		return if a == b then tests else fail()
	end

	function tests.never_is(b: any)
		return if a ~= b then tests else fail()
	end

	function tests.is_true()
		check(typeof(a) == "boolean", "expect() value must be boolean")
		return if a then tests else fail()
	end

	function tests.never_is_true()
		check(typeof(a) == "boolean", "expect() value must be boolean")
		return if not a then tests else fail()
	end

	function tests.is_a(b: string)
		return if typeof(a) == b then tests else fail()
	end

	function tests.never_is_a(b: string)
		return if typeof(a) ~= b then tests else fail()
	end

	function tests.has_key(b: any)
		check(is_indexable(a), "expect() value must be indexable")
		check(b ~= nil, "key cannot be nil")
		return if a[b] then tests else fail()
	end

	function tests.never_has_key(b: any)
		check(is_indexable(a), "expect() value must be indexable")
		check(b ~= nil, "key cannot be nil")
		return if not a[b] then tests else fail(`[{tiniest_quote(b)}] = {tiniest_quote(a[b])}`)
	end

	function tests.has_value(b: any)
		check(typeof(a) == "table", "expect() value must be table")
		check(b ~= nil, "value cannot be nil")
		return if table.find(a, b) then tests else fail()
	end

	function tests.never_has_value(b: any)
		check(typeof(a) == "table", "expect() value must be table")
		check(b ~= nil, "value cannot be nil")
		local index = table.find(a, b)
		return if index == nil then tests else fail(`Found at index {index}`)
	end

	function tests.fails()
		check(typeof(a) == "function", "expect() value must be function")
		local ok = pcall(a)
		return if not ok then tests else fail()
	end

	function tests.never_fails()
		check(typeof(a) == "function", "expect() value must be function")
		local ok, err = pcall(a)
		return if ok then tests else fail(`Failed with {tiniest_quote(err)}`)
	end

	function tests.fails_with(b: string)
		check(typeof(a) == "function", "expect() value must be function")
		local ok, err = pcall(a)
		return if not ok and err:lower():match(b:lower())
			then tests
			else if ok then fail("Did not fail") else fail(`Failed with {tiniest_quote(err)}`)
	end

	function tests.never_fails_with(b: string)
		check(typeof(a) == "function", "expect() value must be function")
		local ok, err = pcall(a)
		return if ok or not err:lower():match(b:lower()) then tests else fail()
	end

	for name: any, body: any in tests do
		tests[name] = function(...)
			context.target = a
			context.test = name
			context.params = { ... }
			return body(...)
		end
	end

	return tests
end

return tiniest_expect


-- From dphfox/tiniest, licensed under BSD3
--!strict

local tiniest = require("./tiniest")
local tiniest_expect = require("./tiniest_expect")
local tiniest_pretty = require("./tiniest_pretty")
local tiniest_snapshot = require("./tiniest_snapshot")
local tiniest_time = require("./tiniest_time")

local fs = require("@lune/fs")
local luau = require("@lune/luau")

export type Options = {
	snapshot_path: string?,
	save_snapshots: boolean?,
	pretty: nil | {
		disable_colour: boolean?,
		disable_emoji: boolean?,
		disable_unicode: boolean?,
		disable_output: nil | {
			after_run: boolean?,
		},
	},
}

local tiniest_for_lune = {}

function tiniest_for_lune.configure(options: Options)
	local self = {}

	local function get_path_to_snapshot(key: string): string
		assert(options.snapshot_path ~= nil)
		return `{options.snapshot_path}/{key}.snap.luau`
	end

	local function load_snapshots(key: string): { string }?
		local path = get_path_to_snapshot(key)
		if not fs.isFile(path) then
			return nil
		else
			local ok, result = pcall(function()
				local source = fs.readFile(path)
				local bytecode = luau.compile(source)
				local loaded = luau.load(bytecode, {
					injectGlobals = false,
				})
				return loaded()
			end)
			if ok then
				return result
			else
				error("[tiniest_for_lune] Failed to load snapshots from disk: " .. tostring(result), 0)
			end
		end
	end

	local function save_snapshots(key: string, snapshots: { string }): ()
		local ok, result = pcall(function()
			snapshots = table.clone(snapshots)
			for index, snapshot in snapshots do
				snapshots[index] = `[====[{snapshot}]====]`
			end
			fs.writeFile(
				get_path_to_snapshot(key),
				"-- Auto-generated by dphfox/tiniest. Do not modify!\n"
					.. "--!nocheck\n"
					.. "return {"
					.. table.concat(snapshots, ", ")
					.. "}"
			)
		end)
		if not ok then
			error("[tiniest_for_lune] Failed to save snapshots to disk: " .. tostring(result), 0)
		end
	end

	self.expect = tiniest_expect.expect

	local tiniest_time = tiniest_time.configure({
		get_timestamp = os.clock,
	})

	local tiniest_snapshot = tiniest_snapshot.configure({
		load_snapshots = if options.snapshot_path then load_snapshots else nil,
		save_snapshots = if options.save_snapshots then save_snapshots else nil,
	})
	self.snapshot = tiniest_snapshot.snapshot

	local tiniest_pretty = tiniest_pretty.configure({
		disable_colour = options.pretty and options.pretty.disable_colour,
		disable_emoji = options.pretty and options.pretty.disable_emoji,
		disable_unicode = options.pretty and options.pretty.disable_unicode,
		disable_output = options.pretty and options.pretty.disable_output,
		plugins = { tiniest_time :: any, tiniest_snapshot },
	})
	self.format_run = tiniest_pretty.format_run

	local tiniest = tiniest.configure({
		plugins = { tiniest_time :: any, tiniest_snapshot, tiniest_pretty },
	})
	self.describe = tiniest.describe
	self.test = tiniest.test
	self.collect_tests = tiniest.collect_tests
	self.run_tests = tiniest.run_tests

	return self
end

-- bro
export type Configured = typeof(tiniest.configure({})) & {
	expect: typeof(tiniest_expect.expect),
}

return tiniest_for_lune


-- From dphfox/tiniest, licenced under BSD
--!strict

export type Plugin = {
	is_tiniest_plugin: true,
}

local tiniest_plugin = {}

function tiniest_plugin.configure(plugins: { any }?)
	if plugins ~= nil then
		for index, plugin in plugins do
			if not plugin.is_tiniest_plugin then
				error(`sanity check: plugin #{index} is not a valid plugin`)
			end
		end
	end

	local self = {}

	function self.notify(method_name: string, ...): ()
		if plugins == nil then
			return
		end
		for _, plugin in plugins do
			local method = plugin[method_name]
			if typeof(method) == "function" then
				method(...)
			end
		end
	end

	return self
end

return tiniest_plugin


-- From dphfox/tiniest, licenced under BSD
--!strict

local tiniest = require("./tiniest")
local tiniest_plugin = require("./tiniest_plugin")
type Test = tiniest.Test
type TestRunResult = tiniest.TestRunResult
type RunResult = tiniest.RunResult

export type Options = {
	plugins: nil | { tiniest_plugin.Plugin },
	disable_emoji: boolean?,
	disable_unicode: boolean?,
	disable_colour: boolean?,
	disable_output: nil | {
		after_run: boolean?,
	},
}

local tiniest_pretty = {}

function tiniest_pretty.configure(options: Options)
	if options.disable_unicode then
		options.disable_emoji = true
	end

	local plugins = tiniest_plugin.configure(options.plugins)

	local function string_len(text: string)
		if options.disable_unicode then
			return string.len(text)
		else
			return utf8.len(text) or string.len(text)
		end
	end
	local status_icons = {
		pass = if options.disable_emoji then "[PASS]" else "✅",
		fail = if options.disable_emoji then "[FAIL]" else "❌",
	}
	local crumb_trail = if options.disable_unicode then " > " else " ▸ "
	local divider = if options.disable_unicode then "=" else "═"
	local margin_line = if options.disable_unicode then "|" else "│"

	local paint = {}
	do
		local function ansi_mode(...)
			if options.disable_colour then
				return ""
			else
				return `{string.char(27)}[{table.concat({ ... }, ";")}m`
			end
		end
		local DIM = ansi_mode("2")
		local PASS = ansi_mode("1", "32")
		local PASS_DIM = ansi_mode("2", "32")
		local FAIL = ansi_mode("1", "31")
		local FAIL_DIM = ansi_mode("2", "31")
		local TRACE = ansi_mode("1", "34")
		local TRACE_DIM = ansi_mode("2", "34")
		local NUMBER = ansi_mode("1", "36")
		local STRING = ansi_mode("1", "33")
		local KEYWORD = ansi_mode("1", "35")
		local RESET = ansi_mode("0")

		function paint.dim(text: string): string
			return DIM .. text .. RESET
		end

		function paint.pass(text: string): string
			return PASS .. text .. RESET
		end

		function paint.pass_dim(text: string): string
			return PASS_DIM .. text .. RESET
		end

		function paint.fail(text: string): string
			return FAIL .. text .. RESET
		end

		function paint.fail_dim(text: string): string
			return FAIL_DIM .. text .. RESET
		end

		function paint.trace(text: string): string
			return TRACE .. text .. RESET
		end

		function paint.trace_dim(text: string): string
			return TRACE_DIM .. text .. RESET
		end

		function paint.number(text: string): string
			return NUMBER .. text .. RESET
		end

		function paint.string(text: string): string
			return STRING .. text .. RESET
		end

		function paint.keyword(text: string): string
			return KEYWORD .. text .. RESET
		end
	end

	local LINE_LENGTH = 80
	local full_width_divider = paint.dim(string.rep(divider, LINE_LENGTH))
	local function title(text: string): string
		local no_ansi = text:gsub(`{string.char(27)}.-m`, "")
		local divider_count = (LINE_LENGTH - string_len(no_ansi) - 2) / 2
		local divider_lhs = string.rep(divider, math.ceil(divider_count))
		local divider_rhs = string.rep(divider, math.floor(divider_count))
		return paint.dim(divider_lhs) .. " " .. text .. " " .. paint.dim(divider_rhs)
	end
	local function syntax(snippet: string): string
		return snippet
			:gsub("0?[xXbB]?%d*%.?%d*[eE]?%-?%d+", paint.number)
			:gsub('".-"', paint.string)
			:gsub("'.-'", paint.string)
			:gsub("function", paint.keyword)
			:gsub("end", paint.keyword)
			:gsub("true", paint.keyword)
			:gsub("false", paint.keyword)
			:gsub("nil", paint.keyword)
			:gsub("%-%-.-\n", paint.dim)
	end
	local function indent(passage: string, indentation: string): string
		return passage:gsub("\n", "\n" .. indentation)
	end

	local self = {}
	self.is_tiniest_plugin = true

	function self.format_run(run_result: RunResult): string
		local lines = {}

		local pretty_results = {}
		for _, test in run_result.tests do
			local result = run_result.individual[test]
			local painted_labels = table.clone(test.labels)
			for index, label in painted_labels do
				local style = result.status .. if index < #painted_labels then "_dim" else ""
				painted_labels[index] = paint[style](label)
			end
			local annotations = ""
			local function add_annotation(annotation: string): ()
				annotations ..= ` - {annotation}`
			end
			plugins.notify("add_annotations", result, options, add_annotation)
			local pretty = {
				test = test,
				result = result,
				crumbs = table.concat(painted_labels, paint[result.status .. "_dim"](crumb_trail)),
				icon = status_icons[result.status],
				annotations = paint.dim(annotations),
			}
			table.insert(pretty_results, pretty)
		end

		if run_result.status_tally.fail > 0 then
			table.insert(lines, title(`Errors from {run_result.status_tally.fail} test(s)`))
			table.insert(lines, "")
			for _, pretty in pretty_results do
				if pretty.result.status == "pass" then
					continue
				end
				table.insert(lines, `{pretty.icon} {pretty.crumbs}`)
				table.insert(lines, paint.trace(pretty.result.error.message))
				local code = pretty.result.error.code
				if code ~= nil then
					local num_length = string_len(code.line)
					local empty_margin = string.rep(" ", num_length + 1) .. paint.dim(margin_line) .. " "
					table.insert(lines, empty_margin)
					table.insert(
						lines,
						`{paint.dim(`{code.line} {margin_line}`)} {indent(syntax(code.snippet), empty_margin)}`
					)
					table.insert(lines, empty_margin)
				end
				local trace = pretty.result.error.trace:gsub("\n+$", "")
				table.insert(lines, paint.trace_dim(trace))
				table.insert(lines, "")
			end
		end

		local line_items = {}
		local function add_line_items(key: string, value: string): ()
			table.insert(line_items, `{paint.dim(key .. ":")} {value}`)
		end
		plugins.notify("add_line_items", run_result, options, add_line_items)

		table.insert(lines, title(`Status of {#pretty_results} test(s)`))
		table.insert(lines, "")
		for _, pretty in pretty_results do
			table.insert(lines, `{pretty.icon} {pretty.crumbs}{pretty.annotations}`)
		end
		table.insert(lines, "")
		table.insert(
			lines,
			title(
				`{paint.pass(`{run_result.status_tally.pass} passed`)}, {paint.fail(
					`{run_result.status_tally.fail} failed`
				)}`
			)
		)
		if #line_items > 0 then
			table.insert(lines, "")
			for _, line_item in line_items do
				table.insert(lines, line_item)
			end
			table.insert(lines, "")
			table.insert(lines, full_width_divider)
		end

		return table.concat(lines, "\n")
	end

	function self.after_run(run_result: RunResult, _): ()
		if options.disable_output and options.disable_output.after_run then
			return
		end
		print(self.format_run(run_result))
	end

	return self
end

return tiniest_pretty


-- From dphfox/tiniest, licenced under BSD
--!strict

local function tiniest_quote(x: unknown, given_indent_amount: number?): string
	if type(x) == "nil" or type(x) == "number" or type(x) == "boolean" or type(x) == "userdata" then
		return tostring(x)
	elseif type(x) == "string" then
		return string.format("%q", x)
	elseif type(x) == "function" then
		return "function() ... end"
	elseif type(x) == "table" then
		local outer_indent_amount = given_indent_amount or 0
		local inner_indent_amount = outer_indent_amount + 1
		local outer_indent = string.rep("  ", outer_indent_amount)
		local inner_indent = string.rep("  ", inner_indent_amount)

		local tbl: {} = x :: any
		local sorted_pairs = {}
		for key, value in tbl do
			table.insert(sorted_pairs, {
				key = tiniest_quote(key, inner_indent_amount),
				value = tiniest_quote(value, inner_indent_amount),
			})
		end
		table.sort(sorted_pairs, function(a, b)
			return a.key < b.key
		end)
		local frozen = table.isfrozen(tbl)
		local lines = { if frozen then "table.freeze {" else "{" }
		for _, pair in sorted_pairs do
			table.insert(lines, `{inner_indent}[{pair.key}] = {pair.value};`)
		end
		table.insert(lines, outer_indent .. "}")
		return table.concat(lines, "\n")
	else
		return `{type(x)}({tostring(x)})`
	end
end

return tiniest_quote :: (unknown) -> string


-- From dphfox/tiniest, licensed under BSD3
--!strict

local tiniest = require("./tiniest")
local tiniest_quote = require("./tiniest_quote")
type Test = tiniest.Test

export type TestRunResult = tiniest.TestRunResult & {
	num_snapshots_updated: number,
	num_snapshots_obsolete: number,
}
export type RunResult = tiniest.RunResult & {
	num_snapshots_updated: number,
	num_snapshots_obsolete: number,
}

export type Options = {
	save_snapshots: nil | (key: string, values: { string }) -> (),
	load_snapshots: nil | (key: string) -> { string }?,
}

type TestContext = {
	key: string,
	snapshots: { string },
	next_snapshot_index: number,
	num_updated: number,
}

type RunContext = {
	num_updated: number,
	num_obsolete: number,
}

local tiniest_snapshot = {}

function tiniest_snapshot.configure(options: Options)
	local self = {}
	self.is_tiniest_plugin = true

	local function get_test_key(test: Test)
		local labels = {}
		for _, label in test.labels do
			label = label:lower():gsub("[^%w%s]", ""):gsub(" ", "_")
			table.insert(labels, label)
		end
		return table.concat(labels, ".")
	end

	local run_context: RunContext?
	local test_context: TestContext?

	function self.snapshot(x: unknown): ()
		if options.load_snapshots == nil then
			error("snapshot() is unavailable - snapshots have not been configured", 0)
		elseif run_context == nil or test_context == nil then
			error("snapshot() can only be used while a test is running with tiniest_snapshot", 0)
		end

		local snapshot_index = test_context.next_snapshot_index
		test_context.next_snapshot_index += 1

		local fresh_snapshot = tiniest_quote(x)
		local snapshot_on_disk = test_context.snapshots[snapshot_index]
		test_context.snapshots[snapshot_index] = fresh_snapshot

		if fresh_snapshot == snapshot_on_disk then
			return
		end

		if options.save_snapshots then
			run_context.num_updated += 1
			test_context.num_updated += 1
			test_context.snapshots[snapshot_index] = fresh_snapshot
		elseif snapshot_on_disk == nil then
			error({
				type = "tiniest.ErrorReport",
				message = "New snapshot() call needs to be saved.\nRun while saving snapshots to save it to disk.",
				trace = debug.traceback(nil, 2),
				code = {
					snippet = `snapshot({fresh_snapshot})`,
					line = debug.info(2, "l"),
				},
			}, 0)
		else
			error({
				type = "tiniest.ErrorReport",
				message = "Snapshot does not match",
				trace = debug.traceback(nil, 2),
				code = {
					snippet = `snapshot({fresh_snapshot})\n\n-- snapshot on disk:\nsnapshot({snapshot_on_disk})`,
					line = debug.info(2, "l"),
				},
			}, 0)
		end
	end

	function self.before_run(_, _): ()
		run_context = {
			num_updated = 0,
			num_obsolete = 0,
		}
	end

	function self.after_run(original_run_result: tiniest.RunResult, _): ()
		assert(run_context ~= nil)
		local run_result = original_run_result :: RunResult
		run_result.num_snapshots_updated = run_context.num_updated
		run_result.num_snapshots_obsolete = run_context.num_obsolete
		run_context = nil
	end

	function self.before_test(test: Test, _): ()
		assert(run_context ~= nil)
		local key = get_test_key(test)
		test_context = {
			key = key,
			snapshots = options.load_snapshots and options.load_snapshots(key) or {},
			next_snapshot_index = 1,
			num_updated = 0,
		}
	end

	function self.after_test(_, original_run_result: tiniest.TestRunResult, _): ()
		assert(run_context ~= nil)
		assert(test_context ~= nil)
		local run_result = original_run_result :: TestRunResult
		run_result.num_snapshots_updated = test_context.num_updated
		run_result.num_snapshots_obsolete = test_context.next_snapshot_index - #test_context.snapshots - 1
		run_context.num_obsolete += run_result.num_snapshots_obsolete
		if test_context.num_updated > 0 and options.save_snapshots ~= nil then
			options.save_snapshots(test_context.key, test_context.snapshots)
		end
		test_context = nil
	end

	function self.add_annotations(original_run_result: tiniest.TestRunResult, _, add_annotation: (string) -> ())
		local run_result = original_run_result :: TestRunResult
		if run_result.num_snapshots_updated > 0 then
			add_annotation(`{run_result.num_snapshots_updated} snapshot(s) updated`)
		end
		if run_result.num_snapshots_obsolete > 0 then
			add_annotation(`{run_result.num_snapshots_obsolete} snapshot(s) obsolete`)
		end
	end

	function self.add_line_items(original_run_result: tiniest.RunResult, _, add_line_item: (string, string) -> ())
		local run_result = original_run_result :: RunResult
		if run_result.num_snapshots_updated > 0 then
			add_line_item("Updated snapshots", tostring(run_result.num_snapshots_updated))
		end
		if run_result.num_snapshots_obsolete > 0 then
			add_line_item("Obsolete snapshots", tostring(run_result.num_snapshots_obsolete))
		end
	end

	return self
end

return tiniest_snapshot


-- From dphfox/tiniest, licenced under BSD
--!strict

local tiniest = require("./tiniest")
local tiniest_pretty = require("./tiniest_pretty")
type Test = tiniest.Test

export type TestRunResult = tiniest.TestRunResult & {
	duration: number,
}
export type RunResult = tiniest.RunResult & {
	duration: number,
}

export type Options = {
	get_timestamp: () -> number,
}

local tiniest_time = {}

local function format_duration(seconds: number, options: tiniest_pretty.Options): string
	local SECOND = 1
	local MILLISECOND = SECOND / 1000
	local MICROSECOND = MILLISECOND / 1000
	local suffix = {
		micro = if options.disable_unicode then "u" else "µ",
		milli = "m",
	}

	if seconds < 100 * MICROSECOND then
		return `{math.ceil(seconds / MICROSECOND * 100) / 100}{suffix.micro}s`
	elseif seconds < 100 * MILLISECOND then
		return `{math.ceil(seconds / MILLISECOND * 100) / 100}{suffix.milli}s`
	else
		return `{math.ceil(seconds * 100) / 100}s`
	end
end

function tiniest_time.configure(options: Options)
	local self = {}
	self.is_tiniest_plugin = true

	local start_times = {}

	function self.before_run(tests: { Test }, _): ()
		start_times[tests] = options.get_timestamp()
	end

	function self.after_run(original_run_result: tiniest.RunResult, _): ()
		local run_result = original_run_result :: RunResult
		run_result.duration = options.get_timestamp() - start_times[run_result.tests]
		start_times[run_result.tests] = nil
	end

	function self.before_test(test: Test, _): ()
		start_times[test] = options.get_timestamp()
	end

	function self.after_test(test: Test, original_run_result: tiniest.TestRunResult, _): ()
		local run_result = original_run_result :: TestRunResult
		run_result.duration = options.get_timestamp() - start_times[test]
		start_times[test] = nil
	end

	function self.add_annotations(
		original_run_result: tiniest.TestRunResult,
		options: tiniest_pretty.Options,
		add_annotation: (string) -> ()
	)
		local run_result = original_run_result :: TestRunResult
		add_annotation(format_duration(run_result.duration, options))
	end

	function self.add_line_items(
		original_run_result: tiniest.RunResult,
		options: tiniest_pretty.Options,
		add_line_item: (string, string) -> ()
	)
		local run_result = original_run_result :: RunResult
		add_line_item("Time to run", format_duration(run_result.duration, options))
	end

	return self
end

return tiniest_time


local configure = require("@lune-lib/configure")

export type PackageContext = {
	rootDir: string,
	outDir: string,
	outWorkDir: string,
	packagesDir: string,
	packages: { [string]: Package },
}

export type Package = {
	absolutePath: string,
	relativePath: string,
	config: configure.PackageConfig,
}

return nil


local configure = require("@lune-lib/configure")
local fs = require("@lune/fs")
local path = require("@lune-lib/utils/path")
local types = require("@lune-lib/types")

local function collectPackages(rootPath: string, packageDir: string)
	local packages: { types.Package } = {}
	local packageDirPath = path(rootPath, packageDir)
	local knownNames = {}

	for _, package in fs.readDir(packageDirPath) do
		local packagePath = path(packageDirPath, package)
		if fs.isDir(packagePath) then
			local packageRelativePath = path(packageDir, package)
			local configRequirePath = path(packageRelativePath, "prvd.config")
			local configPath = path(packagePath, "prvd.config.luau")

			if fs.isFile(configPath) then
				-- HACK: lune i beg you to have a cwd parameter for requires
				local config: configure.PackageConfig = (require)(`@lune-lib/../../{configRequirePath}`)

				assert(not knownNames[config.name], `Duplicate package name found: {config.name}`)
				knownNames[config.name] = true

				table.insert(packages, {
					absolutePath = packagePath,
					relativePath = packageRelativePath,
					config = config,
				})
			end
		end
	end

	return packages
end

return collectPackages


local process = require("@lune/process")

local delimiter = if process.os == "windows" then "\\" else "/"

local function path(...: string)
	return table.concat({ ... }, delimiter)
end

return path


local styles = require("./styles")

local LINE_COUNT = 80

local bold = styles.bold
local dim = styles.dim

local function printDivider(str: string, lineCount: number?)
	local dashRepeats = ((lineCount or LINE_COUNT) - str:len()) * 0.5
	local extraIfUneven = if str:len() % 2 ~= 0 then 1 else 0
	return print(bold(`{dim(string.rep("=", dashRepeats))} {str} {dim(string.rep("=", dashRepeats + extraIfUneven))}`))
end

return printDivider


local process = require("@lune/process")
local stdio = require("@lune/stdio")
local types = require("@lune-lib/types")

local function selectPackages(packages: { types.Package }): {
	names: { string },
	nameToPackage: { [string]: types.Package },
	selected: { string },
}
	local packageNames = {}
	local nameToPackage = {}

	local selected: { string } = {}

	table.sort(packages, function(lhs, rhs)
		return lhs.config.name < rhs.config.name
	end)

	for _, package in packages do
		local name = package.config.name
		table.insert(packageNames, name)
		nameToPackage[name] = package
	end

	if #process.args == 0 then
		for _, index in stdio.prompt("multiselect", "Which packages to build?", packageNames) do
			table.insert(selected, packageNames[index])
		end
	elseif table.find(process.args, "all") then
		selected = table.clone(packageNames)
	else
		table.move(process.args, 1, #process.args, 1, selected)
	end

	return {
		names = packageNames,
		nameToPackage = nameToPackage,
		selected = selected,
	}
end

return selectPackages


local fs = require("@lune/fs")
local tiniest = require("@lune-lib/tiniest/tiniest_for_lune").configure({})

local tests = tiniest.collect_tests(function()
	local function visitDir(path: string, luauPath: string)
		for _, entry in fs.readDir(path) do
			local fullPath = path .. "/" .. entry
			local fullLuauPath = luauPath .. "/" .. entry
			if fs.isDir(fullPath) then
				visitDir(fullPath, fullLuauPath)
			elseif fs.isFile(fullPath) and fullPath:match("%.spec.luau$") ~= nil then
				(require)(fullLuauPath)(tiniest)
			end
		end
	end

	for _, package in fs.readDir("packages") do
		tiniest.describe(package, function()
			visitDir("packages/" .. package, "../packages/" .. package)
		end)
	end
end)

tiniest.run_tests(tests, {})
-- print(tiniest.format_run())


local logger = require("./logger")
local types = require("./types")

local function useBeforeConstructed(unresolved: types.UnresolvedDependency)
	logger:fatalError({ template = logger.useBeforeConstructed, unresolved.dependency.name or "subdependency" })
end

local unresolvedMt = {
	__metatable = "This metatable is locked.",
	__index = useBeforeConstructed,
	__newindex = useBeforeConstructed,
}

function unresolvedMt:__tostring()
	return "UnresolvedDependency"
end

-- Wtf luau
local function depend<Self, Dependencies, NewArgs...>(dependency: types.Dependency<Self, Dependencies, NewArgs...>): typeof(({} :: types.Dependency<Self, Dependencies, NewArgs...>).new(
	(nil :: any) :: Dependencies,
	...
))
	-- Return a mock value that will be resolved during dependency resolution.
	return setmetatable({
		type = "UnresolvedDependency",
		dependency = dependency,
	}, unresolvedMt) :: any
end

return depend


local depend = require("@self/depend")
local processDependencies = require("@self/process-dependencies")
local sortByPriority = require("@self/sort-by-priority")
local types = require("@self/types")

export type Dependency<Self, Dependencies = nil, NewArgs... = ()> = types.Dependency<Self, Dependencies, NewArgs...>

return table.freeze({
	depend = depend,
	processDependencies = processDependencies,
	sortByPriority = sortByPriority,
})


local logger = require("../logger")

return logger.create("@prvdmwrong/dependencies", logger.standardErrorInfoUrl("dependencies"), {
	useBeforeConstructed = "Cannot use %s before it is constructed. Are you using the dependency outside a class method?",
	alreadyDestroyed = "Cannot destroy an already destroyed Root.",
	alreadyFinished = "Root has already finished.",
})


local lifecycles = require("../lifecycles")
local types = require("./types")

export type ProccessDependencyResult = {
	sortedDependencies: { types.Dependency<any> },
	lifecycles: { lifecycles.Lifecycle },
}

type Set<T> = { [T]: true }

local function processDependencies(dependencies: Set<types.Dependency<any>>): ProccessDependencyResult
	local sortedDependencies = {}
	local lifecycles = {}

	-- TODO: optimize ts
	local function visitDependency(dependency: types.Dependency<any>)
		-- print("Visiting dependency", dependency)

		for key, value in dependency :: any do
			if key == "dependencies" then
				-- print("Checking [prvd.dependencies]")
				if typeof(value) == "table" then
					for key, value in value do
						-- print("Checking key", key, "value", value)
						if typeof(value) == "table" and value.type == "UnresolvedDependency" then
							local unresolved: types.UnresolvedDependency = value
							-- print("Got unresolved dependency", unresolved.dependency, "visiting")
							visitDependency(unresolved.dependency)
						end
					end
				end

				continue
			end

			if typeof(value) == "table" and value.type == "Lifecycle" and not table.find(lifecycles, value) then
				table.insert(lifecycles, value)
			end
		end

		if dependencies[dependency] and not table.find(sortedDependencies, dependency) then
			-- print("Pushing to sort")
			table.insert(sortedDependencies, dependency)
		end
	end

	for dependency in dependencies do
		visitDependency(dependency)
	end

	return {
		sortedDependencies = sortedDependencies,
		lifecycles = lifecycles,
	}
end

return processDependencies


local configure = require("@lune-lib/configure")

return configure.package({
	name = "dependencies",
	description = "Dependency resolution logic for Prvd 'M Wrong",

	types = table.freeze({
		["Dependency<Self, Dependencies = nil, NewArgs... = ()>"] = "Dependency<Self, Dependencies, NewArgs...>",
	}),

	dependencies = configure.dependencies({
		lifecycles = true,
		logger = true,
	}),
})


local types = require("./types")

local function sortByPriority(dependencies: { types.Dependency<unknown> })
	table.sort(dependencies, function(left: any, right: any)
		if left.priority ~= right.priority then
			return (left.priority or 1) > (right.priority or 1)
		end
		local leftIndex = table.find(dependencies, left)
		local rightIndex = table.find(dependencies, right)
		return leftIndex < rightIndex
	end)
end

return sortByPriority


export type Dependency<Self, Dependencies = nil, NewArgs... = ()> = Self & {
	dependencies: Dependencies,
	priority: number?,
	new: (dependencies: Dependencies, NewArgs...) -> Dependency<Self, Dependencies, NewArgs...>,
}

export type UnresolvedDependency = {
	type: "UnresolvedDependency",
	dependency: Dependency<any>,
}

export type dependencies = { __prvdmwrong_dependencies: never }

return nil


local await = require("./methods/await")
local clear = require("./methods/unregister-all")
local destroy = require("./methods/destroy")
local lifecycleConstructed = require("./hooks/lifecycle-constructed")
local methodToLifecycles = require("./method-to-lifecycles")
local onRegistered = require("./methods/on-registered")
local onUnregistered = require("./methods/on-unregistered")
local register = require("./methods/register")
local types = require("./types")
local unregister = require("./methods/unregister")

local function create<Args...>(
	method: string,
	onFire: (lifecycle: types.Lifecycle<Args...>, Args...) -> ()
): types.Lifecycle<Args...>
	local self: types.Self<Args...> = {
		_isDestroyed = false,
		_selfRegistered = {},
		_selfUnregistered = {},

		type = "Lifecycle",
		callbacks = {},

		fire = onFire,
		method = method,
		register = register,
		unregister = unregister,
		clear = clear,
		onRegistered = onRegistered,
		onUnregistered = onUnregistered,
		await = await,
		destroy = destroy,
	} :: any

	methodToLifecycles[method] = methodToLifecycles[method] or {}
	table.insert(methodToLifecycles[method], self)

	for _, onLifecycleConstructed in lifecycleConstructed.callbacks do
		onLifecycleConstructed(self :: types.Lifecycle)
	end

	return self
end

return create


local runtime = require("../../runtime")
local types = require("../types")

local function fireConcurrent<Args...>(lifecycle: types.Lifecycle<Args...>, ...: Args...)
	for _, callback in lifecycle.callbacks do
		runtime.threadpool.spawn(callback, ...)
	end
end

return fireConcurrent


local types = require("../types")

local function fireSequential<Args...>(lifecycle: types.Lifecycle<Args...>, ...: Args...)
	for _, callback in lifecycle.callbacks do
		callback(...)
	end
end

return fireSequential


local types = require("../types")

local callbacks: { (lifecycle: types.Lifecycle) -> () } = {}

local function onLifecycleConstructed(listener: (lifecycle: types.Lifecycle) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onLifecycleConstructed = onLifecycleConstructed,
})


local types = require("../types")

local callbacks: { (lifecycle: types.Lifecycle) -> () } = {}

local function onLifecycleDestroyed(listener: (lifecycle: types.Lifecycle) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onLifecycleDestroyed = onLifecycleDestroyed,
})


local types = require("../types")

local callbacks: { (lifecycle: types.Lifecycle, callback: (...unknown) -> ()) -> () } = {}

local function onLifecycleRegistered<Args...>(
	listener: (
		lifecycle: types.Lifecycle<Args...>,
		callback: (Args...) -> ()
	) -> ()
): () -> ()
	table.insert(callbacks, listener :: any)
	return function()
		table.remove(callbacks, table.find(callbacks, listener :: any))
	end
end

return table.freeze({
	callbacks = callbacks,
	onLifecycleRegistered = onLifecycleRegistered,
})


local types = require("../types")

local callbacks: { (lifecycle: types.Lifecycle, callback: (...unknown) -> ()) -> () } = {}

local function onLifecycleUnregistered<Args...>(
	listener: (
		lifecycle: types.Lifecycle<Args...>,
		callback: (Args...) -> ()
	) -> ()
): () -> ()
	table.insert(callbacks, listener :: any)
	return function()
		table.remove(callbacks, table.find(callbacks, listener :: any))
	end
end

return table.freeze({
	callbacks = callbacks,
	onLifecycleUnregistered = onLifecycleUnregistered,
})


local create = require("@self/create")
local fireConcurrent = require("@self/handlers/fire-concurrent")
local fireSequential = require("@self/handlers/fire-sequential")
local lifecycleConstructed = require("@self/hooks/lifecycle-constructed")
local lifecycleDestroyed = require("@self/hooks/lifecycle-destroyed")
local lifecycleRegistered = require("@self/hooks/lifecycle-registered")
local lifecycleUnregistered = require("@self/hooks/lifecycle-unregistered")
local methodToLifecycles = require("@self/method-to-lifecycles")
local types = require("@self/types")

export type Lifecycle<Args... = ...unknown> = types.Lifecycle<Args...>

return table.freeze({
	create = create,
	handlers = table.freeze({
		fireConcurrent = fireConcurrent,
		fireSequential = fireSequential,
	}),
	hooks = table.freeze({
		onLifecycleRegistered = lifecycleRegistered.onLifecycleRegistered,
		onLifecycleUnregistered = lifecycleUnregistered.onLifecycleUnregistered,
		onLifecycleConstructed = lifecycleConstructed.onLifecycleConstructed,
		onLifecycleDestroyed = lifecycleDestroyed.onLifecycleDestroyed,
	}),
	_ = table.freeze({
		methodToLifecycles = methodToLifecycles,
	}),
})


local logger = require("../logger")

return logger.create("@prvdmwrong/lifecycles", logger.standardErrorInfoUrl("lifecycles"), {
	useAfterDestroy = "Cannot use Lifecycle after it is destroyed.",
	alreadyDestroyed = "Cannot destroy an already destroyed Lifecycle.",
})


local types = require("./types")

local methodToLifecycles: { [string]: { types.Lifecycle } } = {}

return methodToLifecycles


local logger = require("../logger")
local types = require("../types")

local function await<Args...>(lifecycle: types.Self<Args...>): Args...
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.useAfterDestroy })
	end
	local currentThread = coroutine.running()
	local function callback(...: Args...)
		lifecycle:unregister(callback)
		coroutine.resume(currentThread, ...)
	end
	lifecycle:register(callback)
	return coroutine.yield()
end

return await


local lifecycleDestroyed = require("../hooks/lifecycle-destroyed")
local logger = require("../logger")
local methodToLifecycles = require("../method-to-lifecycles")
local types = require("../types")

local function destroy<Args...>(lifecycle: types.Self<Args...>)
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.alreadyDestroyed })
	end
	table.remove(methodToLifecycles[lifecycle.method], table.find(methodToLifecycles[lifecycle.method], lifecycle))
	table.clear(lifecycle.callbacks)
	for _, callback in lifecycleDestroyed.callbacks do
		callback(lifecycle)
	end
	lifecycle._isDestroyed = true
end

return destroy


local logger = require("../logger")
local types = require("../types")

local function onRegistered<Args...>(lifecycle: types.Self<Args...>, listener: (callback: (Args...) -> ()) -> ())
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.useAfterDestroy })
	end
	if _G.PRVDMWRONG_DISALLOW_MULTIPLE_LISTENERS then
		table.remove(lifecycle._selfRegistered, table.find(lifecycle._selfRegistered, listener))
	end
	table.insert(lifecycle._selfRegistered, listener)
	return function()
		table.remove(lifecycle._selfRegistered, table.find(lifecycle._selfRegistered, listener))
	end
end

return onRegistered


local logger = require("../logger")
local types = require("../types")

local function onUnregistered<Args...>(lifecycle: types.Self<Args...>, listener: (callback: (Args...) -> ()) -> ())
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.useAfterDestroy })
	end
	if _G.PRVDMWRONG_DISALLOW_MULTIPLE_LISTENERS then
		table.remove(lifecycle._selfUnregistered, table.find(lifecycle._selfUnregistered, listener))
	end
	table.insert(lifecycle._selfUnregistered, listener)
	return function()
		table.remove(lifecycle._selfUnregistered, table.find(lifecycle._selfUnregistered, listener))
	end
end

return onUnregistered


local logger = require("../logger")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function register<Args...>(lifecycle: types.Self<Args...>, callback: (Args...) -> ())
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.useAfterDestroy })
	end
	if _G.PRVDMWRONG_DISALLOW_MULTIPLE_LISTENERS then
		table.remove(lifecycle.callbacks, table.find(lifecycle.callbacks, callback))
	end
	table.insert(lifecycle.callbacks, callback)
	threadpool.spawnCallbacks(lifecycle._selfRegistered, callback)
end

return register


local logger = require("../logger")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function clear<Args...>(lifecycle: types.Self<Args...>)
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.useAfterDestroy })
	end
	for _, callback in lifecycle.callbacks do
		threadpool.spawnCallbacks(lifecycle._selfUnregistered, callback)
	end
	table.clear(lifecycle.callbacks)
end

return clear


local logger = require("../logger")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function unregister<Args...>(lifecycle: types.Self<Args...>, callback: (Args...) -> ())
	if lifecycle._isDestroyed then
		logger:fatalError({ template = logger.useAfterDestroy })
	end
	local index = table.find(lifecycle.callbacks, callback)
	if index then
		table.remove(lifecycle.callbacks, index)
		threadpool.spawnCallbacks(lifecycle._selfUnregistered, callback)
	end
end

return unregister


local configure = require("@lune-lib/configure")

return configure.package({
	name = "lifecycles",
	description = "Provider method lifecycles implementation for Prvd 'M Wrong",

	dependencies = configure.dependencies({
		logger = true,
		runtime = true,
	}),

	types = {
		["Lifecycle<Args... = ...unknown>"] = "Lifecycle<Args...>",
	},
})


export type Lifecycle<Args... = ...any> = {
	type: "Lifecycle",

	callbacks: { (Args...) -> () },
	method: string,

	register: (self: Lifecycle<Args...>, callback: (Args...) -> ()) -> (),
	fire: (self: Lifecycle<Args...>, Args...) -> (),
	unregister: (self: Lifecycle<Args...>, callback: (Args...) -> ()) -> (),
	clear: (self: Lifecycle<Args...>) -> (),
	onRegistered: (self: Lifecycle<Args...>, listener: (callback: (Args...) -> ()) -> ()) -> () -> (),
	onUnregistered: (self: Lifecycle<Args...>, listener: (callback: (Args...) -> ()) -> ()) -> () -> (),
	await: (self: Lifecycle<Args...>) -> Args...,
	destroy: (self: Lifecycle<Args...>) -> (),
}

export type Self<Args... = ...any> = Lifecycle<Args...> & {
	_isDestroyed: boolean,
	_selfRegistered: { (callback: (Args...) -> ()) -> () },
	_selfUnregistered: { (callback: (Args...) -> ()) -> () },
}

return nil


local runtime = require("../runtime")

local allowWebLinks = if runtime.name == "roblox" then game:GetService("RunService"):IsStudio() else true

return allowWebLinks


local allowWebLinks = require("./allow-web-links")
local formatLog = require("./format-log")
local runtime = require("../runtime")
local types = require("./types")

local task = runtime.task
local warn = runtime.warn

local function create<T>(label: string, errorInfoUrl: string?, templates: T): types.Logger<T>
	local self = {} :: types.Logger<T>
	self.type = "Logger"

	label = `[{label}] `
	errorInfoUrl = if allowWebLinks then errorInfoUrl else nil

	function self:warn(log)
		warn(label .. formatLog(self, log, errorInfoUrl))
	end

	function self:error(log)
		task.spawn(error, label .. formatLog(self, log, errorInfoUrl), 0)
	end

	function self:fatalError(log)
		error(label .. formatLog(self, log, errorInfoUrl))
	end

	setmetatable(self :: any, { __index = templates })
	return self
end

return create


local types = require("./types")

local function formatLog<Templates>(self: types.Logger<Templates>, log: types.Log, errorInfoUrl: string?): string
	local formattedTemplate = string.format(log.template, table.unpack(log))

	local error = log.error
	local trace: string? = log.trace

	if error then
		trace = error.trace
		formattedTemplate = string.gsub(formattedTemplate, "ERROR_MESSAGE", error.message)
	end

	local id: string?

	-- TODO: find a better way to do ts while still being ergonomic
	for templateKey, template in (self :: any) :: { [string]: string } do
		if template == log.template then
			id = templateKey
			break
		end
	end

	if id then
		formattedTemplate ..= `\nID: {id}`
	end

	if errorInfoUrl then
		formattedTemplate ..= `\nLearn more: {errorInfoUrl}`
		if id then
			formattedTemplate ..= `#{string.lower(id)}`
		end
	end

	if trace then
		formattedTemplate ..= `\n---- Stack trace ----\n{trace}`
	end

	-- Labels are added from createLogger, so we don't add it here
	return string.gsub(formattedTemplate, "\n", "\n    ")
end

return formatLog


local allowWebLinks = require("@self/allow-web-links")
local create = require("@self/create")
local formatLog = require("@self/format-log")
local parseError = require("@self/parse-error")
local standardErrorInfoUrl = require("@self/standard-error-info-url")
local types = require("@self/types")

export type Logger<Templates> = types.Logger<Templates>
export type Log = types.Log
export type Error = types.Error

return table.freeze({
	create = create,
	formatLog = formatLog,
	parseError = parseError,
	allowWebLinks = allowWebLinks,
	standardErrorInfoUrl = standardErrorInfoUrl,
})


local types = require("./types")

local function parseError(err: string): types.Error
	return {
		type = "Error",
		raw = err,
		message = err:gsub("^.+:%d+:%s*", ""),
		trace = debug.traceback(nil, 2),
	}
end

return parseError


local configure = require("@lune-lib/configure")

return configure.package({
	name = "logger",
	description = "Logging for Prvd 'M Wrong",

	dependencies = configure.dependencies({
		runtime = true,
	}),

	types = {
		["Logger<Templates>"] = true,
		Log = true,
		Error = true,
	},
})


local function standardErrorInfoUrl(label: string): string
	return `https://prvdmwrong.luau.page/api-reference/{label}/errors`
end

return standardErrorInfoUrl


export type Error = {
	type: "Error",
	raw: string,
	message: string,
	trace: string,
}

export type Log = {
	template: string,
	error: Error?,
	trace: string?,
	[number]: unknown,
}

export type Logger<Templates> = Templates & {
	type: "Logger",
	print: (self: Logger<Templates>, props: Log) -> (),
	warn: (self: Logger<Templates>, props: Log) -> (),
	error: (self: Logger<Templates>, props: Log) -> (),
	fatalError: (self: Logger<Templates>, props: Log) -> never,
}

return nil


local nameOf = require("./name-of")
local providerClasses = require("./provider-classes")
local types = require("./types")

local function createProvider<Self>(self: Self): types.Provider<Self>
	local provider: types.Provider<Self> = self :: any

	if provider.__index == nil then
		provider.__index = provider
	end

	if provider.__tostring == nil then
		provider.__tostring = nameOf
	end

	if provider.new == nil then
		function provider.new(dependencies)
			local self: types.Provider<Self> = setmetatable({}, provider) :: any
			if provider.constructor then
				return provider.constructor(self, dependencies) or self
			end
			return self
		end
	end

	providerClasses[provider] = true
	return provider
end

return createProvider


local create = require("@self/create")
local nameOf = require("@self/name-of")
local providerClasses = require("@self/provider-classes")
local types = require("@self/types")

export type Provider<Self, Dependencies = nil> = types.Provider<Self, Dependencies>

local providers = table.freeze({
	create = create,
	nameOf = nameOf,
	_ = table.freeze({
		providerClasses = providerClasses,
	}),
})

return providers


local types = require("./types")

local function nameOf(provider: types.Provider<any, any>)
	return provider.name or "Provider"
end

return nameOf


local types = require("./types")

type Set<T> = { [T]: true }

local providerClasses: Set<types.Provider<any>> = setmetatable({}, { __mode = "k" }) :: any

return providerClasses


local configure = require("@lune-lib/configure")

return configure.package({
	name = "providers",
	description = "Provider creation for Prvd 'M Wrong",

	types = {
		["Provider<Self, Dependencies = nil>"] = "Provider<Self, Dependencies>",
	},

	dependencies = configure.dependencies({
		dependencies = true,
		logger = true,
		lifecycles = true,
		runtime = true,
	}),
})


local dependencies = require("../dependencies")

export type Provider<Self, Dependencies = nil> = dependencies.Dependency<Self & {
	__index: Provider<Self, Dependencies>,
	__tostring: (self: Provider<Self, Dependencies>) -> string,
	name: string?,
	constructor: ((self: Provider<Self, Dependencies>, dependencies: Dependencies) -> ())?,
	start: (self: Provider<Self, Dependencies>) -> ()?,
	destroy: (self: Provider<Self, Dependencies>) -> ()?,
}, Dependencies>

return nil


local dependencies = require("./dependencies")
local lifecycles = require("./lifecycles")
local providers = require("./providers")
local roots = require("./roots")

export type Provider<Self, Dependencies = nil> = providers.Provider<Self, Dependencies>
export type Lifecycle<Args... = ...unknown> = lifecycles.Lifecycle<Args...>
export type Root = roots.Root
export type StartedRoot = roots.StartedRoot

local prvd = {
	provider = providers.create,
	root = roots.create,
	depend = dependencies.depend,
	nameOf = providers.nameOf,

	lifecycle = lifecycles.create,
	fireConcurrent = lifecycles.handlers.fireConcurrent,
	fireSequential = lifecycles.handlers.fireSequential,

	hooks = table.freeze({
		onProviderUsed = roots.hooks.onProviderUsed,
		onLifecycleUsed = roots.hooks.onLifecycleUsed,
		onRootConstructing = roots.hooks.onRootConstructing,
		onRootUsed = roots.hooks.onRootUsed,
		onRootStarted = roots.hooks.onRootStarted,
		onRootFinished = roots.hooks.onRootFinished,
		onRootDestroyed = roots.hooks.onRootDestroyed,

		onLifecycleConstructed = lifecycles.hooks.onLifecycleConstructed,
		onLifecycleDestroyed = lifecycles.hooks.onLifecycleDestroyed,
		onLifecycleRegistered = lifecycles.hooks.onLifecycleRegistered,
		onLifecycleUnregistered = lifecycles.hooks.onLifecycleUnregistered,
	}),
}

type PrvdModule = (<Self>(provider: Self) -> Provider<Self, any>) & typeof(prvd)

local mt = {}
function mt.__call<Self>(_, provider: Self): providers.Provider<Self, any>
	return providers.create(provider) :: any
end

local module: PrvdModule = setmetatable(prvd, mt) :: any
return module


local configure = require("@lune-lib/configure")

return configure.package({
	name = "prvdmwrong",
	description = "Entry point to Prvd 'M Wrong",

	types = {
		["Lifecycle<Args... = ...unknown>"] = "Lifecycle<Args...>",
		["Provider<Self, Dependencies = nil>"] = "Provider<Self, Dependencies>",
		Root = true,
		StartedRoot = true,
	},

	dependencies = configure.dependencies({
		dependencies = true,
		lifecycles = true,
		providers = true,
		roots = true,
	}),
})


local types = require("./types")

type Set<T> = { [T]: true }

local componentClasses: Set<types.Component<any>> = setmetatable({}, { __mode = "k" }) :: any

return componentClasses


local CollectionService = game:GetService("CollectionService")

local componentClasses = require("./component-classes")
local logger = require("./logger")
local providers = require("../providers")
local runtime = require("../runtime")
local types = require("./types")

local threadpool = runtime.threadpool

type Set<T> = { [T]: true }
type ConstructedComponent = types.AnyComponent
type LuauBug = any

local ComponentProvider = {}
ComponentProvider.priority = -math.huge
ComponentProvider.name = "@rbx-components"

function ComponentProvider.constructor(self: ComponentProvider)
	self.componentClasses = {} :: Set<types.AnyComponent>
	self.tagToComponentClasses = {} :: { [string]: Set<types.AnyComponent> }
	self.instanceToComponents = {} :: { [Instance]: { [types.AnyComponent]: ConstructedComponent } }
	self.componentToInstances = {} :: { [types.AnyComponent]: Set<Instance>? }
	self.constructedToClass = {} :: { [ConstructedComponent]: types.AnyComponent? }

	self._destroyingConnections = {} :: { [Instance]: RBXScriptConnection }
	self._connections = {} :: { RBXScriptConnection }
end

function ComponentProvider.start(self: ComponentProvider)
	for tag, components in self.tagToComponentClasses do
		local function onTagAdded(instance)
			local instanceToComponents = self.instanceToComponents[instance]
			if not instanceToComponents then
				instanceToComponents = {}
				self.instanceToComponents[instance] = instanceToComponents
			end

			for componentClass in components do
				if
					(componentClass.blacklistInstances and (componentClass.blacklistInstances :: LuauBug)[instance])
					or (componentClass.whitelistInstances and not (componentClass.whitelistInstances :: LuauBug)[instance])
					or (componentClass.instanceCheck and not componentClass.instanceCheck(instance))
				then
					continue
				end

				local component: ConstructedComponent = instanceToComponents[componentClass]
				if not component then
					component = componentClass.new(instance)
					instanceToComponents[componentClass] = component
					self.constructedToClass[component :: any] = componentClass
				end

				if component.added then
					component:added()
				end
			end

			if not self._destroyingConnections[instance] then
				self._destroyingConnections[instance] = instance.Destroying:Once(function()
					for _, component in instanceToComponents do
						if component.destroy then
							threadpool.spawn(component.destroy, component)
						end
					end
					table.clear(instanceToComponents)
					self.instanceToComponents[instance] = nil
				end)
			end
		end

		table.insert(self._connections, CollectionService:GetInstanceAddedSignal(tag):Connect(onTagAdded))
		table.insert(self._connections, CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance) end))

		for _, instance in CollectionService:GetTagged(tag) do
			onTagAdded(instance)
		end
	end
end

function ComponentProvider.destroy(self: ComponentProvider)
	for _, components in self.instanceToComponents do
		for _, component in components do
			if component.destroy then
				component:destroy()
			end
		end
		table.clear(components)
	end

	table.clear(self.componentClasses)
	table.clear(self.tagToComponentClasses)
	table.clear(self.instanceToComponents)
	table.clear(self.componentToInstances)
	table.clear(self.constructedToClass)

	for _, connection in self._connections do
		if connection.Connected then
			connection:Disconnect()
		end
	end

	for _, connection in self._destroyingConnections do
		if connection.Connected then
			connection:Disconnect()
		end
	end

	table.clear(self._connections)
	table.clear(self._destroyingConnections)
end

function ComponentProvider.addComponentClass(self: ComponentProvider, class: types.AnyComponent)
	if not componentClasses[class] then
		logger:fatalError({ template = logger.invalidComponent })
	end

	if self.componentClasses[class] then
		logger:fatalError({ template = logger.alreadyRegisteredComponent })
	end

	if class.tag then
		local tagToComponentClasses = self.tagToComponentClasses[class.tag]

		if tagToComponentClasses then
			if not _G.PRVDMWRONG_SUPPRESS_MULTIPLE_SAME_TAG and #tagToComponentClasses > 0 then
				logger:warn({ template = logger.multipleSameTag, class.tag })
			end
		else
			tagToComponentClasses = {}
			self.tagToComponentClasses[class.tag] = tagToComponentClasses
		end

		(tagToComponentClasses :: any)[class] = true
	end

	self.componentClasses[class] = true
	self.componentToInstances[class] = {}
end

function ComponentProvider.getFromInstance<C, I>(
	self: ComponentProvider,
	class: types.Component<C, I>,
	instance: I
): types.Component<C, I>?
	return self.instanceToComponents[instance :: any][class]
end

function ComponentProvider.getAllComponents<C, I>(
	self: ComponentProvider,
	class: types.Component<C, I>
): Set<types.Component<C, I>>
	local result = {}
	for _, components in self.instanceToComponents do
		for _, component in components do
			if self.constructedToClass[component] == class then
				result[component] = true
			end
		end
	end
	return result :: any
end

function ComponentProvider.addComponent<C, I>(
	self: ComponentProvider,
	class: types.Component<C, I>,
	instance: I
): types.Component<C, I>?
	error("not yet implemented")
end

function ComponentProvider.removeComponent<C, I>(self: ComponentProvider, class: types.Component<C, I>, instance: I)
	local constructed = self:getFromInstance(class, instance)
	if constructed then
		self.instanceToComponents[instance :: any][class] = nil

		if constructed.removed then
			threadpool.spawn(constructed.removed :: any, constructed, instance)
		end
	end
end

export type ComponentProvider = typeof(ComponentProvider)
return providers.create(ComponentProvider)


local componentClasses = require("./component-classes")
local types = require("./types")

-- TODO: prvdmwrong/classes package?
local function create<Self>(self: Self): types.Component<Self, any>
	local component: types.Component<Self> = self :: any

	if component.__index == nil then
		component.__index = component
	end

	-- TODO: abstract nameOf

	if component.new == nil then
		function component.new(instance: any)
			local self: types.Component<Self> = setmetatable({}, component) :: any
			if component.constructor then
				return component.constructor(self, instance) or self
			end
			return self
		end
	end

	componentClasses[component] = true
	return component
end

return create


local ComponentProvider = require("../component-provider")
local providers = require("../../providers")
local types = require("../types")

local ComponentClassProvider = {}
ComponentClassProvider.priority = -math.huge
ComponentClassProvider.name = "@rbx-components.ComponentClassProvider"
ComponentClassProvider.dependencies = table.freeze({
	componentProvider = ComponentProvider,
})

ComponentClassProvider._components = {} :: { types.AnyComponent }

function ComponentClassProvider.constructor(
	self: ComponentClassProvider,
	dependencies: typeof(ComponentClassProvider.dependencies)
)
	for _, class in self._components do
		dependencies.componentProvider:addComponentClass(class)
	end
end

export type ComponentClassProvider = typeof(ComponentClassProvider)

local function createComponentClassProvider(components: { types.AnyComponent })
	local provider = table.clone(ComponentClassProvider)
	provider._components = components
	return providers.create(provider)
end

return createComponentClassProvider


local ComponentProvider = require("./component-provider")
local createComponentClassProvider = require("@self/create-component-class-provider")
local logger = require("./logger")
local roots = require("../roots")
local types = require("./types")
local useComponent = require("@self/use-component")
local useComponents = require("@self/use-components")
local useModuleAsComponent = require("@self/use-module-as-component")
local useModulesAsComponents = require("@self/use-modules-as-components")

local function extendRoot(root: types.RootPrivate): types.Root
	if root._hasComponentExtensions then
		return logger:fatalError({ template = logger.alreadyExtendedRoot })
	end

	root._hasComponentExtensions = true
	root._classes = {}

	root:useProvider(ComponentProvider)
	root:useProvider(createComponentClassProvider(root._classes))

	root.useComponent = useComponent :: any
	root.useComponents = useComponents :: any
	root.useModuleAsComponent = useModuleAsComponent :: any
	root.useModulesAsComponents = useModulesAsComponents :: any

	return root
end

return extendRoot :: (root: roots.Root) -> types.Root


local types = require("../types")

local function useComponent(root: types.RootPrivate, component: types.AnyComponent)
	table.insert(root._classes, component)
	return root
end

return useComponent


local types = require("../types")

local function useComponents(root: types.RootPrivate, components: { types.AnyComponent })
	for index = 1, #components do
		table.insert(root._classes, components[index])
	end
	return root
end

return useComponents


local componentClasses = require("../component-classes")
local types = require("../types")

local function useModuleAsComponent(root: types.RootPrivate, module: ModuleScript)
	local required = (require)(module)
	if componentClasses[required] then
		table.insert(root, required)
	end
	return root
end

return useModuleAsComponent


local componentClasses = require("../component-classes")
local types = require("../types")

local function useModulesAsComponents(
	root: types.RootPrivate,
	modules: { Instance },
	predicate: ((ModuleScript) -> boolean)?
)
	for index = 1, #modules do
		local module = modules[index]

		if not module:IsA("ModuleScript") then
			continue
		end

		if predicate and not predicate(module) then
			continue
		end

		local required = (require)(module)
		if componentClasses[required] then
			table.insert(root, required)
		end
	end
	return root
end

return useModulesAsComponents


local componentClasses = require("@self/component-classes")
local create = require("@self/create")
local extendRoot = require("@self/extend-root")
local types = require("@self/types")

export type Component<Self, Instance = Instance> = types.Component<Self, Instance>
export type Root = types.Root

local components = {
	create = create,
	extendRoot = extendRoot,
	_ = table.freeze({
		componentClasses = componentClasses,
	}),
}

local mt = {}

function mt.__call<Self>(_, component: Self): types.Component<Self, any>
	return create(component)
end

table.freeze(mt)
return table.freeze(setmetatable(components, mt))


local logger = require("../logger")

return logger.create("@prvdmwrong/rbx-components", logger.standardErrorInfoUrl("rbx-components"), {
	alreadyExtendedRoot = "Root already has component extension.",
	invalidComponent = "Not a component, create one with `components(MyComponent)` or `components.create(MyComponent)`.",
	alreadyRegisteredComponent = "Component already registered.",
	multipleSameTag = "Multiple components use the CollectionService tag '%s', which is likely a mistake. Supress this warning with `_G.PRVDMWRONG_SUPPRESS_MULTIPLE_SAME_TAG = true`.",
})


local configure = require("@lune-lib/configure")

return configure.package({
	name = "rbx-components",
	description = "Component functionality for Prvd 'M Wrong",

	unreleased = true,

	dependencies = configure.dependencies({
		logger = true,
		roots = true,
		providers = true,
		dependencies = true,
	}),
})


local dependencies = require("../dependencies")
local roots = require("../roots")

export type Component<Self, Instance = Instance> = dependencies.Dependency<Self & {
	__index: Component<Self, Instance>,
	tag: string?,
	instanceCheck: (unknown) -> boolean,
	blacklistInstances: { Instance }?,
	whitelistInstances: { Instance }?,
	constructor: (self: Component<Self, Instance>, instance: Instance) -> ()?,
	added: (self: Component<Self, Instance>) -> ()?,
	removed: (self: Component<Self, Instance>) -> ()?,
	destroyed: (self: Component<Self, Instance>) -> ()?,
}, nil, Instance>

export type Root = roots.Root & {
	useModuleAsComponent: (root: Root, module: ModuleScript) -> Root,
	useModulesAsComponents: (root: Root, modules: { Instance }, predicate: ((ModuleScript) -> boolean)?) -> Root,
	useComponent: (root: Root, component: Component<any, any>) -> Root,
	useComponents: (root: Root, components: { Component<any, any> }) -> Root,
}

export type RootPrivate = Root & {
	_hasComponentExtensions: true?,
	_classes: { Component<any, any> },
}

export type AnyComponent = Component<any, any>

return nil


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

local prvd = require("./prvdmwrong")

local fireConcurrent = prvd.fireConcurrent

local RbxLifecycles = {}
RbxLifecycles.name = "@prvdmwrong/rbx-lifecycles"
RbxLifecycles.priority = -math.huge

RbxLifecycles.preSimulation = prvd.lifecycle("preSimulation", fireConcurrent) :: prvd.Lifecycle<number>
RbxLifecycles.postSimulation = prvd.lifecycle("postSimulation", fireConcurrent) :: prvd.Lifecycle<number>
RbxLifecycles.preAnimation = prvd.lifecycle("preAnimation", fireConcurrent) :: prvd.Lifecycle<number>
RbxLifecycles.preRender = prvd.lifecycle("preRender", fireConcurrent) :: prvd.Lifecycle<number>
RbxLifecycles.playerAdded = prvd.lifecycle("playerAdded", fireConcurrent) :: prvd.Lifecycle<Player>
RbxLifecycles.playerRemoving = prvd.lifecycle("playerRemoving", fireConcurrent) :: prvd.Lifecycle<Player>

local function wrapLifecycle(lifecycle: prvd.Lifecycle<...unknown>)
	return function(...: unknown)
		lifecycle:fire(...)
	end
end

function RbxLifecycles.constructor(self: RbxLifecycles)
	self.connections = {} :: { RBXScriptConnection }

	self:_addConnections(
		RunService.PreSimulation:Connect(wrapLifecycle(RbxLifecycles.preSimulation :: any)),
		RunService.PostSimulation:Connect(wrapLifecycle(RbxLifecycles.postSimulation :: any)),
		RunService.PreAnimation:Connect(wrapLifecycle(RbxLifecycles.preAnimation :: any))
	)

	if RunService:IsClient() then
		self:_addConnections(RunService.PreRender:Connect(wrapLifecycle(RbxLifecycles.preRender :: any)))
	end

	self:_addConnections(
		Players.PlayerAdded:Connect(wrapLifecycle(RbxLifecycles.playerAdded :: any)),
		Players.PlayerRemoving:Connect(wrapLifecycle(RbxLifecycles.playerRemoving :: any))
	)
end

function RbxLifecycles.finish(self: RbxLifecycles)
	for _, connection in self.connections do
		if connection.Connected then
			connection:Disconnect()
		end
	end
	table.clear(self.connections)
end

function RbxLifecycles._addConnections(self: RbxLifecycles, ...: RBXScriptConnection)
	for index = 1, select("#", ...) do
		table.insert(self.connections, select(index, ...) :: any)
	end
end

-- Needed so typescript can require the provider as:
--
-- ```ts
-- import { RbxLifecycles } from "@rbxts/rbx-lifecycles"
-- ```
RbxLifecycles.RbxLifecycles = RbxLifecycles

export type RbxLifecycles = typeof(RbxLifecycles)
return prvd(RbxLifecycles)


local configure = require("@lune-lib/configure")

return configure.package({
	name = "rbx-lifecycles",
	description = "Lifecycles implementations for Roblox",

	pesdeTargets = {
		roblox = true,
	},

	dependencies = configure.dependencies({
		prvdmwrong = true,
	}),
})


local destroy = require("./methods/destroy")
local lifecycles = require("../lifecycles")
local rootConstructing = require("./hooks/root-constructing")
local start = require("./methods/start")
local types = require("./types")
local useModule = require("./methods/use-module")
local useModules = require("./methods/use-modules")
local useProvider = require("./methods/use-provider")
local useProviders = require("./methods/use-providers")
local useRoot = require("./methods/use-root")
local useRoots = require("./methods/use-roots")
local willFinish = require("./methods/will-finish")
local willStart = require("./methods/will-start")

local function create(): types.Root
	local self: types.Self = {
		type = "Root",

		start = start,

		useProvider = useProvider,
		useProviders = useProviders,
		useModule = useModule,
		useModules = useModules,
		useRoot = useRoot,
		useRoots = useRoots,
		destroy = destroy,

		willStart = willStart,
		willFinish = willFinish,

		_destroyed = false,
		_rootProviders = {},
		_rootLifecycles = {},
		_subRoots = {},
		_start = lifecycles.create("start", lifecycles.handlers.fireConcurrent),
		_finish = lifecycles.create("destroy", lifecycles.handlers.fireSequential),
	} :: any

	for _, callback in rootConstructing.callbacks do
		callback(self)
	end

	return self
end

return create


local lifecycles = require("../../lifecycles")
local types = require("../types")

local callbacks: { (root: types.Self, provider: lifecycles.Lifecycle) -> () } = {}

local function onLifecycleUsed(listener: (root: types.Root, lifecycle: lifecycles.Lifecycle) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onLifecycleUsed = onLifecycleUsed,
})


local providers = require("../../providers")
local types = require("../types")

local callbacks: { (root: types.Self, provider: providers.Provider<unknown>) -> () } = {}

local function onProviderUsed(listener: (root: types.Root, provider: providers.Provider<unknown>) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onProviderUsed = onProviderUsed,
})


local types = require("../types")

local callbacks: { (root: types.Self) -> () } = {}

local function onRootConstructing(listener: (root: types.Root) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onRootConstructing = onRootConstructing,
})


local types = require("../types")

local callbacks: { (root: types.Self) -> () } = {}

local function onRootDestroyed(listener: (root: types.Root) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onRootDestroyed = onRootDestroyed,
})


local types = require("../types")

local callbacks: { (root: types.Self) -> () } = {}

local function onRootFinished(listener: (root: types.Root) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onRootFinished = onRootFinished,
})


local types = require("../types")

local callbacks: { (root: types.Self) -> () } = {}

local function onRootStarted(listener: (root: types.Root) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onRootStarted = onRootStarted,
})


local types = require("../types")

local callbacks: { (root: types.Self, subRoot: types.Root) -> () } = {}

local function onRootUsed(listener: (root: types.Root, subRoot: types.Root) -> ()): () -> ()
	table.insert(callbacks, listener)
	return function()
		table.remove(callbacks, table.find(callbacks, listener))
	end
end

return table.freeze({
	callbacks = callbacks,
	onRootUsed = onRootUsed,
})


local create = require("@self/create")
local lifecycleUsed = require("@self/hooks/lifecycle-used")
local providerUsed = require("@self/hooks/provider-used")
local rootConstructing = require("@self/hooks/root-constructing")
local rootDestroyed = require("@self/hooks/root-destroyed")
local rootFinished = require("@self/hooks/root-finished")
local rootStarted = require("@self/hooks/root-started")
local rootUsed = require("@self/hooks/root-used")
local types = require("@self/types")

export type Root = types.Root
export type StartedRoot = types.StartedRoot

local roots = table.freeze({
	create = create,
	hooks = table.freeze({
		onLifecycleUsed = lifecycleUsed.onLifecycleUsed,
		onProviderUsed = providerUsed.onProviderUsed,
		onRootUsed = rootUsed.onRootUsed,

		onRootConstructing = rootConstructing.onRootConstructing,
		onRootStarted = rootStarted.onRootStarted,
		onRootFinished = rootFinished.onRootFinished,
		onRootDestroyed = rootDestroyed.onRootDestroyed,
	}),
})

return roots


local logger = require("../logger")

return logger.create("@prvdmwrong/roots", logger.standardErrorInfoUrl("roots"), {
	useAfterDestroy = "Cannot use Root after it is destroyed.",
	alreadyDestroyed = "Cannot destroy an already destroyed Root.",
	alreadyFinished = "Root has already finished.",
})


local logger = require("../logger")
local types = require("../types")

local function destroy(root: types.Self)
	if root._destroyed then
		logger:fatalError({ template = logger.alreadyDestroyed })
	end
	root._destroyed = true
	table.clear(root._rootLifecycles)
	table.clear(root._rootProviders)
	table.clear(root._subRoots)
	root._start:clear()
	root._finish:clear()
end

return destroy


local dependencies = require("../../dependencies")
local lifecycles = require("../../lifecycles")
local logger = require("../logger")
local providers = require("../../providers")
local rootFinished = require("../hooks/root-finished").callbacks
local rootStarted = require("../hooks/root-started").callbacks
local types = require("../types")

local function bindLifecycle(lifecycle: lifecycles.Lifecycle<...any>, provider: providers.Provider<any>)
	local lifecycleMethod = (provider :: any)[lifecycle.method]
	if typeof(lifecycleMethod) == "function" then
		-- local isProfiling = _G.PRVDMWRONG_PROFILE_LIFECYCLES

		-- if isProfiling then
		-- 	lifecycle:register(function(...)
		-- 		debug.profilebegin(memoryCategory)
		-- 		debug.setmemorycategory(memoryCategory)
		-- 		lifecycleMethod(provider, ...)
		-- 		debug.resetmemorycategory()
		-- 		debug.profileend()
		-- 	end)
		-- else
		lifecycle:register(function(...)
			lifecycleMethod(provider, ...)
		end)
		-- end
	end
end

local function nameOf(provider: providers.Provider<unknown>, extension: string?): string
	return if extension then `{tostring(provider)}.{extension}` else tostring(provider)
end

local function start(root: types.Self): types.StartedRoot
	local processed = dependencies.processDependencies(root._rootProviders)
	local sortedProviders = processed.sortedDependencies :: { providers.Provider<any> }
	local processedLifecycles = processed.lifecycles
	table.move(root._rootLifecycles, 1, #root._rootLifecycles, #processedLifecycles + 1, processedLifecycles)

	dependencies.sortByPriority(sortedProviders)

	local constructedProviders = {}
	for _, provider in sortedProviders do
		local providerDependencies = (provider :: any).dependencies
		if typeof(providerDependencies) == "table" then
			providerDependencies = table.clone(providerDependencies)
			for key, value in providerDependencies do
				providerDependencies[key] = constructedProviders[value]
			end
		end

		local constructed = provider.new(providerDependencies)
		constructedProviders[provider] = constructed
		bindLifecycle(root._start, constructed)
		bindLifecycle(root._finish, constructed)

		for _, lifecycle in processedLifecycles do
			bindLifecycle(lifecycle, constructed)
		end
	end

	local subRoots: { types.StartedRoot } = {}
	for root in root._subRoots do
		table.insert(subRoots, root:start())
	end

	root._start:fire()

	local startedRoot = {} :: types.StartedRoot
	startedRoot.type = "StartedRoot"

	local didFinish = false

	function startedRoot:finish()
		if didFinish then
			return logger:fatalError({ template = logger.alreadyFinished })
		end

		didFinish = true
		root._finish:fire()

		-- NOTE(znotfireman): Assume the user cleans up their own lifecycles
		root._start:clear()
		root._finish:clear()

		for _, subRoot in subRoots do
			subRoot:finish()
		end

		for _, hook in rootFinished do
			hook(root)
		end
	end

	for _, hook in rootStarted do
		hook(root)
	end

	return startedRoot
end

return start


local lifecycleUsed = require("../hooks/lifecycle-used").callbacks
local lifecycles = require("../../lifecycles")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function useLifecycle(root: types.Self, lifecycle: lifecycles.Lifecycle)
	table.insert(root._rootLifecycles, lifecycle)
	threadpool.spawnCallbacks(lifecycleUsed, root, lifecycle)
	return root
end

return useLifecycle


local lifecycleUsed = require("../hooks/lifecycle-used").callbacks
local lifecycles = require("../../lifecycles")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function useLifecycles(root: types.Self, lifecycles: { lifecycles.Lifecycle })
	for _, lifecycle in lifecycles do
		table.insert(root._rootLifecycles, lifecycle)
		threadpool.spawnCallbacks(lifecycleUsed, root, lifecycle)
	end
	return root
end

return useLifecycles


local providerUsed = require("../hooks/provider-used").callbacks
local providers = require("../../providers")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool
local providerClasses = providers._.providerClasses

local function useModule(root: types.Self, module: ModuleScript)
	local moduleExport = (require)(module)

	if providerClasses[moduleExport] then
		root._rootProviders[moduleExport] = true
		if typeof(moduleExport.name) ~= "string" then
			moduleExport.name = module.Name
		end
		threadpool.spawnCallbacks(providerUsed, root, moduleExport)
	end

	return root
end

return useModule


local providerUsed = require("../hooks/provider-used").callbacks
local providers = require("../../providers")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool
local providerClasses = providers._.providerClasses

local function useModules(root: types.Self, modules: { Instance }, predicate: ((module: ModuleScript) -> boolean)?)
	for _, module in modules do
		if not module:IsA("ModuleScript") or predicate and not predicate(module) then
			continue
		end

		local moduleExport = (require)(module)
		if providerClasses[moduleExport] then
			root._rootProviders[moduleExport] = true
			if typeof(moduleExport.name) ~= "string" then
				moduleExport.name = module.Name
			end
			threadpool.spawnCallbacks(providerUsed, root, moduleExport)
		end
	end

	return root
end

return useModules


local providerUsed = require("../hooks/provider-used").callbacks
local providers = require("../../providers")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function useProvider(root: types.Self, provider: providers.Provider<unknown>)
	root._rootProviders[provider] = true
	threadpool.spawnCallbacks(providerUsed, root, provider)
	return root
end

return useProvider


local providerUsed = require("../hooks/provider-used").callbacks
local providers = require("../../providers")
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function useProviders(root: types.Self, providers: { providers.Provider<unknown> })
	for _, provider in providers do
		root._rootProviders[provider] = true
		threadpool.spawnCallbacks(providerUsed, root, provider)
	end
	return root
end

return useProviders


local rootUsed = require("../hooks/root-used").callbacks
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function useRoot(root: types.Self, subRoot: types.Root)
	root._subRoots[subRoot] = true
	threadpool.spawnCallbacks(rootUsed, root, subRoot)
	return root
end

return useRoot


local rootUsed = require("../hooks/root-used").callbacks
local runtime = require("../../runtime")
local types = require("../types")

local threadpool = runtime.threadpool

local function useRoots(root: types.Self, subRoots: { types.Root })
	for _, subRoot in subRoots do
		root._subRoots[subRoot] = true
		threadpool.spawnCallbacks(rootUsed, root, subRoot)
	end
	return root
end

return useRoots


local types = require("../types")

local function willFinish(root: types.Self, callback: () -> ())
	root._finish:register(callback)
	return root
end

return willFinish


local types = require("../types")

local function willStart(root: types.Self, callback: () -> ())
	root._start:register(callback)
	return root
end

return willStart


local configure = require("@lune-lib/configure")

return configure.package({
	name = "roots",
	description = "Roots implementation for Prvd 'M Wrong",

	types = {
		Root = true,
		StartedRoot = true,
	},

	dependencies = configure.dependencies({
		dependencies = true,
		logger = true,
		lifecycles = true,
		runtime = true,
		providers = true,
	}),
})


local lifecycles = require("../lifecycles")
local providers = require("../providers")

type Set<T> = { [T]: true }

export type Root = {
	type: "Root",

	start: (root: Root) -> StartedRoot,

	useModule: (root: Root, module: ModuleScript) -> Root,
	useModules: (root: Root, modules: { Instance }, predicate: ((ModuleScript) -> boolean)?) -> Root,
	useRoot: (root: Root, root: Root) -> Root,
	useRoots: (root: Root, roots: { Root }) -> Root,
	useProvider: (root: Root, provider: providers.Provider<unknown>) -> Root,
	useProviders: (root: Root, providers: { providers.Provider<unknown> }) -> Root,
	useLifecycle: (root: Root, lifecycle: lifecycles.Lifecycle) -> Root,
	useLifecycles: (root: Root, lifecycles: { lifecycles.Lifecycle }) -> Root,
	destroy: (root: Root) -> (),

	willStart: (callback: () -> ()) -> Root,
	willFinish: (callback: () -> ()) -> Root,
}

export type StartedRoot = {
	type: "StartedRoot",

	finish: (root: StartedRoot) -> (),
}

export type Self = Root & {
	_destroyed: boolean,
	_rootProviders: Set<providers.Provider<unknown>>,
	_rootLifecycles: { lifecycles.Lifecycle },
	_subRoots: Set<Root>,
	_start: lifecycles.Lifecycle<()>,
	_finish: lifecycles.Lifecycle<()>,
}

return nil


local task = require("../libs/task")

local freeThreads: { thread } = {}

local function run<T...>(f: (T...) -> (), thread: thread, ...)
	f(...)
	table.insert(freeThreads, thread)
end

local function yielder()
	while true do
		run(coroutine.yield())
	end
end

local function spawn<T...>(f: (T...) -> (), ...: T...)
	local thread
	if #freeThreads > 0 then
		thread = freeThreads[#freeThreads]
		freeThreads[#freeThreads] = nil
	else
		thread = coroutine.create(yielder)
		coroutine.resume(thread)
	end

	task.spawn(thread, f, thread, ...)
end

local function spawnCallbacks<Args...>(callbacks: { (Args...) -> () }, ...: Args...)
	for _, callback in callbacks do
		spawn(callback, ...)
	end
end

return table.freeze({
	spawn = spawn,
	spawnCallbacks = spawnCallbacks,
})