--[=[
	@class PromiseQueue
	@private

	Implements a queue that processes asynchronous operations (represented by functions
	returning Promises or synchronous functions) one at a time, ensuring serial execution.

	**Purpose:** Useful for scenarios where operations on a shared resource must not
	run concurrently to prevent race conditions or ensure logical order. For example,
	processing updates to a specific player's data session.

	**Core Logic:**
	- Operations are added to the queue via the `add` method.
	- Each operation is wrapped in a `Promise`.
	- A single processing loop ([PromiseQueue:_processQueue]) runs as long as the queue is not empty.
	- The loop takes the next item, executes its associated function, and waits for
	  its Promise to resolve or reject before moving to the next item.
	- Includes timeout and deadlock detection for individual items.
	- Supports adding an operation atomically across multiple queues ([PromiseQueue:multiQueueAdd]).
]=]

local Log = require(script.Parent.Log)
local Promise = require(script.Parent.Promise)
local Tables = require(script.Parent.Tables)

--[=[
	Internal implementation details and methods for the PromiseQueue class.
	@interface PromiseQueueImpl
	@within PromiseQueue
	@private
	.__index PromiseQueueImpl
	.new (params: CreatePromiseQueueParams) -> PromiseQueue
	._processQueue (self: PromiseQueue) -> ()
	._addResumableBlock (queue: PromiseQueue) -> Promise<() -> ()>
	._getLogContext (self: PromiseQueue, item: QueueItem?) -> { [string]: any }
	.add (self: PromiseQueue, callback: () -> ()) -> Promise
	.multiQueueAdd (queues: { PromiseQueue }, callback: () -> ()) -> Promise -- Public static method
]=]
type PromiseQueueImpl = {
	__index: PromiseQueueImpl,
	new: (params: CreatePromiseQueueParams) -> PromiseQueue,
	_processQueue: (self: PromiseQueue) -> (),
	_addResumableBlock: (queue: PromiseQueue) -> Promise.TPromise<() -> ()>,
	_getLogContext: (self: PromiseQueue, item: QueueItem?) -> { [string]: any },
	multiQueueAdd: (queues: { PromiseQueue }, callback: () -> ()) -> Promise.Promise,
	add: (self: PromiseQueue, callback: () -> ()) -> Promise.Promise,
}

--[=[
	Internal properties stored within a PromiseQueue instance.
	@interface PromiseQueueProps
	@within PromiseQueue
	@private
	._queue { QueueItem } -- The actual queue holding items to be processed.
	._logger Log.Logger -- Logger instance for internal logging.
	._totalItemCount number -- Counter for assigning unique IDs to queue items.
]=]
type PromiseQueueProps = {
	_queue: { QueueItem },
	_logger: Log.Logger,
	_totalItemCount: number,
}

--[=[
	Represents a single item within the queue.
	@interface QueueItem
	@within PromiseQueue
	@private
	.id number -- Unique identifier for the item within this queue instance.
	.fn () -> () -- The function to execute for this item. Can be sync or return a Promise.
	.resolve (value: any) -> () -- The resolve function of the Promise returned by `add`.
	.reject (error: any) -> () -- The reject function of the Promise returned by `add`.
	.trace string -- Debug traceback captured when the item was added.
]=]
type QueueItem = {
	id: number,
	fn: () -> (),
	resolve: (value: any) -> (),
	reject: (error: any) -> (),
	trace: string,
}

export type PromiseQueue = typeof(setmetatable({} :: PromiseQueueProps, {} :: PromiseQueueImpl))

local PromiseQueue = {} :: PromiseQueueImpl
PromiseQueue.__index = PromiseQueue

--[=[
	Parameters for creating a new PromiseQueue instance.
	@interface CreatePromiseQueueParams
	@within PromiseQueue
	.logger Log.Logger
]=]
type CreatePromiseQueueParams = {
	logger: Log.Logger,
}

--[=[
	Creates a new PromiseQueue instance.
	@param params -- Configuration parameters.
	@return PromiseQueue -- A new PromiseQueue object.
	@within PromiseQueue
]=]
function PromiseQueue.new(params: CreatePromiseQueueParams): PromiseQueue
	return setmetatable({
		_queue = {},
		_logger = params.logger,
		_totalItemCount = 0,
	}, PromiseQueue) :: PromiseQueue
end

--[=[
	Adds a new operation (callback function) to the end of the queue.

	The callback will be executed only after all preceding items in the queue
	have completed.

	@param callback () -> T -- The function to execute. This function can be synchronous or return a Promise. Its result or error will resolve/reject the Promise returned by this `add` call.
	@return Promise<T> -- A Promise that resolves or rejects with the result or error of the provided `callback` function once it's processed by the queue.
	@within PromiseQueue
]=]
function PromiseQueue:add(callback: () -> ()): Promise.Promise
	local trace = debug.traceback(nil, 2) -- Capture stack trace for debugging slow/failed items.
	return Promise.new(function(resolve, reject, onCancel)
		self._totalItemCount += 1

		-- Create the queue item record.
		local record = {
			id = self._totalItemCount,
			fn = callback,
			resolve = resolve,
			reject = reject,
			trace = trace,
		}
		table.insert(self._queue, record)

		self._logger:log("trace", "added item to queue", self:_getLogContext())

		-- Handle cancellation: if the returned promise is cancelled before processing,
		-- remove the item from the queue.
		onCancel(function()
			local idx = table.find(self._queue, record)
			if idx then
				table.remove(self._queue, idx)
				self._logger:log("trace", "removed cancelled item from queue", self:_getLogContext(record))
			end
		end)

		-- If this is the first item added to an empty queue, start the processing loop.
		if #self._queue == 1 then
			task.spawn(function()
				self:_processQueue()
			end)
		end
	end)
end

--[=[
	Internal function that processes items from the queue sequentially.
	It runs as long as there are items in the queue.
	@within PromiseQueue
	@private
]=]
function PromiseQueue:_processQueue()
	self._logger:log("trace", "processing queue", self:_getLogContext())
	-- Loop continues as long as items exist in the queue.
	while #self._queue > 0 do
		local item = self._queue[1] -- Get the next item from the front.

		-- Set up a warning timer for potential deadlocks or long-running items.
		-- If an item takes longer than 60 seconds, a warning is logged.
		local deadlockWarn = task.delay(60, function()
			local ctx = self:_getLogContext(item)
			ctx.trace = item.trace -- Include original call stack in warning.
			self._logger:log("warn", "queue item taking > 60s", ctx)
		end)

		self._logger:log("trace", "processing queue item", self:_getLogContext(item))
		-- Execute the item's function within a Promise context.
		Promise
			.try(item.fn)
			:timeout(60) -- Apply a 60-second timeout to the item's execution.
			:andThen(
				item.resolve, -- If successful, resolve the original promise returned by `add`.
				function(e) -- If failed (error or timeout)...
					-- Log the failure details.
					local ctx = self:_getLogContext(item)
					ctx.error = e
					ctx.trace = item.trace

					local msg
					if Promise.Error.isKind(e, Promise.Error.Kind.TimedOut) then
						msg = "queue item timed out"
					else
						msg = "queue item failed"
					end
					self._logger:log("debug", msg, ctx)

					-- Reject the original promise with the error and traceback.
					item.reject(`Queue item failed: {e}\nCreated at:\n{item.trace}`)
				end
			)
			:finally(function()
				-- This runs regardless of success or failure.
				self._logger:log("trace", "finished processing queue item", self:_getLogContext(item))

				-- Remove the item *only if* it's still the first item.
				-- This prevents issues if an item was cancelled and removed while processing.
				if self._queue[1] == item then
					table.remove(self._queue, 1)
				end

				-- Cancel the deadlock warning timer as the item has finished.
				task.cancel(deadlockWarn)
			end)
			:await() -- Wait for the item's promise (including timeout/finally) to complete before the loop continues.
	end
	self._logger:log("trace", "finished processing queue", self:_getLogContext())
end

--[=[
	Internal helper function used by `multiQueueAdd`.
	Adds a special "blocking" item to a single queue. This item's function
	returns a Promise that only resolves when an external `resume` function is called.
	`resume` is just a Promise resolver function - calling it unblocks the queue item.

	@param queue -- The PromiseQueue instance to add the block to.
	@return Promise<() -> ()> -- A Promise that resolves with the `resume` function once the block becomes the active item in the queue.
	@within PromiseQueue
	@private
]=]
local function addResumableBlock(queue: PromiseQueue): Promise.TPromise<() -> ()>
	return Promise.new(function(outerResolve)
		-- Add an item whose function returns a promise...
		queue:add(function()
			return Promise.new(function(resume)
				-- ...that resolves the outer promise (`addResumableBlock`'s promise)
				-- with the `resume` function needed to unblock this item.
				outerResolve(resume :: any)
			end)
		end)
	end)
end
PromiseQueue._addResumableBlock = addResumableBlock -- Assign to the metatable for internal use.

--[=[
	Atomically adds a callback function to be executed across multiple queues.

	Ensures that the callback only runs when it has effectively acquired the "lock"
	(become the currently processing item) on *all* specified queues simultaneously.
	This is useful for operations that need to coordinate across multiple resources
	managed by separate queues.

	**Mechanism:**
	1. Uses `_addResumableBlock` to add a blocking item to each queue.
	2. Waits for all these blocking items to become active (i.e., all `_addResumableBlock`
	   promises resolve, returning their `resume` functions).
	3. Once all queues are blocked, executes the provided `callback`.
	4. After the `callback` finishes (successfully or with an error), calls all the
	   `resume` functions to unblock all the queues.

	@param queues -- A table array of PromiseQueue instances to coordinate.
	@param callback -- The function to execute once all queues are ready.
	@return Promise -- A Promise that resolves/rejects with the result/error of the `callback`.
	@within PromiseQueue
]=]
function PromiseQueue.multiQueueAdd(queues: { PromiseQueue }, callback: () -> ()): Promise.Promise
	local trace = debug.traceback(nil, 2)
	return Promise.new(function(resolve, reject)
		-- Add a resumable block to each queue.
		local promises = Tables.map(queues, addResumableBlock)

		-- Wait for all blocks to be added and become active.
		Promise.all(promises):andThen(function(resumes)
			-- All queues are now blocked at our added item. Execute the callback.
			Promise.try(callback)
				:andThen(resolve, function(e) -- Handle callback success/failure
					reject(`multiQueueAdd callback failed: {e}\nCreated at:\n{trace}`)
				end)
				:finally(function()
					-- Unblock all queues regardless of callback outcome.
					for _, resume in resumes do
						resume()
					end
				end)
		end)
	end)
end

--[=[
	Internal helper to generate a context table for logging.
	@param item -- (Optional) The QueueItem currently being processed.
	@return { [string]: any } -- A table containing common context fields like queue length and item ID.
	@within PromiseQueue
	@private
]=]
function PromiseQueue:_getLogContext(item: QueueItem?): { [string]: any }
	return {
		queueLength = #self._queue,
		totalItems = self._totalItemCount,
		itemId = item and item.id,
	}
end

return PromiseQueue


--[=[
	@class Types
	@private

	Common types shared among different modules.
]=]

local Log = require(script.Parent.Log)
local t = require(script.Parent.Parent.t)

--[=[
	Represents a single operation within a JSON Patch array.
	Used for describing changes between data states.

	@interface PatchOperation
	@within Types
	.op "add" | "replace" | "remove" -- The operation type.
	.path string -- A JSON Pointer string indicating the target location.
	.value any? -- The value to add or replace (used for "add" and "replace"). Optional for "remove".
]=]
export type PatchOperation =
	{ op: "add", path: string, value: any }
	| { op: "replace", path: string, value: any }
	| { op: "remove", path: string }

--[=[
	An array of PatchOperation objects representing the changes made during a transaction.

	@type TxPatch { PatchOperation }
	@within Types
]=]
export type TxPatch = { PatchOperation }

--[=[
	Holds information about the state of data potentially involved in a transaction.
	Used by the `Transactions` module to determine the correct data to return during reads.

	@interface TxInfo
	@within Types
	.committedData any -- The last known data state that was successfully saved to the primary DataStore record *before* the transaction identified by `txId` began.
	.txId string? -- The unique identifier of the transaction currently attempting to modify this data. If `nil`, no transaction is active or the last one completed and was cleaned up.
	.txPatch TxPatch? -- The set of changes (JSON Patch) applied by the transaction identified by `txId`. This is used to reconstruct the final state if the transaction is confirmed as committed.
]=]
export type TxInfo = {
	committedData: any,
	txId: string?,
	txPatch: TxPatch?,
}

--[=[
	Represents the stored data, abstracting away the sharding mechanism.

	If the data was small enough, it's stored directly in the `data` field.
	If the data was large and sharded, `shard` and `count` are present instead,
	pointing to the location and number of data shards stored separately.

	@interface File
	@within Types
	.data any? -- The actual data, if it was stored directly (not sharded). Mutually exclusive with `shard` and `count`.
	.shard string? -- The unique identifier for the set of shards, if the data was sharded. Mutually exclusive with `data`.
	.count number? -- The total number of shards, if the data was sharded. Mutually exclusive with `data`.
]=]
export type File = {
	data: any,
} & {
	shard: string,
	count: number,
}

--[=[
	The structure of the primary record stored in the main DataStore for each key.
	This record contains metadata and a reference (`File`) to the actual user data.

	@interface DataStoreRecord
	@within Types
	.appliedMigrations {string} -- A list of names of migration steps that have already been successfully applied to the data associated with this record. Initialized as empty.
	.file File -- A `File` object representing the actual user data. This might contain the data directly or point to shards.
	.orphanedFiles {File} -- A list of sharded `File` objects that are no longer referenced by any active record. This is used for cleanup and garbage collection of unused data. Initialized as empty.
]=]
export type DataStoreRecord = {
	appliedMigrations: { string },
	file: File,
	orphanedFiles: { File },
}

--[=[
	Represents a migration step that can be applied to data when loading it.
	Each step has a name and an `apply` function that takes the data as input
	and returns a modified version of the data.

	@interface MigrationStep
	@within Types
	.name string -- The unique name of the migration step.
	.apply (data: { [string]: any }) -> { [string]: any } -- The function that transforms the data for this step.
]=]
export type MigrationStep = {
	name: string,
	apply: (data: { [string]: any }) -> { [string]: any },
}

--[=[
	Contains all the contextual information and dependencies required for a `Store`
	or `PlayerStore` instance to operate. This includes configuration, service instances,
	callbacks, and underlying storage objects.

	@interface StoreContext<T>
	@within Types
	.name string -- The name of the store, used for logging and potentially identifying DataStore keys.
	.template T -- A default template object representing the initial state for new data entries.
	.schema (value: any) -> (boolean, string?) -- A validation function (like one created by `t`) used to check if loaded or modified data conforms to the expected structure. Returns `true` if valid, or `false` and an error message string if invalid.

	.migrationSteps {MigrationStep} -- A list of migration steps to apply to data when it's loaded, based on the `appliedMigrations` field in the `DataStoreRecord`. Initialized as empty.
	.importLegacyData ((key: string) -> any?)? -- An optional function to load data from a legacy storage system when a key is accessed for the first time in this store.

	.dataStoreService DataStoreService -- The Roblox DataStoreService instance.
	.memoryStoreService MemoryStoreService -- The Roblox MemoryStoreService instance.

	.changedCallbacks { (key: string, newData: T, oldData: T?) -> () } -- A list of functions to call whenever data for a key is successfully changed. Provides the key, the new data state, and the previous data state (if available). Initialized as empty.
	.logger Logger -- A `Logger` instance used for internal logging within the store and its components.
	.onLockLost ((key: string) -> ())? -- An optional callback function triggered if the distributed lock for a key is lost unexpectedly (e.g., due to expiration or external interference).
	.useDSSLocking boolean -- Whether to use DataStoreService for distributed locking instead of MemoryStoreService.

	.recordStore DataStore -- The DataStore used to store `DataStoreRecord` objects.
	.shardStore DataStore -- The DataStore used to store the actual data shards for large files.
	.txStore DataStore -- The DataStore used to store transaction status markers (`txId` keys).
	.lockHashMap MemoryStoreHashMap -- The MemoryStore HashMap used for managing distributed locks.
]=]
export type StoreContext<T> = {
	name: string,
	template: T,
	schema: (value: any) -> (boolean, string?),

	migrationSteps: { MigrationStep },
	importLegacyData: ((key: string) -> any?)?,

	dataStoreService: DataStoreService,
	memoryStoreService: MemoryStoreService,

	changedCallbacks: { (key: string, newData: T, oldData: T?) -> () },
	logger: Log.Logger,
	onLockLost: ((key: string) -> ())?,
	useDSSLocking: boolean,

	recordStore: DataStore,
	shardStore: DataStore,
	txStore: DataStore,
	lockHashMap: DataStore | MemoryStoreHashMap,
}

-- Validation check for TxInfo structure using 't' library.
local txInfoCheck = t.some(
	t.strictInterface({ committedData = t.any }),
	t.strictInterface({ committedData = t.any, txId = t.string, txPatch = t.any })
)
-- Validation check for File structure using 't' library.
local fileCheck = t.some(t.strictInterface({ data = t.any }), t.strictInterface({ shard = t.string, count = t.number }))

--[=[
	A handle returned by retry utility functions like `hashMapRetry`.
	It bundles the core Promise with a way to cancel the retry operation.

	When the `cancel` function is called, instead of cancelling the Promise itself,
	the retry mechanism is stopped, and the Promise is rejected with a cancellation error
	when the next retry attempt is made.

	@interface RetryHandle<T>
	@within Types
	.promise T -- The Promise representing the asynchronous operation being retried. This promise resolves or rejects based on the outcome of the operation after retries.
	.cancel () -> () -- A function that can be called to signal cancellation. If called, the retry mechanism will stop further attempts and reject the `promise`.
]=]
export type RetryHandle<T> = {
	promise: T,
	cancel: () -> (),
}

return {
	txInfoCheck = txInfoCheck,
	fileCheck = fileCheck,
}


--[=[
	@class hashMapRetry
	@private

	Provides a utility function to wrap Roblox MemoryStoreService HashMap API calls
	(like `GetAsync`, `SetAsync`, `UpdateAsync`) with automatic retries for specific,
	commonly transient error conditions identified by error message strings.

	**Purpose:** Similar to [dataStoreRetry], MemoryStore operations can fail due to
	temporary service issues, throttling, or internal errors. This module implements
	a retry mechanism with exponential backoff to handle these transient failures.
	It differs from [dataStoreRetry] by matching against error *strings* rather than
	numeric codes, as MemoryStore errors often manifest this way. It also includes
	a cancellation mechanism.

	**Usage:**
	```lua
	local MemoryStoreService = game:GetService("MemoryStoreService")
	local hashMapRetry = require(script.Parent.hashMapRetry)
	local myMap = MemoryStoreService:GetHashMap("MyMap")

	local handle = hashMapRetry(function()
		-- This function will be retried automatically on specific errors
		return myMap:GetAsync("MyKey")
	end)

	handle.promise:andThen(function(data)
		print("Successfully retrieved data:", data)
	end):catch(function(err)
		warn("Failed to get data after retries or cancelled:", err)
	end)

	-- To cancel the operation:
	-- handle.cancel()
	```
]=]

local Promise = require(script.Parent.Promise)
local Types = require(script.Parent.Types)

-- Maximum number of times to attempt the operation before giving up.
local MAX_RETRIES = 5

--[[
	List of error message substrings that indicate potentially transient MemoryStore
	errors worth retrying. These are based on observed errors and common failure modes
	like throttling and internal issues. Unlike DataStores, MemoryStore often reports
	errors via strings rather than distinct numeric codes.

	Refer to Roblox Creator Documentation for MemoryStore limits and potential issues:
	https://create.roblox.com/docs/cloud-services/memory-stores#observability
	(Note: Specific error message strings may not be exhaustively documented.)
]]
local RETRY_ERROR_CODES = {
	"TotalRequestsOverLimit", -- Exceeds universe-level request unit limit.
	"InternalError", -- Generic internal server error.
	"RequestThrottled", -- Recent MemoryStores requests hit one or more limits.
	"PartitionRequestsOverLimit", -- Exceeds partition request unit limit.
	"Throttled", -- Undocumented, included for completeness.
	"Timeout", -- Undocumented, included for completeness.
}

--[=[
	Wraps a function that performs a MemoryStore HashMap operation, automatically
	retrying it if it fails with specific transient error message substrings.

	Implements an exponential backoff strategy: waits 1s, 2s, 4s, 8s between retries.
	Includes a cancellation mechanism via the returned [RetryHandle].

	@within hashMapRetry
	@param func () -> any -- The function to execute and potentially retry. This function should perform the desired MemoryStore HashMap operation (e.g., `GetAsync`, `SetAsync`, `UpdateAsync`). It should return the result on success or throw an error on failure.
	@return RetryHandle<Promise<any>> -- A handle containing: `promise` (A Promise that resolves with the return value of `func` if it succeeds within `MAX_RETRIES` attempts) and `cancel` (A function that, when called, attempts to stop further retries and rejects the promise).
	@error string -- Rejects if `func` fails with a non-retriable error, if it fails with a retriable error `MAX_RETRIES` times, or if `cancel()` is called.
]=]
local function hashMapRetry(func: () -> any): Types.RetryHandle<Promise.TPromise<any>>
	local cancel = false

	local handle: Types.RetryHandle<Promise.TPromise<any>> = {
		promise = Promise.new(function(resolve, reject)
			local lastError
			for i = 1, MAX_RETRIES do
				-- Apply exponential backoff delay before retrying (starting from the second attempt)
				-- Only wait if the operation hasn't been cancelled.
				if i > 1 and not cancel then
					-- Wait times: 2^0=1s, 2^1=2s, 2^2=4s, 2^3=8s
					local retryAfter = 2 ^ (i - 1)
					task.wait(retryAfter)
				end

				if cancel then
					return reject(`HashMap error: operation cancelled`)
				end

				local result = table.pack(pcall(func)) -- { success: boolean, ...results | error: string }
				if result[1] == true then
					-- Success! Resolve the promise with the function's return values.
					return resolve(table.unpack(result, 2))
				end

				-- Failure. Store the error message.
				lastError = result[2]

				-- Check if the error message contains any of the retriable substrings.
				local retry = false
				for _, errorCodeSubstring in RETRY_ERROR_CODES do
					if lastError:find(errorCodeSubstring, 1, true) then
						retry = true
						break
					end
				end

				-- If the error is retriable, continue to the next iteration (if not cancelled).
				if retry then
					continue
				end

				-- Non-retriable error. Reject immediately.
				return reject(`HashMap error: {lastError}`)
			end
			-- If the loop completes, it means MAX_RETRIES were reached without success.
			-- Check for cancellation one last time before rejecting due to retries.
			if cancel then
				return reject(`HashMap error: operation cancelled`)
			else
				return reject(`HashMap error: too many retries ({MAX_RETRIES}). Last error: {lastError}`)
			end
		end),
		cancel = function()
			cancel = true
		end,
	}

	return handle
end

return hashMapRetry


--[=[
	@class Lyra

	The main entry point and public facade for the Lyra library.

	This module re-exports the primary functions and constructors needed to interact
	with Lyra, simplifying the public API and hiding internal module structure.
	Users of the library should typically only need to require this top-level module.
]=]

local Log = require(script.Log)
local Migrations = require(script.Migrations)
local PlayerStore = require(script.PlayerStore)

--[=[
	Provides helper functions for creating common migration steps.

	@interface MigrationStep
	.field addFields (name: string, fields: { [string]: any }) -> MigrationStep -- Creates a step to add new fields with default values.
	.field transform (name: string, transformFunc: (currentValue: { [string]: any }) -> { [string]: any }) -> MigrationStep -- Creates a step with a custom data transformation function.

	@within Lyra
]=]
local MigrationStep = {
	addFields = Migrations.makeAddFieldsStep,
	transform = Migrations.makeTransformStep,
}

--[=[
	Factory function to create a new PlayerStore instance.

	@function createPlayerStore<T>
	@param context PlayerStoreConfig<T> -- The configuration for the PlayerStore.
	@return PlayerStore<T> -- A new PlayerStore instance.
	@within Lyra
]=]
local createPlayerStore = PlayerStore.createPlayerStore

--[=[
	Sets the global minimum log level for all Lyra loggers.

	@function setLogLevel
	@param level LogLevel -- The minimum log level ("fatal", "error", "warn", "info", "debug", "trace").
	@within Lyra
]=]
local setLogLevel = Log.setLevel

-- The public interface of the Lyra library.
return {
	MigrationStep = MigrationStep,
	createPlayerStore = createPlayerStore,
	setLogLevel = setLogLevel,
}


--[[
	Calls a function and throws an error if it attempts to yield.

	Pass any number of arguments to the function after the callback.

	This function supports multiple return; all results returned from the
	given function will be returned.
]]

local function resultHandler(co: thread, ok: boolean, ...)
	if not ok then
		local message = (...)
		error(debug.traceback(co, message), 2)
	end

	if coroutine.status(co) ~= "dead" then
		error(debug.traceback(co, "attempt to yield"), 2)
	end

	return ...
end

local function noYield<Input..., Output...>(callback: (Input...) -> Output..., ...: Input...): Output...
	local co = coroutine.create(callback)

	return resultHandler(co, coroutine.resume(co, ...))
end

return noYield


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

--[[
	Create a new array with the given dataTypeInterface
]]
@native
local function array<T>(value_ty: types.DataType<T>): types.DataType<{ T }>
	local val_write = value_ty.write
	local val_read = value_ty.read

	return {
		__T = nil :: T,

		read = @native function(buff: buffer, cursor: number)
			local array_length = buffer.readu16(buff, cursor)
			local array_cursor = cursor + 2

			local array = table.create(array_length)

			for i = 1, array_length do
				local item, length = val_read(buff, array_cursor)
				array[i] = item

				array_cursor += length
			end

			return array, array_cursor - cursor
		end,
		write = @native function(value: any)
			local length = #value
			buffer.writeu16(channel_state.buff, channel_state.cursor, length)

			-- numeric iteration is about 2x faster than generic iteration
			for i = 1, length do
				val_write(value[i])
			end
		end,
	}
end

return array


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

local bool_modifier = {
	[1] = true,
	[0] = false,
}
local bool_modifier2 = {
	[true] = 1,
	[false] = 0,
}

local bool_data_type = {
	__T = (nil :: any) :: boolean,

	--[[
		1 = true
		0 = false

		Write and read based off a uint8
	]]
	read = @native function(b: buffer, cursor: number)
		return bool_modifier[buffer.readu8(b, cursor)], 1
	end,

	length = 1,

	write = @native function(value)
		buffer.writeu8(bool_modifier2[value])
	end,
}

@native
local function bool(): types.DataType<boolean>
	return bool_data_type
end

return bool


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

local buff = {
	read = @native function(b: buffer, cursor: number)
		local length = buffer.readu16(b, cursor)
		local freshBuffer = buffer.create(length)

		-- copy the data from the main buffer to the new buffer with an offset of 2 because of length
		buffer.copy(freshBuffer, 0, b, cursor + 2, length)

		return freshBuffer, length + 2
	end,
	write = @native function(data: buffer)
		local length = buffer.len(data)
		writeu16(length)

		dyn_alloc(length)

		-- write the length of the buffer, then the buffer itself
		writecopy(data)
	end,
}

return @native function(): types.DataType<buffer>
	return buff
end


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

local cframe: types.DataType<CFrame> = {
	__T = ... :: CFrame,

	read = @native function(b: buffer, cursor: number)
		local x = buffer.readf32(b, cursor)
		local y = buffer.readf32(b, cursor + 4)
		local z = buffer.readf32(b, cursor + 8)
		local rx = buffer.readf32(b, cursor + 12)
		local ry = buffer.readf32(b, cursor + 16)
		local rz = buffer.readf32(b, cursor + 20)

		return CFrame.new(x, y, z) * CFrame.Angles(rx, ry, rz), 24
	end,
	write = @native function(value: CFrame)
		local x, y, z = value.X, value.Y, value.Z
		local rx, ry, rz = value:ToEulerAnglesXYZ()

		-- Math done, write it now
		alloc(24)
		writef32NoAlloc(x)
		writef32NoAlloc(y)
		writef32NoAlloc(z)
		writef32NoAlloc(rx)
		writef32NoAlloc(ry)
		writef32NoAlloc(rz)
	end,
}

return @native function(): types.DataType<CFrame>
	return cframe
end


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

local color3 = {
	__T = ... :: Color3,

	write = @native function(input: Color3)
		alloc(3)
		uint8NoAlloc(input.R * 255)
		uint8NoAlloc(input.G * 255)
		uint8NoAlloc(input.B * 255)
	end,

	read = @native function(b: buffer, cursor: number)
		return Color3.fromRGB(
			buffer.readu8(b, cursor),
			buffer.readu8(b, cursor + 1),
			buffer.readu8(b, cursor + 2)
		),
			3
	end,

	length = 3,
}

return @native function(): types.DataType<Color3>
	return color3
end


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

local float32: types.DataType<number> = {
	__T = ... :: number,

	write = f32,

	read = @native function(b: buffer, cursor: number)
		return buffer.readf32(b, cursor), 4
	end,
}

return @native function(): types.DataType<number>
	return float32
end


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

local float64: types.DataType<number> = {
	__T = ... :: number,

	write = writef64,

	read = @native function(b: buffer, cursor: number)
		return buffer.readf64(b, cursor), 8
	end,
}

return @native function(): types.DataType<number>
	return float64
end


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

return @native function(): types.DataType<Instance?>
	return {
		write = @native function(value)
			alloc(1)
			writeReference(value)
		end,

		read = @native function(b: buffer, cursor: number)
			local refs = readRefs.get()

			if not refs then
				return nil, 1
			end

			local ref = refs[buffer.readu8(b, cursor)]

			if typeof(ref) == "Instance" then
				return ref, 1
			else
				return nil, 1
			end
		end,
	}
end


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

local int16 = {
	write = writei16,

	read = @native function(b: buffer, cursor: number)
		return buffer.readi16(b, cursor), 2
	end,
}

return @native function(): types.DataType<number>
	return int16
end


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

local int32 = {
	write = writei32,

	read = @native function(b: buffer, cursor: number)
		return buffer.readi32(b, cursor), 4
	end,
}

return @native function(): types.DataType<number>
	return int32
end


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

local int8 = {
	write = writei8,

	read = @native function(b: buffer, cursor: number)
		return buffer.readi8(b, cursor), 1
	end,
}

return @native function(): types.DataType<number>
	return int8
end


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

@native
local function map(
	keyType: types.DataType<any>,
	valueType: types.DataType<any>
): types.DataType<{ [any]: any }>
	-- Cache these functions to avoid the overhead of the index
	local key_write = keyType.write
	local value_write = valueType.write

	return {
		read = @native function(b: buffer, cursor: number)
			local map = {}
			local mapCursor = cursor

			-- Read map length
			local mapLength = buffer.readu16(b, mapCursor)
			mapCursor += 2

			for _ = 1, mapLength do
				-- read key/value pairs and add them to the map
				local key, keyLength = keyType.read(b, mapCursor)
				mapCursor += keyLength

				local value, valueLength = valueType.read(b, mapCursor)
				mapCursor += valueLength

				map[key] = value
			end

			-- Return the map, alongside length, because mapCursor - cursor = size
			return map, mapCursor - cursor
		end,
		write = @native function(map: any)
			local count = 0
			for _ in map do
				count += 1
			end

			-- Write length
			writeu16(count)

			for k, v in map do
				-- Write key/value pairs
				key_write(k)
				value_write(v)
			end
		end,
	}
end

return map


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

local nothing = {
	write = @native function() end,

	read = @native function()
		return nil, 0
	end,
}

return @native function(): types.DataType<nil>
	return nothing
end


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

@native
local function optional<T>(value_type: types.DataType<T>): types.DataType<T?>
	local value_read = value_type.read
	local value_write = value_type.write

	return {
		--[[
			first byte is a boolean, if it's true, the next bytes are the value of valueType
			if it's false, its length of 1 cuz only 1 boolean
		]]

		read = @native function(b: buffer, cursor: number)
			if buffer.readu8(b, cursor) == 0 then
				-- doesn't exist
				return nil, 1
			else
				-- exists, read the value
				local item, length = value_read(b, cursor + 1)
				return item, length + 1
			end
		end,

		write = @native function(value: any)
			local exists = value ~= nil

			writebool(exists)

			if exists then
				value_write(value)
			end
		end,
	}
end

return optional


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

local str = {
	-- 2 bytes for the length, then the string

	read = @native function(b: buffer, cursor: number)
		local length = buffer.readu16(b, cursor)

		return buffer.readstring(b, cursor + 2, length), length + 2
	end,
	write = @native function(data: string)
		local length = string.len(data)
		writeu16(length)

		dyn_alloc(length)
		writestring(data)
	end,
}

return @native function(): types.DataType<string>
	return str
end


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

type structData = {
	[string]: number,
}

return function<T, V>(input: T & {
	[string]: types.DataType<V>,
}): types.DataType<T>
	local index_value_type_pairs: {
		[number]: types.DataType<any>,
	} = {}
	local index_key_pairs: { [number]: string } = {}

	local count = 0
	for key in input :: any do
		count += 1

		-- Store the index-value pairs and the index-key pairs as a shortcut for serializing n all that
		index_value_type_pairs[count] = input[key]
		index_key_pairs[count] = key
	end

	return {
		__T = nil :: T,

		read = @native function(b, cursor)
			local constructed = table.clone(input)
			local struct_cursor = cursor

			for index, value_type in index_value_type_pairs do
				local value, length = value_type.read(b, struct_cursor)

				constructed[index_key_pairs[index]] = value

				struct_cursor += length
			end

			return constructed, struct_cursor - cursor
		end,

		write = @native function(struct_value)
			for index, value_type in index_value_type_pairs do
				value_type.write(struct_value[index_key_pairs[index]])
			end
		end,
	}
end


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

local uint16 = {
	write = writeu16,

	read = @native function(b: buffer, cursor: number)
		return buffer.readu16(b, cursor), 2
	end,
}

return @native function(): types.DataType<number>
	return uint16
end


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

local uint32 = {
	write = writeu32,

	read = @native function(b: buffer, cursor: number)
		return buffer.readu32(b, cursor), 4
	end,
}

return @native function(): types.DataType<number>
	return uint32
end


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

local uint8 = {
	write = writeu8,

	read = @native function(b: buffer, cursor: number)
		return buffer.readu8(b, cursor), 1
	end,
}

return @native function(): types.DataType<number>
	return uint8
end


local read_refs = require("../process/readRefs")
local types = require("../types")

@native
local function unknown(): types.DataType<unknown>
	return {
		write = @native function(value: unknown)
			alloc(1)
			writeReference(value)
		end,

		read = @native function(b: buffer, cursor: number)
			local refs = read_refs.get()

			if not refs then
				return nil, 1
			end

			return refs[buffer.readu8(b, cursor)], 1
		end,
	}
end

return unknown


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

local vec2 = {
	--[[
		2 float32s, one for X, one for Y
	]]

	read = @native function(b: buffer, cursor: number)
		return Vector2.new(buffer.readf32(b, cursor), buffer.readf32(b, cursor + 4)), 8
	end,

	write = @native function(value: Vector2)
		buffer.writef32(0, 0, value.X)
		buffer.writef32(0, 0, value.Y)
	end,
}

return @native function(): types.DataType<Vector2>
	return vec2
end


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

local vec3 = {
	__T = ... :: vector,

	read = @native function(b: buffer, cursor: number)
		return vector.create(
			buffer.readf32(b, cursor),
			buffer.readf32(b, cursor + 4),
			buffer.readf32(b, cursor + 8)
		),
			12
	end,

	write = @native function(value: vector)
		buffer.writef32(value.x)
		buffer.writef32(value.y)
		buffer.writef32(value.z)
	end,

	length = 12,
}

return @native function(): types.DataType<vector>
	return vec3
end


local RunService = game:GetService("RunService")

local array = require("@self/data_types/array")
local bool = require("@self/data_types/bool")
local buff = require("@self/data_types/buff")
local cframe = require("@self/data_types/cframe")
local client = require("@self/process/client")
local define_packet = require("@self/packets/define_packet")
local float32 = require("@self/data_types/float32")
local float64 = require("@self/data_types/float64")
local inst = require("@self/data_types/inst")
local int16 = require("@self/data_types/int16")
local int32 = require("@self/data_types/int32")
local int8 = require("@self/data_types/int8")
local map = require("@self/data_types/map")
local nothing = require("@self/data_types/nothing")
local optional = require("@self/data_types/optional")
local server = require("@self/process/server")
local string = require("@self/data_types/string")
local struct = require("@self/data_types/struct")
local uint16 = require("@self/data_types/uint16")
local uint32 = require("@self/data_types/uint32")
local uint8 = require("@self/data_types/uint8")
local unknown = require("@self/data_types/unknown")
local vec2 = require("@self/data_types/vec2")
local vec3 = require("@self/data_types/vec3")

if RunService:IsServer() then
	server.start()
else
	client.init()
end

return table.freeze({
	define_packet = define_packet,

	t = {
		array = array,
		bool = bool(),
		optional = optional,
		uint8 = uint8(),
		uint16 = uint16(),
		uint32 = uint32(),
		int8 = int8(),
		int16 = int16(),
		int32 = int32(),
		float32 = float32(),
		float64 = float64(),
		cframe = cframe(),
		string = string(),
		vec2 = vec2(),
		vec3 = vec3(),
		buff = buff(),
		struct = struct,
		map = map,
		inst = inst(),
		unknown = unknown(),
		nothing = nothing(),
	},
})


--!native
--!optimize 2
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local types = require("../types")
local client = require("../process/client")
local server = require("../process/server")

local run_context: "server" | "client" = if RunService:IsServer() then "server" else "client"

--[[
	We use closures here instead of metatables for performance
	It's just faster to use closures than metatables
]]
return@native function(props: types.PacketProps<types.DataType<any>>, id: number)
	-- Basic properties: reliability type, "unique" which is used to get the packet ID, and set up listeners
	local reliabilityType = props.reliability or "reliable"
	local listeners = {}

	local serverSendFunction: (player: Player, id: number, writer: (value: any) -> (), data: any) -> () = if reliabilityType
			== "reliable"
		then server.sendPlayerReliable
		else server.sendPlayerUnreliable

	local serverSendAllFunction: (id: number, writer: (value: any) -> (), data: any) -> () = if reliabilityType
			== "reliable"
		then server.sendAllReliable
		else server.sendAllUnreliable

	local clientSendFunction: (id: number, writer: (value: any) -> (), data: any) -> () = if reliabilityType
			== "reliable"
		then client.send_reliable
		else client.send_unreliable

	-- shorcut to avoid indexxing
	local writer = props.value.write

	local exported = {}

	-- RunContext error checking that doesn't have performance drawbacks
	setmetatable(exported, {
		__index =@native function(index)
			if
				(index == "sendTo" or index == "sendToAllExcept" or index == "sendToAll")
				and run_context == "client"
			then
				error("You cannot use sendTo, sendToAllExcept, or sendToAll on the client")
			elseif index == "send" and run_context == "server" then
				error("You cannot use send on the server")
			end
		end,
	})

	-- exposed for the reader file
	exported.reader = props.value.read

	if run_context == "server" then
		function exported.sendToList(data, players: { Player })
			for _, player in players do
				serverSendFunction(player, id, writer, data)
			end
		end

		function exported.sendTo(data, player: Player)
			serverSendFunction(player, id, writer, data)
		end

		function exported.sendToAllExcept(data, except: Player)
			for _, player: Player in Players:GetPlayers() do
				if player ~= except then
					serverSendFunction(player, id, writer, data)
				end
			end
		end

		function exported.sendToAll(data)
			serverSendAllFunction(id, writer, data)
		end
	elseif run_context == "client" then
		function exported.send(data)
			clientSendFunction(id, writer, data)
		end
	end

	function exported.wait()
		-- define it up here so we can use it to disconnect
		local index: number

		local runningThread = coroutine.running()
		table.insert(listeners,@native function(data, player)
			task.spawn(runningThread, data, player)

			-- Disconnects the listener
			table.remove(listeners, index)
		end)

		-- we connected, time to set the index for when we need to disconnect.
		index = #listeners

		-- the listener will resume the thread
		return coroutine.yield()
	end

	function exported.listen(callback)
		table.insert(listeners, callback)
	end

	function exported.getListeners()
		return listeners
	end

	return exported
end


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

local channel_state: types.ChannelState = {
	buff = buffer.create(0),
	ref_map = {},
	cursor = 0,
}

return channel_state


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

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

@native
local function onClientEvent(receivedBuffer, ref)
	read(receivedBuffer, ref)
end

-- Shared with: src/process/server.luau (Infeasible to split this into another file)
@native
local function create(): types.ChannelState
	return {
		cursor = 0,
		size = 256,
		references = {},
		buff = buffer.create(256),
	}
end

@native
local function dump(channel: types.ChannelState): (buffer, { unknown }?)
	local cursor = channel.cursor
	local dump_buffer = buffer.create(cursor)

	buffer.copy(dump_buffer, 0, channel.buff, 0, cursor)

	return dump_buffer, if #channel.references > 0 then channel.references else nil
end
-- No longer shared

local reliable: types.ChannelState = create()
local unreliable: types.ChannelState = create()

local client = {}

function client.send_reliable<T>(id: number, data: T)
	reliable = writePacket(reliable, id, writer, data)
end

function client.send_unreliable(id: number, writer: (value: any) -> (), data: { [string]: any })
	unreliable = writePacket(unreliable, id, writer, data)
end

function client.init()
	local reliableRemote = ReplicatedStorage:WaitForChild("ByteNetReliable")
	reliableRemote.OnClientEvent:Connect(onClientEvent)

	local unreliableRemote = ReplicatedStorage:WaitForChild("ByteNetUnreliable")
	unreliableRemote.OnClientEvent:Connect(onClientEvent)

	RunService.Heartbeat:Connect(function()
		-- Again, checking if there's anything in the channel before we send it.
		if reliable.cursor > 0 then
			local b, r = dump(reliable)
			reliableRemote:FireServer(b, r)

			-- effectively clears the channel
			reliable.cursor = 0
			table.clear(reliable.references)
		end

		if unreliable.cursor > 0 then
			local b, r = dump(unreliable)
			unreliableRemote:FireServer(b, r)

			unreliable.cursor = 0
			table.clear(unreliable.references)
		end
	end)
end

return client


--!optimize 2
--!native
local packetIDs = require("../a")
local readRefs = require("./readRefs")

local ref = packetIDs.ref()
local free_thread: thread?

@native
local function function_passer(fn, ...)
	local aquiredThread = free_thread
	free_thread = nil
	fn(...)
	free_thread = aquiredThread
end

@native
local function yielder()
	while true do
		function_passer(coroutine.yield())
	end
end

@native
local function run_listener(fn, ...)
	free_thread = task.spawn(free_thread or task.spawn(coroutine.create(yielder)), fn, ...)
end

@native
local function read(incoming_buff: buffer, references: { [number]: unknown }?, player: Player?)
	local length = buffer.len(incoming_buff)
	local read_cursor = 0

	readRefs.set(references)

	while read_cursor < length do
		local packet = ref[buffer.readu8(incoming_buff, read_cursor)]
		read_cursor += 1

		local value, value_len = packet.reader(incoming_buff, read_cursor)

		read_cursor += value_len

		for _, listener in packet.getListeners() do
			run_listener(listener, value, player)
		end
	end
end

return read


--!native
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

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

local write_packet = buffer_writer.write_packet

-- All ChannelState is set to nil upon being sent which is why these are all optionals
local per_player_reliable: { [Player]: types.ChannelState } = {}
local per_player_unreliable: { [Player]: types.ChannelState } = {}

-- Shared with: src/process/client.luau (Infeasible to split this into another file)
@native
local function create(): types.ChannelState
	return {
		cursor = 0,
		size = 256,
		references = {},
		buff = buffer.create(256),
	}
end

@native
local function dump(channel: types.ChannelState): (buffer, { unknown }?)
	local cursor = channel.cursor
	local dump_buff = buffer.create(cursor)

	buffer.copy(dump_buff, 0, channel.buff, 0, cursor)

	return dump_buff, if #channel.references > 0 then channel.references else nil
end
-- No longer shared

local globalReliable: types.ChannelState = create()
local globalUnreliable: types.ChannelState = create()

-- TODO handle invalid data better
@native
local function on_server_event(player: Player, data, references)
	-- Only accept buffer data
	if not (typeof(data) == "buffer") then
		return
	end

	read(data, references, player)
end

@native
local function player_added(player)
	if not per_player_reliable[player] then
		per_player_reliable[player] = create()
	end

	if not per_player_unreliable[player] then
		per_player_unreliable[player] = create()
	end
end

local server = {}

function server.sendAllReliable(id: number, writer: (value: any) -> (), data: { [string]: any })
	globalReliable = write_packet(globalReliable, id, writer, data)
end

function server.sendAllUnreliable(id: number, writer: (value: any) -> (), data: { [string]: any })
	globalUnreliable = write_packet(globalUnreliable, id, writer, data)
end

function server.sendPlayerReliable(
	player: Player,
	id: number,
	writer: (value: any) -> (),
	data: { [string]: any }
)
	per_player_reliable[player] = write_packet(per_player_reliable[player], id, writer, data)
end

function server.sendPlayerUnreliable(
	player: Player,
	id: number,
	writer: (value: any) -> (),
	data: { [string]: any }
)
	per_player_unreliable[player] = write_packet(per_player_unreliable[player], id, writer, data)
end

function server.start()
	local reliableRemote = Instance.new("RemoteEvent")
	reliableRemote.Name = "ByteNetReliable"
	reliableRemote.OnServerEvent:Connect(on_server_event)
	reliableRemote.Parent = ReplicatedStorage

	local unreliableRemote = Instance.new("UnreliableRemoteEvent")
	unreliableRemote.Name = "ByteNetUnreliable"
	unreliableRemote.OnServerEvent:Connect(on_server_event)
	unreliableRemote.Parent = ReplicatedStorage

	for _, player in Players:GetPlayers() do
		player_added(player)
	end

	Players.PlayerAdded:Connect(player_added)

	RunService.Heartbeat:Connect(function()
		-- Check if the channel has anything before trying to send it
		if globalReliable.cursor > 0 then
			local b, r = dump(globalReliable)
			reliableRemote:FireAllClients(b, r)

			globalReliable.cursor = 0
			table.clear(globalReliable.references)
		end

		if globalUnreliable.cursor > 0 then
			local b, r = dump(globalUnreliable)
			unreliableRemote:FireAllClients(b, r)

			globalUnreliable.cursor = 0
			table.clear(globalUnreliable.references)
		end

		for _, player in Players:GetPlayers() do
			if per_player_reliable[player].cursor > 0 then
				local b, r = dump(per_player_reliable[player])
				reliableRemote:FireClient(player, b, r)

				per_player_reliable[player].cursor = 0
				table.clear(per_player_reliable[player].references)
			end

			if per_player_unreliable[player].cursor > 0 then
				local b, r = dump(per_player_unreliable[player])
				unreliableRemote:FireClient(player, b, r)

				per_player_unreliable[player].cursor = 0
				table.clear(per_player_unreliable[player].references)
			end
		end
	end)
end

return server


export type ChannelState = {
	buff: buffer,
	cursor: number,
	ref_map: { [number]: unknown },
}

export type PacketProps<T> = {
	value: T,

	reliability: ("Reliable" | "Unreliable")?,

	max_per_frame: number?,
}

export type DataType<T> = {
	read __T: T,

	write: (value: T) -> (),
	read: (b: buffer, cursor: number) -> (T, number),

	read length: number?,
}

type Packet<T> = {
	send_to_all: (data: T) -> (),
	send_to: (data: T, target: Player) -> (),
	send_to_list: (data: T, targets: { Player }) -> (),
	send_to_all_except: (data: T, exception: Player) -> (),

	wait: () -> T,
	send: (data: T, target: Player?) -> (),
	listen: (callback: (data: T, player: Player?) -> ()) -> (),
}

export type function ValueFromDataType(data_type: type)
	return data_type:readproperty(types.singleton("__T"))
end

return nil


--!native
--!optimize 2
--!strict

local Destroy = require("./Utilities/Destroy")
local FileInstance = require("./Classes/FileInstance")
local PathUtilities = require("./Utilities/PathUtilities")
local warn = require("./Utilities/Warn")

local fs = require("@lune/fs")
local process = require("@lune/process")
local serde = require("@lune/serde")
local stdio = require("@lune/stdio")

local PROJECT_NAME = "built-release.project.json"
local SOURCEMAP_NAME = "built-sourcemap.json"

local EXTRA_STEPS: {
	{
		Enabled: boolean,
		Name: string,
		Function: (builtProject: FileInstance.FileInstance) -> boolean,
	}
} =
	{
		{
			Enabled = true;
			Name = "Fix Promise import";
			Function = function(builtProject)
				local promise = builtProject:FindFirstFile("Promise.luau")
				if not promise then
					return false
				end

				local promiseFileSource = promise:Read()
				if not promiseFileSource then
					return false
				end

				promise:Write(
					(
						string.gsub(
							promiseFileSource,
							"local Packages = script.Parent.Parent",
							"local Packages = script.Parent.Packages"
						)
					)
				)

				return true
			end;
		};
	}

type Cleanup = () -> ()

local function CreateBuildProject(): Cleanup
	if fs.isDir("built-project") then
		Destroy("built-project")
	end

	fs.copy("src", "built-project", true)
	local builtProject = FileInstance.Mark("built-project")
	if not fs.isDir("Packages") then
		Destroy("Packages")
	end

	local installResult = process.spawn("wally", {"install"})
	if not installResult.ok then
		warn(`wally install failed: {installResult.stderr} ({installResult.code})`)
		return function()
			builtProject:Destroy()
		end
	end

	local packages = FileInstance.Mark("Packages")

	local tests = builtProject:FindFirstDirectory("__tests__")
	if tests then
		tests:Destroy()
	end

	local configuration = builtProject:FindFirstFile("jest.config.luau")
	if configuration then
		configuration:Destroy()
	end

	local descendants = builtProject:GetDescendants()
	for _, child in builtProject:GetChildren() do
		if child.IsDirectory and #child:GetChildren() == 0 then
			child:Destroy()
		end
	end

	for _, extraStep in EXTRA_STEPS do
		if not extraStep.Enabled then
			continue
		end
		extraStep.Function(builtProject)
	end

	if not fs.isFile(PROJECT_NAME) then
		local json = serde.encode("json", {
			globIgnorePaths = {
				"**/*.spec.luau";
				"**/*.spec.lua";
				"**/*.story.luau";
				"**/*.story.lua";
				"**/__tests__";
				"**/*.test.luau";
				"**/*.test.lua";
			};
			name = "Janitor";
			tree = {
				["$path"] = "built-project";
				Packages = {["$path"] = "Packages"};
			};
		}, false)
		fs.writeFile(PROJECT_NAME, json)
	end

	local sourcemapResult = process.spawn("rojo", {"sourcemap", "--output", SOURCEMAP_NAME, PROJECT_NAME})
	if not sourcemapResult.ok then
		warn(`rojo sourcemap failed: {sourcemapResult.stderr} ({sourcemapResult.code})`)
		return function()
			Destroy(PROJECT_NAME)
			builtProject:Destroy()
		end
	end

	local packageTypesResult = process.spawn("wally-package-types", {"--sourcemap", SOURCEMAP_NAME, "Packages/"})
	if not packageTypesResult.ok then
		warn(`wally-package-types failed: {packageTypesResult.stderr} ({packageTypesResult.code})`)
		return function()
			Destroy(PROJECT_NAME)
			Destroy(SOURCEMAP_NAME)
			builtProject:Destroy()
		end
	end

	local packageNames: {[string]: string} = {}
	for _, child in packages:GetChildren() do
		if child.IsDirectory then
			continue
		end

		local packageName = PathUtilities.FileNameNoExtension(child.Name)
		if not packageName or packageNames[packageName] then
			continue
		end

		packageNames[packageName] = `local {packageName} = require%(script.(.*).{packageName}%)`
		print("Adding", packageName)
	end

	for _, descendant in descendants do
		if descendant.IsDirectory or descendant.Name == "README.luau" then
			continue
		end

		local extension = PathUtilities.ExtensionName(descendant.Name)
		if not extension then
			warn("No extension found for", descendant:GetFullName())
			continue
		end

		if extension ~= ".lua" and extension ~= ".luau" then
			continue
		end

		local fileSource = descendant:Read()
		if not fileSource then
			warn("Failed to read", descendant:GetFullName())
			continue
		end

		local splitByNewline = string.split(fileSource, "\n")
		local wasModified = false
		for index, line in splitByNewline do
			for packageName, pattern in packageNames do
				local parentPath = string.match(line, pattern)
				if not parentPath then
					continue
				end

				local requirePath = {"script"}
				local length = 1

				local wasFound = false

				local object = descendant
				while true do
					local parent = object.Parent

					length += 1
					requirePath[length] = "Parent"

					if parent == builtProject then
						requirePath[length + 1] = "Packages"
						requirePath[length + 2] = packageName
						wasFound = true
						break
					end

					if not parent then
						warn("Failed to find parent for", object:GetFullName())
						break
					end

					object = parent
				end

				if wasFound then
					local newLine = `local {packageName} = require({table.concat(requirePath, ".")})`
					splitByNewline[index] = newLine
					wasModified = true
				end
			end
		end

		if wasModified then
			descendant:Write(table.concat(splitByNewline, "\n"))
		end
	end

	return function()
		Destroy(PROJECT_NAME)
		Destroy(SOURCEMAP_NAME)
		builtProject:Destroy()
	end
end

local function Install(lastCleanup: Cleanup?): Cleanup
	local rbxmResult = process.spawn("rojo", {"build", "--output", "Janitor.rbxm", PROJECT_NAME})
	if not rbxmResult.ok then
		warn(`rojo build rbxm failed: {rbxmResult.stderr} ({rbxmResult.code})`)
		return function()
			if lastCleanup then
				lastCleanup()
			end
		end
	end

	local rbxmxResult = process.spawn("rojo", {"build", "--output", "Janitor.rbxmx", PROJECT_NAME})
	if not rbxmxResult.ok then
		warn(`rojo build rbxmx failed: {rbxmxResult.stderr} ({rbxmxResult.code})`)
		return function()
			if lastCleanup then
				lastCleanup()
			end
		end
	end

	return function()
		if lastCleanup then
			lastCleanup()
		end
	end
end

local cleanup = Install(CreateBuildProject())
stdio.prompt("confirm", "Press enter to continue and cleanup", true)

cleanup()

local sourcemapResult = process.spawn("rojo", {"sourcemap", "--output", "sourcemap.json", "place.project.json"})
if not sourcemapResult.ok then
	warn(`rojo sourcemap failed: {sourcemapResult.stderr} ({sourcemapResult.code})`)
	process.exit(1)
end

local packageTypesResult = process.spawn("wally-package-types", {"--sourcemap", "sourcemap.json", "Packages/"})
if not packageTypesResult.ok then
	warn(`wally-package-types failed: {packageTypesResult.stderr} ({packageTypesResult.code})`)
	process.exit(1)
end

local devPackageTypesResult = process.spawn("wally-package-types", {"--sourcemap", "sourcemap.json", "DevPackages/"})
if not devPackageTypesResult.ok then
	warn(`wally-package-types failed: {devPackageTypesResult.stderr} ({devPackageTypesResult.code})`)
	process.exit(1)
end


--!native
--!optimize 2
--!strict

local Destroy = require("../Utilities/Destroy")
local PathUtilities = require("../Utilities/PathUtilities")

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

export type FileInstance = {
	FilePath: string,
	IsDirectory: boolean,
	Name: string,
	Parent: FileInstance?,

	GetChildren: (self: FileInstance) -> {FileInstance},
	GetDescendants: (self: FileInstance) -> {FileInstance},
	GetFullName: (self: FileInstance) -> string,

	FindFirstFile: (self: FileInstance, name: string, recursive: boolean?) -> FileInstance?,
	FindFirstDirectory: (self: FileInstance, name: string, recursive: boolean?) -> FileInstance?,

	Read: (self: FileInstance) -> string?,
	Write: (self: FileInstance, source: string) -> (),
	Destroy: (self: FileInstance) -> (),
}
type Private = {
	FilePath: string,
	IsDirectory: boolean,
	Name: string,
	Parent: FileInstance?,

	Children: {FileInstance}?,
	Descendants: {FileInstance}?,

	GetChildren: (self: Private) -> {FileInstance},
	GetDescendants: (self: Private) -> {FileInstance},
	GetFullName: (self: Private) -> string,

	FindFirstFile: (self: Private, name: string, recursive: boolean?) -> FileInstance?,
	FindFirstDirectory: (self: Private, name: string, recursive: boolean?) -> FileInstance?,

	Read: (self: Private) -> string?,
	Write: (self: Private, source: string) -> (),
	Destroy: (self: Private) -> (),
}
type Static = {
	ClassName: "FileInstance",
	Mark: (filePath: string, fileName: string?, parent: FileInstance?) -> FileInstance,
	Is: (value: any) -> boolean,
}
type PrivateStatic = Static & {
	__eq: (self: FileInstance, other: FileInstance) -> boolean,
	__tostring: (self: FileInstance) -> string,
}

local FileInstance = {} :: FileInstance & Static
local Private = FileInstance :: Private & PrivateStatic
FileInstance.ClassName = "FileInstance";
(FileInstance :: any).__index = FileInstance

function FileInstance.Mark(filePath, fileName, parent): FileInstance
	local newFilePath = if fileName then `{filePath}/{fileName}` else filePath

	local self: Private = setmetatable({}, FileInstance) :: never
	self.Children = nil
	self.Descendants = nil

	self.Name = PathUtilities.FileName(newFilePath)
	self.FilePath = newFilePath
	self.IsDirectory = fs.isDir(newFilePath)
	self.Parent = parent

	if not self.IsDirectory then
		-- if it's an init.luau file, the parent is actually the next parent up
		if self.Name == "init.luau" and parent then
			self.Parent = parent.Parent
		end
	end

	return self
end

function FileInstance.Is(value)
	return type(value) == "table" and getmetatable(value) == FileInstance
end

function FileInstance:GetFullName()
	local filePath = self.FilePath
	if string.sub(filePath, 1, 2) == "./" then
		filePath = string.sub(filePath, 3)
	end

	return process.cwd .. filePath
end

function Private:GetChildren()
	-- local cached = self.Children
	-- if cached then
	-- 	return cached
	-- end

	if self.IsDirectory then
		local inDirectory = fs.readDir(self.FilePath)
		local children = table.create(#inDirectory)
		for index, child in inDirectory do
			children[index] = FileInstance.Mark(self.FilePath, child, self)
		end

		self.Children = children
		return children
	end

	local children = {}
	self.Children = children
	return children
end
function Private:GetDescendants()
	-- local cached = self.Descendants
	-- if cached then
	-- 	return cached
	-- end

	local descendants = self:GetChildren()
	local totalDescendants = #descendants
	local length = 0

	if totalDescendants > 0 then
		repeat
			length += 1
			local grandChildren = descendants[length]:GetChildren()
			for index, grandChild in grandChildren do
				descendants[totalDescendants + index] = grandChild
			end
			totalDescendants += #grandChildren
		until length == totalDescendants
	end

	self.Descendants = descendants
	return descendants
end

function FileInstance:FindFirstFile(name, recursive)
	name = PathUtilities.Join(self.Name, name)
	if recursive then
		for _, descendant in self:GetDescendants() do
			if PathUtilities.OsPath(descendant.Name) == name then
				return descendant
			end
		end

		return nil
	end

	for _, child in self:GetChildren() do
		if PathUtilities.OsPath(child.Name) == name then
			return child
		end
	end

	return nil
end
function FileInstance:FindFirstDirectory(name, recursive)
	name = PathUtilities.Join(self.Name, name)
	if recursive then
		for _, descendant in self:GetDescendants() do
			if descendant.IsDirectory and PathUtilities.OsPath(descendant.Name) == name then
				return descendant
			end
		end

		return nil
	end

	for _, child in self:GetChildren() do
		if child.IsDirectory and PathUtilities.OsPath(child.Name) == name then
			return child
		end
	end

	return nil
end

function FileInstance:Read()
	return if self.IsDirectory then nil else fs.readFile(self.FilePath)
end
function FileInstance:Write(source)
	if not self.IsDirectory then
		fs.writeFile(self.FilePath, source)
	end
end

function FileInstance:Destroy()
	Destroy(self.FilePath)
	table.clear(self)
	setmetatable(self, nil)
end

function Private:__eq(other)
	return self.FilePath == other.FilePath
end
Private.__tostring = FileInstance.GetFullName
return table.freeze(FileInstance :: Static)


--!native
--!optimize 2

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

if process.env.windir ~= nil then
	local appDataPath = process.env.LOCALAPPDATA
	local robloxVersionsPath = `{appDataPath}\\Roblox\\Versions`

	if not fs.isDir(robloxVersionsPath) then
		error(`Roblox versions folder not found. Please install Roblox. Checked here: {robloxVersionsPath}`)
	end

	local mostRecentStudioVersionPath
	for _, versionDirName in fs.readDir(robloxVersionsPath) do
		local versionDirPath = `{robloxVersionsPath}\\{versionDirName}`
		if not fs.isDir(versionDirPath) or not fs.isFile(`{versionDirPath}\\RobloxStudioBeta.exe`) then
			continue
		end

		mostRecentStudioVersionPath = versionDirPath
		break
	end

	if mostRecentStudioVersionPath == nil then
		error(`Roblox Studio not found. Please install Roblox. Checked here: {robloxVersionsPath}`)
	end

	local clientSettingsDir = `{mostRecentStudioVersionPath}\\ClientSettings`
	if not fs.isDir(clientSettingsDir) then
		fs.writeDir(clientSettingsDir)
	end

	local clientAppSettingsPath = `{clientSettingsDir}\\ClientAppSettings.json`
	local clientAppSettings

	if fs.isFile(clientAppSettingsPath) then
		clientAppSettings = serde.decode("json", fs.readFile(clientAppSettingsPath))

		local metadata = fs.metadata(clientAppSettingsPath)
		if metadata.permissions and metadata.permissions.readOnly then
			local result = process.spawn("attrib", {"-r", clientAppSettingsPath})
			assert(result.ok, `Failed to remove read-only attribute:\n{result.stderr}`)
		end
	else
		clientAppSettings = {}
	end

	clientAppSettings.FFlagEnableLoadModule = true

	fs.writeFile(clientAppSettingsPath, serde.encode("json", clientAppSettings))

	local result = process.spawn("attrib", {"+r", clientAppSettingsPath})
	assert(result.ok, `Failed to add read-only attribute:\n{result.stderr}`)

	print(`Wrote FFlagEnableLoadModule to ClientAppSettings.json at {clientAppSettingsPath}`)
	process.exit(0)
end

-- TODO: Get the old iMac and figure out Mac
warn(`No support for {process.os} yet. ):`, 2)
process.exit(1)


--!native
--!optimize 2
--!strict

local function ParsePrereleaseAndBuildWithSign(value: string): (string, string)
	local prereleaseWithSign, buildWithSign = string.match(value, "^(-[^+]+)(+.+)$")
	if prereleaseWithSign == nil or buildWithSign == nil then
		prereleaseWithSign = string.match(value, "^(-.+)$")
		buildWithSign = string.match(value, "^(+.+)$")
	end

	if prereleaseWithSign == nil and buildWithSign == nil then
		error(string.format("The parameter %q must begin with + or - to denote a prerelease or a build", value))
	end

	return prereleaseWithSign :: string, buildWithSign :: string
end

local function ParsePrerelease(prereleaseWithSign: string?): string?
	if prereleaseWithSign == nil then
		return nil
	end

	local prerelease = string.match(prereleaseWithSign, "^-(%w[%.%w-]*)$" :: string)
	if prerelease == nil then
		error(
			string.format(
				"The prerelease %q is not a slash followed by alphanumerics, dots and slashes",
				prereleaseWithSign
			)
		)
	end

	return prerelease
end

local function ParseBuild(buildWithSign: string?): string?
	if buildWithSign == nil then
		return nil
	end

	local build = string.match(buildWithSign, "^%+(%w[%.%w-]*)$" :: string)
	if build == nil then
		error(string.format("The build %q is not a + sign followed by alphanumerics, dots and slashes", buildWithSign))
	end

	return build
end

type Option = {
	Build: string,
	Prerelease: string,
} | {
	Build: nil,
	Prerelease: nil,
}

local function ParsePrereleaseAndBuild(value: string?): Option
	if not (value ~= "" and value) then
		return {}
	end

	local prereleaseWithSign, buildWithSign = ParsePrereleaseAndBuildWithSign(value)
	local prerelease = ParsePrerelease(prereleaseWithSign)
	local build = ParseBuild(buildWithSign)
	return {
		Build = build :: string;
		Prerelease = prerelease :: string;
	}
end

local function ParseVersion(value: string)
	local stringMajor, stringMinor, stringPatch, stringPrereleaseAndBuild =
		string.match(value, "^(%d+)%.?(%d*)%.?(%d*)(.-)$")
	if stringMajor == nil then
		error(string.format("Could not extract version number(s) from %q", value))
	end

	local major = tonumber(stringMajor) :: number
	local minor = tonumber(stringMinor) :: number
	local patch = tonumber(stringPatch) :: number
	local option = ParsePrereleaseAndBuild(stringPrereleaseAndBuild :: string)
	return major, minor, patch, option.Prerelease, option.Build
end

local function _Compare(a: any, b: any)
	return if a == b then 0 elseif a < b then -1 else 1
end
local Compare = (_Compare :: never) :: ((a: number, b: number) -> number) & ((a: string, b: string) -> number)

local function CompareIds(selfId: string, otherId: string)
	if selfId == otherId then
		return 0
	end

	if selfId == nil then
		return -1
	end

	if otherId == nil then
		return 1
	end

	local selfNumber = tonumber(selfId)
	local otherNumber = tonumber(otherId)
	if selfNumber and otherNumber then
		return Compare(selfNumber, otherNumber)
	end

	if selfNumber then
		return -1
	end

	if otherNumber then
		return 1
	end

	return Compare(selfId, otherId)
end

local function SmallerIdList(selfIds: {string}, otherIds: {string})
	for index, value in selfIds do
		local comparison = CompareIds(value, otherIds[index])
		if comparison ~= 0 then
			return comparison == -1
		end
	end

	return #selfIds < #otherIds
end

local function SmallerPrerelease(mine: string?, other: string?)
	if mine == other or not mine then
		return false
	end

	if not other then
		return true
	end

	return SmallerIdList(string.split(mine, "."), string.split(other, "."))
end

export type SemanticVersion = typeof(setmetatable(
	{} :: {
		Major: number,
		Minor: number,
		Patch: number,
		Prerelease: string?,
		Build: string?,

		NextMajor: (self: SemanticVersion) -> SemanticVersion,
		NextMinor: (self: SemanticVersion) -> SemanticVersion,
		NextPatch: (self: SemanticVersion) -> SemanticVersion,
	},
	{} :: {
		__eq: (self: SemanticVersion, other: SemanticVersion) -> boolean,
		__lt: (self: SemanticVersion, other: SemanticVersion) -> boolean,
		__pow: (self: SemanticVersion, other: SemanticVersion) -> boolean,
		__tostring: (self: SemanticVersion) -> string,
	}
))

type BaseStatics = {
	ClassName: "SemanticVersion",
	new: (
		major: number | string,
		minor: number?,
		patch: number?,
		prerelease: string?,
		build: string?
	) -> SemanticVersion,
}
type SemanticVersionStatics = BaseStatics & {
	__eq: (self: SemanticVersion, other: SemanticVersion) -> boolean,
	__lt: (self: SemanticVersion, other: SemanticVersion) -> boolean,
	__pow: (self: SemanticVersion, other: SemanticVersion) -> boolean,
	__tostring: (self: SemanticVersion) -> string,
}

local SemanticVersion = {} :: SemanticVersion & SemanticVersionStatics
SemanticVersion.ClassName = "SemanticVersion";
(SemanticVersion :: any).__index = SemanticVersion

function SemanticVersion.new(
	major: number | string,
	minor: number?,
	patch: number?,
	prerelease: string?,
	build: string?
): SemanticVersion
	local self = setmetatable({}, SemanticVersion)
	if type(major) == "string" then
		major, minor, patch, prerelease, build = ParseVersion(major)
	end

	local trueMinor = minor or 0
	local truePatch = patch or 0

	self.Major = major
	self.Minor = trueMinor
	self.Patch = truePatch
	self.Prerelease = prerelease
	self.Build = build
	table.freeze(self)
	return (self :: any) :: SemanticVersion
end

function SemanticVersion:NextMajor(): SemanticVersion
	return SemanticVersion.new(self.Major + 1, 0, 0)
end

function SemanticVersion:NextMinor(): SemanticVersion
	return SemanticVersion.new(self.Major, self.Minor + 1, 0)
end

function SemanticVersion:NextPatch(): SemanticVersion
	return SemanticVersion.new(self.Major, self.Minor, self.Patch + 1)
end

function SemanticVersion:__eq(other)
	return self.Major == other.Major
		and self.Minor == other.Minor
		and self.Patch == other.Patch
		and self.Prerelease == other.Prerelease
end
function SemanticVersion:__lt(other)
	if self.Major ~= other.Major then
		return self.Major < other.Major
	end

	if self.Minor ~= other.Minor then
		return self.Minor < other.Minor
	end

	if self.Patch ~= other.Patch then
		return self.Patch < other.Patch
	end

	return SmallerPrerelease(self.Prerelease, other.Prerelease)
end

function SemanticVersion:__pow(other)
	return if self.Major == 0 then self == other else self.Major == other.Major and self.Minor <= other.Minor
end

function SemanticVersion:__tostring()
	local stringBuilder = {string.format("%*.%*.%*", self.Major, self.Minor, self.Patch)}
	local build = self.Build
	local prerelease = self.Prerelease

	if prerelease ~= "" and prerelease then
		table.insert(stringBuilder, `-{prerelease}`)
	end

	if build ~= "" and build then
		table.insert(stringBuilder, `+{build}`)
	end

	return table.concat(stringBuilder)
end

return table.freeze(SemanticVersion :: BaseStatics)


--!native
--!optimize 2

local SemanticVersion = require("./Packages/SemanticVersion")

local fs = require("@lune/fs")
local serde = require("@lune/serde")
local stdio = require("@lune/stdio")

local wally = serde.decode("toml", fs.readFile("wally.toml"))
local lastVersion = wally.package.version

local lastVersionSemantic = SemanticVersion.new(lastVersion)

local nextVersion = stdio.prompt("text", "What is the next version?", tostring(lastVersionSemantic:NextMinor()))
local nextVersionSemantic = SemanticVersion.new(nextVersion)

local function SanitizePattern(value: string)
	return (string.gsub(value, "([%.%-%*%+%?%%])", "%%%1"))
end

local function ReplaceVersion(file: string, from: string, to: string)
	fs.writeFile(file, (string.gsub(fs.readFile(file), SanitizePattern(from), to)))
end

do
	ReplaceVersion("docs/installation.md", `@{lastVersionSemantic}`, `@{nextVersionSemantic}`)
	ReplaceVersion("docs/intro.md", `@{lastVersionSemantic}`, `@{nextVersionSemantic}`)
	ReplaceVersion("README.md", `@{lastVersionSemantic}`, `@{nextVersionSemantic}`)
end
do
	ReplaceVersion("docs/installation.md", `@%^{lastVersionSemantic}`, `@^{nextVersionSemantic}`)
	ReplaceVersion("docs/intro.md", `@%^{lastVersionSemantic}`, `@^{nextVersionSemantic}`)
	ReplaceVersion("README.md", `@%^{lastVersionSemantic}`, `@^{nextVersionSemantic}`)
end
ReplaceVersion("wally.toml", `version = "{lastVersionSemantic}"`, `version = "{nextVersionSemantic}"`)


--!native
--!optimize 2
--!strict

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

local HAS_RIM_RAF = (function()
	if process.os ~= "windows" then
		return false
	end

	return pcall(function()
		fs.writeDir("swage")
		process.spawn("rimraf", {"swage"})
	end)
end)()

local Destroy: (path: string) -> ()
if HAS_RIM_RAF then
	function Destroy(path)
		process.spawn("rimraf", {path})
	end
else
	function Destroy(path)
		process.spawn("rm", {"-rf", path})
	end
end

return Destroy


--!native
--!optimize 2
--!strict

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

local FileSystemUtilities = {}

function FileSystemUtilities.JustFileName(path: string)
	return string.match(string.match(path, "^(.+)%..+$") or path, "([^\\/]+)$") or path
end
function FileSystemUtilities.JustFileNameWithExtension(path: string)
	return string.match(path, "([^\\/]+)$") or path
end

function FileSystemUtilities.WithoutExtension(path: string): string
	return string.match(path, "^(.+)%..+$") or path
end

function FileSystemUtilities.GetExtension(path: string)
	return string.match(path, "%.([^%.]+)$")
end

type CreatorFunction = (path: string, contents: string?) -> ()
type SafeCreate =
	((name: string, createType: "File", contents: string) -> ())
	& ((name: string, createType: "Directory") -> ())

local function SafeCreate(name: string, createType: "File" | "Directory", contents: string?)
	local CheckerFunction: typeof(fs.isDir) = if createType == "File" then fs.isFile else fs.isDir
	local CreatorFunction = (if createType == "File" then fs.writeFile else fs.writeDir) :: CreatorFunction

	if not CheckerFunction(name) then
		CreatorFunction(name, contents)
	else
		local index = 1
		while CheckerFunction(`{FileSystemUtilities.WithoutExtension(name)} ({index})`) do
			index += 1
		end

		local extension = FileSystemUtilities.GetExtension(name)
		local newFilePath = if extension
			then `{FileSystemUtilities.WithoutExtension(name)} ({index}).{extension}`
			else `{FileSystemUtilities.WithoutExtension(name)} ({index})`

		CreatorFunction(newFilePath, contents)
	end
end
FileSystemUtilities.SafeCreate = SafeCreate :: SafeCreate

function FileSystemUtilities.GetOrCreateDirectory(name: string)
	if not fs.isDir(name) then
		FileSystemUtilities.SafeCreate(name, "Directory")
	end
end

return table.freeze(FileSystemUtilities)


--!native
--!optimize 2

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

local PathUtilities = {}
local SEPARATOR = if process.os == "windows" then "\\" else "/"
PathUtilities.Separator = SEPARATOR

--- Fixes the path so that it's properly formatted to the local operating system
--- @param path The path to format
--- @return A fixed path matching the OS format
function PathUtilities.OsPath(path: string)
	local newPath = ""

	if process.os == "windows" then
		local index = 1
		while index <= #path do
			local character = string.sub(path, index, index)
			if character == "/" then
				character = "\\"
			end

			newPath ..= character
			index += 1
		end

		newPath = string.gsub(newPath, "\\+", "\\")
	else
		local index = 1
		while index <= #path do
			local character = string.sub(path, index, index)
			if character == "\\" then
				character = "/"
			end

			newPath ..= character
			index += 1
		end

		newPath = string.gsub(newPath, "/+", "/")
	end

	return newPath
end

--- Join multiple path parts into a combined path
--- @return A joined path
function PathUtilities.Join(...: string)
	return PathUtilities.OsPath(table.concat({...}, SEPARATOR))
end

--- Gets the extension of the given path (if applicable) - otherwise returns nil
--- @param path The path to get the extension of
--- @return The path extension or nil
function PathUtilities.ExtensionName(path: string): string?
	return string.match(path, ".(%.[A-z0-9_-]+)$")
end

--- Gets the full directory path of the given path
--- @param value The path
--- @return The full directory path without the end item
function PathUtilities.DirectoryName(value: string): string
	local parts = string.split(PathUtilities.OsPath(value), SEPARATOR)
	table.remove(parts)
	return table.concat(parts, SEPARATOR)
end

function PathUtilities.FileName(value: string)
	return string.match(value, `[^{PathUtilities.Separator}]+$`)
end
function PathUtilities.FileNameNoExtension(value: string)
	local extension = PathUtilities.ExtensionName(value)
	local fileName = string.match(value, `[^{PathUtilities.Separator}]+$`)
	if extension then
		return if fileName then string.gsub(fileName, `{extension}$`, "") else nil
	end

	return fileName
end

return table.freeze(PathUtilities)


--!native
--!optimize 2
--!strict

local Chalk = require("../Packages/Chalk")

local ORANGE = Chalk.Rgb(255, 142, 60)

local function Concat(...: unknown)
	local length = select("#", ...)
	if length == 0 then
		return ""
	end

	if length == 1 then
		return tostring(...)
	end

	if length == 2 then
		local a, b = ...
		return tostring(a) .. " " .. tostring(b)
	end

	if length == 3 then
		local a, b, c = ...
		return tostring(a) .. " " .. tostring(b) .. " " .. tostring(c)
	end

	if length == 4 then
		local a, b, c, d = ...
		return tostring(a) .. " " .. tostring(b) .. " " .. tostring(c) .. " " .. tostring(d)
	end

	if length == 5 then
		local a, b, c, d, e = ...
		return tostring(a) .. " " .. tostring(b) .. " " .. tostring(c) .. " " .. tostring(d) .. " " .. tostring(e)
	end

	if length == 6 then
		local a, b, c, d, e, f = ...
		return tostring(a)
			.. " "
			.. tostring(b)
			.. " "
			.. tostring(c)
			.. " "
			.. tostring(d)
			.. " "
			.. tostring(e)
			.. " "
			.. tostring(f)
	end

	local array = table.create(length)
	for index = 1, length do
		array[index] = tostring(select(index, ...))
	end
	return table.concat(array, " ")
end

local function Warn(...)
	warn(ORANGE(Concat(...)))
end

return Warn


--!native

-- duster
-- fast and lightweight maid impl ( thats also better than janitor )

type Types = "table" | "function" | "thread" | "Instance" | "RBXScriptConnection"

type TableCleanupMethod = <V>(self: (V & {})) -> ()

export type Cleanable =
	| { Disconnect: TableCleanupMethod }
	| { disconnect: TableCleanupMethod }
	| { Cleanup: TableCleanupMethod }
	| { destroy: TableCleanupMethod }
	| { Destroy: TableCleanupMethod }
	| { cleanup: TableCleanupMethod }
	| { Cleanup: TableCleanupMethod }
	| { clear: TableCleanupMethod }
	| { Clear: TableCleanupMethod }
	| RBXScriptConnection
	| Instance
	| () -> ()
	| thread

local DISCONNECT = (function()
	local conn = script.Changed:Connect(function() end)
	local disconnect = conn.Disconnect
	disconnect(conn)
	return disconnect
end)()
local TYPEOF = typeof :: <V>(value: V) -> Types
local DESTROY = game.Destroy
local REMOVE = table.remove
local INSERT = table.insert
local CANCEL = task.cancel
local CLEAR = table.clear
local FIND = table.find

local duster = {}

-- have to keep the generic, as otherwise typechecker WILL kick and scream
function duster.clearval<V>(duster: { Cleanable }, value: V & Cleanable)
	local index = FIND(duster, (value :: any))

	if index then
		local type = TYPEOF(value)

		if type == "function" then
			(value :: any)()
		elseif type == "thread" then
			CANCEL((value :: any))
		elseif type == "table" then
			local disconnect1 = (value :: any).Disconnect

			if disconnect1 then
				disconnect1(value)
				return
			end

			local disconnect = (value :: any).disconnect

			if disconnect then
				disconnect(value)
				return
			end

			local destroy = (value :: any).destroy

			if destroy then
				destroy(value)
				return
			end
			local clear = (value :: any).clear

			if clear then
				clear(value)
				return
			end
			local clean = (value :: any).cleanup

			if clean then
				clean(value)
				return
			end
			local destroy2 = (value :: any).Destroy

			if destroy2 then
				destroy2(value)
				return
			end
			local clear2 = (value :: any).Clear

			if clear2 then
				clear2(value)
				return
			end
			local clean2 = (value :: any).Cleanup

			if clean2 then
				clean2(value)
			end
		elseif type == "Instance" then
			DESTROY((value :: any))
		elseif type == "RBXScriptConnection" then
			DISCONNECT((value :: any))
		end
		REMOVE(duster, index)
	end
end

function duster.cleari(duster: { Cleanable }, index: number)
	local value = duster[index]

	if value then
		local type = TYPEOF(value)

		if type == "function" then
			(value :: any)()
		elseif type == "thread" then
			CANCEL((value :: any))
		elseif type == "table" then
			local disconnect1 = (value :: any).Disconnect

			if disconnect1 then
				disconnect1(value)
				return
			end

			local disconnect = (value :: any).disconnect

			if disconnect then
				disconnect(value)
				return
			end

			local destroy = (value :: any).destroy

			if destroy then
				destroy(value)
				return
			end
			local clear = (value :: any).clear

			if clear then
				clear(value)
				return
			end
			local clean = (value :: any).cleanup

			if clean then
				clean(value)
				return
			end
			local destroy2 = (value :: any).Destroy

			if destroy2 then
				destroy2(value)
				return
			end
			local clear2 = (value :: any).Clear

			if clear2 then
				clear2(value)
				return
			end
			local clean2 = (value :: any).Cleanup

			if clean2 then
				clean2(value)
			end
		elseif type == "Instance" then
			DESTROY((value :: any))
		elseif type == "RBXScriptConnection" then
			DISCONNECT((value :: any))
		end
		REMOVE(duster, index)
	end
end

function duster.clear(duster: { Cleanable })
	for _, value in duster do
		local type = TYPEOF(value)

		if type == "function" then
			(value :: any)()
		elseif type == "thread" then
			CANCEL((value :: any))
		elseif type == "table" then
			local disconnect1 = (value :: any).Disconnect

			if disconnect1 then
				disconnect1(value)
				continue
			end

			local disconnect = (value :: any).disconnect

			if disconnect then
				disconnect(value)
				continue
			end

			local destroy = (value :: any).destroy

			if destroy then
				destroy(value)
				continue
			end
			local clear = (value :: any).clear

			if clear then
				clear(value)
				continue
			end
			local clean = (value :: any).cleanup

			if clean then
				clean(value)
				continue
			end
			local destroy2 = (value :: any).Destroy

			if destroy2 then
				destroy2(value)
				continue
			end
			local clear2 = (value :: any).Clear

			if clear2 then
				clear2(value)
				continue
			end
			local clean2 = (value :: any).Cleanup

			if clean2 then
				clean2(value)
			end
		elseif type == "Instance" then
			DESTROY((value :: any))
		elseif type == "RBXScriptConnection" then
			DISCONNECT((value :: any))
		end
	end
	CLEAR(duster)
end

function duster.insert<V>(
	duster: { Cleanable },
	value: V & Cleanable,
	cleaner: (((self: V) -> ()) | () -> ())?
): V
	if cleaner then
		INSERT(duster, function()
			(cleaner :: any)(value)
		end)
	else
		INSERT(duster, value :: any)
	end
	return value
end

return table.freeze(duster)


--!optimize 2
--!strict

local Promise = require(script.Promise)
type Promise<T...> = Promise.TypedPromise<T...>

local LinkToInstanceIndex = setmetatable({}, {
	__tostring = function()
		return "LinkToInstanceIndex"
	end;
})

local INVALID_METHOD_NAME =
	"Object is a %* and as such expected `true?` for the method name and instead got %*. Traceback: %*"
local METHOD_NOT_FOUND_ERROR = "Object %* doesn't have method %*, are you sure you want to add it? Traceback: %*"
local NOT_A_PROMISE = "Invalid argument #1 to 'Janitor:AddPromise' (Promise expected, got %* (%*)) Traceback: %*"

export type Janitor = typeof(setmetatable({} :: {
	CurrentlyCleaning: boolean,
	SuppressInstanceReDestroy: boolean,

	Add: <T>(self: Janitor, object: T, methodName: BooleanOrString?, index: any?) -> T,
	AddObject: <T, A...>(
		self: Janitor,
		constructor: {new: (A...) -> T},
		methodName: BooleanOrString?,
		index: any?,
		A...
	) -> T,
	AddPromise: <T...>(self: Janitor, promiseObject: Promise<T...>) -> Promise<T...>,

	Remove: (self: Janitor, index: any) -> Janitor,
	RemoveNoClean: (self: Janitor, index: any) -> Janitor,

	RemoveList: (self: Janitor, ...any) -> Janitor,
	RemoveListNoClean: (self: Janitor, ...any) -> Janitor,

	Get: (self: Janitor, index: any) -> any?,
	GetAll: (self: Janitor) -> {[any]: any},

	Cleanup: (self: Janitor) -> (),
	Destroy: (self: Janitor) -> (),

	LinkToInstance: (self: Janitor, Object: Instance, allowMultiple: boolean?) -> RBXScriptConnection,
	LinkToInstances: (self: Janitor, ...Instance) -> Janitor,
}, {} :: {__call: (self: Janitor) -> ()}))
type Private = typeof(setmetatable({} :: {
	CurrentlyCleaning: boolean,
	SuppressInstanceReDestroy: boolean,

	[any]: BooleanOrString,

	Add: <T>(self: Private, object: T, methodName: BooleanOrString?, index: any?) -> T,
	AddObject: <T, A...>(
		self: Private,
		constructor: {new: (A...) -> T},
		methodName: BooleanOrString?,
		index: any?,
		A...
	) -> T,
	AddPromise: <T...>(self: Private, promiseObject: Promise<T...>) -> Promise<T...>,

	Remove: (self: Private, index: any) -> Private,
	RemoveNoClean: (self: Private, index: any) -> Private,

	RemoveList: (self: Private, ...any) -> Private,
	RemoveListNoClean: (self: Private, ...any) -> Private,

	Get: (self: Private, index: any) -> any?,
	GetAll: (self: Private) -> {[any]: any},

	Cleanup: (self: Private) -> (),
	Destroy: (self: Private) -> (),

	LinkToInstance: (self: Private, object: Instance, allowMultiple: boolean?) -> RBXScriptConnection,
	LinkToInstances: (self: Private, ...Instance) -> Private,
}, {} :: {__call: (self: Private) -> ()}))
type Static = {
	ClassName: "Janitor",
	CurrentlyCleaning: boolean,
	SuppressInstanceReDestroy: boolean,

	new: () -> Janitor,
	Is: (object: any) -> boolean,
	instanceof: (object: any) -> boolean,
}
type PrivateStatic = Static & {
	__call: (self: Private) -> (),
	__tostring: (self: Private) -> string,
}

local Janitor = {} :: Janitor & Static
local Private = Janitor :: Private & PrivateStatic
Janitor.ClassName = "Janitor"
Janitor.CurrentlyCleaning = true
Janitor.SuppressInstanceReDestroy = false;
(Janitor :: any).__index = Janitor

local Janitors = setmetatable({} :: {[Private]: {[any]: any}}, {__mode = "k"})

local TYPE_DEFAULTS = {
	["function"] = true;
	thread = true;
	RBXScriptConnection = "Disconnect";
}

function Janitor.new(): Janitor
	return setmetatable({
		CurrentlyCleaning = false;
	}, Janitor) :: never
end

function Janitor.Is(object: any): boolean
	return type(object) == "table" and getmetatable(object) == Janitor
end

Janitor.instanceof = Janitor.Is

local function Remove(self: Private, index: any)
	local this = Janitors[self]

	if this then
		local object = this[index]
		if not object then
			return self
		end

		local methodName = self[object]
		if methodName then
			if methodName == true then
				if type(object) == "function" then
					object()
				else
					local wasCancelled: boolean = nil
					if coroutine.running() ~= object then
						wasCancelled = pcall(function()
							task.cancel(object)
						end)
					end

					if not wasCancelled then
						local toCleanup = object
						task.defer(function()
							task.cancel(toCleanup)
						end)
					end
				end
			else
				local objectMethod = object[methodName]
				if objectMethod then
					if self.SuppressInstanceReDestroy and methodName == "Destroy" and typeof(object) == "Instance" then
						pcall(objectMethod, object)
					else
						objectMethod(object)
					end
				end
			end

			self[object] = nil
		end

		this[index] = nil
	end

	return self
end

type BooleanOrString = boolean | string

local function Add<T>(self: Private, object: T, methodName: BooleanOrString?, index: any?): T
	if index then
		Remove(self, index)

		local this = Janitors[self]
		if not this then
			this = {}
			Janitors[self] = this
		end

		this[index] = object
	end

	local typeOf = typeof(object)
	local newMethodName = methodName or TYPE_DEFAULTS[typeOf] or "Destroy"

	if typeOf == "function" or typeOf == "thread" then
		if newMethodName ~= true then
			warn(string.format(INVALID_METHOD_NAME, typeOf, tostring(newMethodName), debug.traceback(nil, 2)))
		end
	else
		if not (object :: never)[newMethodName] then
			warn(
				string.format(
					METHOD_NOT_FOUND_ERROR,
					tostring(object),
					tostring(newMethodName),
					debug.traceback(nil, 2)
				)
			)
		end
	end

	self[object] = newMethodName
	return object
end

Private.Add = Add

function Janitor:AddObject<T, A...>(constructor: {new: (A...) -> T}, methodName: BooleanOrString?, index: any?, ...: A...): T
	return Add(self, constructor.new(...), methodName, index)
end

function Janitor:AddPromise<T...>(promiseObject: Promise<T...>): Promise<T...>
	if not Promise then
		return promiseObject
	end

	if not Promise.is(promiseObject) then
		error(string.format(NOT_A_PROMISE, typeof(promiseObject), tostring(promiseObject), debug.traceback(nil, 2)))
	end

	if promiseObject:getStatus() == Promise.Status.Started then
		local uniqueId = newproxy(false)
		local newPromise = Add(self, Promise.new(function(resolve, _, onCancel)
			if onCancel(function()
				promiseObject:cancel()
			end) then
				return
			end

			resolve(promiseObject)
		end), "cancel", uniqueId)

		newPromise:finally(function()
			Remove(self, uniqueId)
		end)

		return newPromise :: never
	end

	return promiseObject
end

Private.Remove = Remove

function Private:RemoveNoClean(index: any)
	local this = Janitors[self]

	if this then
		local object = this[index]
		if object then
			self[object] = nil
			this[index] = nil
		end
	end

	return self
end

function Janitor:RemoveList(...: any)
	local this = Janitors[self]
	if this then
		local length = select("#", ...)
		if length == 1 then
			return Remove(self, ...)
		end
		if length == 2 then
			local indexA, indexB = ...
			Remove(self, indexA)
			Remove(self, indexB)
			return self
		end
		if length == 3 then
			local indexA, indexB, indexC = ...
			Remove(self, indexA)
			Remove(self, indexB)
			Remove(self, indexC)
			return self
		end

		for selectIndex = 1, length do
			local removeObject = select(selectIndex, ...)
			Remove(self, removeObject)
		end
	end

	return self
end

function Janitor:RemoveListNoClean(...: any)
	local this = Janitors[self]
	if this then
		local length = select("#", ...)
		if length == 1 then
			local indexA = ...
			local object = this[indexA]
			if object then
				self[object] = nil
				this[indexA] = nil
			end
			return self
		end
		if length == 2 then
			local indexA, indexB = ...
			local objectA = this[indexA]
			if objectA then
				self[objectA] = nil
				this[indexA] = nil
			end
			local objectB = this[indexB]
			if objectB then
				self[objectB] = nil
				this[indexB] = nil
			end
			return self
		end
		if length == 3 then
			local indexA, indexB, indexC = ...
			local objectA = this[indexA]
			if objectA then
				self[objectA] = nil
				this[indexA] = nil
			end
			local objectB = this[indexB]
			if objectB then
				self[objectB] = nil
				this[indexB] = nil
			end
			local objectC = this[indexC]
			if objectC then
				self[objectC] = nil
				this[indexC] = nil
			end
			return self
		end

		for selectIndex = 1, length do
			local index = select(selectIndex, ...)
			local object = this[index]
			if object then
				self[object] = nil
				this[index] = nil
			end
		end
	end

	return self
end

function Janitor:Get(index: any): any?
	local this = Janitors[self]
	return if this then this[index] else nil
end

function Janitor:GetAll(): {[any]: any}
	local this = Janitors[self]
	return if this then table.freeze(table.clone(this)) else {}
end

local function GetFenv(self: Private): () -> (any, BooleanOrString)
	return function(): ()
		for object, methodName in next, self do
			if object ~= "SuppressInstanceReDestroy" then
				return object, methodName
			end
		end
	end :: never
end

local function Cleanup(self: Private)
	if not self.CurrentlyCleaning then
		self.CurrentlyCleaning = nil :: never

		local get = GetFenv(self)
		local object, methodName = get()

		while object and methodName do
			if methodName == true then
				if type(object) == "function" then
					object()
				elseif type(object) == "thread" then
					local wasCancelled: boolean = nil
					if coroutine.running() ~= object then
						wasCancelled = pcall(function()
							task.cancel(object)
						end)
					end

					if not wasCancelled then
						local toCleanup = object
						task.defer(function()
							task.cancel(toCleanup)
						end)
					end
				end
			else
				local objectMethod = (object :: never)[methodName] :: (object: unknown) -> ()
				if objectMethod then
					if self.SuppressInstanceReDestroy and methodName == "Destroy" and typeof(object) == "Instance" then
						pcall(objectMethod, object)
					else
						objectMethod(object)
					end
				end
			end

			self[object] = nil
			object, methodName = get()
		end

		local this = Janitors[self]
		if this then
			table.clear(this)
			Janitors[self] = nil
		end

		self.CurrentlyCleaning = false
	end
end

Private.Cleanup = Cleanup

function Janitor:Destroy()
	Cleanup(self)
	table.clear(self :: never)
	setmetatable(self :: any, nil)
end

Private.__call = Cleanup

local function LinkToInstance(self: Private, object: Instance, allowMultiple: boolean?): RBXScriptConnection
	local indexToUse = if allowMultiple then newproxy(false) else LinkToInstanceIndex

	return Add(self, object.Destroying:Connect(function()
		Cleanup(self)
	end), "Disconnect", indexToUse)
end

Private.LinkToInstance = LinkToInstance;

(Janitor :: never).LegacyLinkToInstance = LinkToInstance

function Janitor:LinkToInstances(...: Instance)
	local manualCleanup = Janitor.new()
	for index = 1, select("#", ...) do
		local object = select(index, ...)
		if typeof(object) ~= "Instance" then
			continue
		end

		manualCleanup:Add(LinkToInstance(self, object, true), "Disconnect")
	end

	return manualCleanup
end

function Private:__tostring()
	return "Janitor"
end

return Janitor :: Static