--!strict

local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")
local CollectionService = game:GetService("CollectionService")
local ServerScriptService = game:GetService("ServerScriptService")
local ScriptEditorService = game:GetService("ScriptEditorService")

if not RunService:IsEdit() then
	return
end
if not _G["AutoComplete+"] then
	_G["AutoComplete+"] = {}
end

local Types = require(script.Types)

print("Running AutoComplete+ v1.13.4. View ROSS server for update info.")

local pluginGlobalIndex: Types.pluginGlobalIndex = _G["AutoComplete+"]

pluginGlobalIndex.ServiceImportStyle = plugin:GetSetting("ServiceImportStyle") or "PascalCase"
pluginGlobalIndex.ModuleImportStyle = plugin:GetSetting("ModuleImportStyle") or "PascalCase"
pluginGlobalIndex.InstanceImportStyle = plugin:GetSetting("InstanceImportStyle") or "PascalCase"
pluginGlobalIndex.SuggestionCharacter = plugin:GetSetting("SuggestionCharacter")
pluginGlobalIndex.SnippetCharacter = plugin:GetSetting("SnippetCharacter")
pluginGlobalIndex.plugin = plugin

local Transformers = require(script.Transformers)
local UI = require(script.UI)

if pluginGlobalIndex.Connections then
	for _, Connection in pluginGlobalIndex.Connections do
		Connection:Disconnect()
	end
end
pluginGlobalIndex.Connections = {}

local NO_IMPORT_TAG: string = "_NoImport"
local CLIENT_ONLY_TAG: string = "_ClientOnly"
local IMPORTABLE_TAG: string = "_Importable"
local MAXIMUM_LINES_TO_PARSE: number = 100
local SERVICE_PREFIX: string = "[~]"
local MODULE_PREFIX: string = "[;]"
local INSTANCE_PREFIX: string = "[>]"
local DUPLICATE_MODULE_IDENTIFIER: string = "+"

local services: { [string]: string } = {
	-- Manual definition as run service is labelled as "Run Service"
	["runservice"] = "RunService";
	-- Manual definitions as these do not appear as children of game until they are initialised by a :GetService() call.
	["scriptcontext"] = "ScriptContext";
	["datastoreservice"] = "DataStoreService";
	["messagingservice"] = "MessagingService";
	["memorystoreservice"] = "MemoryStoreService";
	["teleportservice"] = "TeleportService"
}
local modules: { ModuleScript } = {}
local instances: { Instance } = {}
local noImportFolders: { Folder } = {}
local moduleScriptOrigins: { Instance } = {
	game.ReplicatedFirst;
	game.ReplicatedStorage;
	game.StarterPlayer.StarterPlayerScripts;
	ServerScriptService;
	ServerStorage
}

for _, service in game:GetChildren() do
	local serviceName = service.Name
	local success, service = pcall(function()
		return game:FindService(serviceName)
	end)

	if success and service then
		services[string.lower(serviceName)] = serviceName
	end
end

local function addItem(item: Instance | ModuleScript): ()
	local isModule = item:IsA("ModuleScript")
	local destination = if isModule then modules else instances

	if table.find(destination, item) then
		return
	end
	if isModule then
		if item:HasTag(NO_IMPORT_TAG) then
			return
		end
		for _, folder in noImportFolders do
			if item:IsDescendantOf(folder) then
				return
			end
		end
	end

	table.insert(destination, item)
end
local function removeItem(item: Instance | ModuleScript): ()
	local origin = if item:IsA("ModuleScript") then modules else instances
	local found = table.find(origin, item)
	if found then
		table.remove(origin, found)
	end
end
local function registerNoImportInstance(instance: Folder | ModuleScript): ()
	if instance:IsA("Folder") then
		if not table.find(noImportFolders, instance) then
			table.insert(noImportFolders, instance)
		end

		for _, descendant in instance:GetDescendants() do
			if descendant:IsA("ModuleScript") then
				removeItem(descendant)
			end
		end
	elseif instance:IsA("ModuleScript") then
		removeItem(instance)
	end
end

for _, noImportInstance: Folder | ModuleScript in CollectionService:GetTagged(NO_IMPORT_TAG) do
	registerNoImportInstance(noImportInstance)
end
for _, instance: Instance in CollectionService:GetTagged(IMPORTABLE_TAG) do
	addItem(instance)
end

table.insert(pluginGlobalIndex.Connections, CollectionService:GetInstanceAddedSignal(NO_IMPORT_TAG):Connect(function(instance: Folder | ModuleScript)
	registerNoImportInstance(instance)
end))
table.insert(pluginGlobalIndex.Connections, CollectionService:GetInstanceRemovedSignal(NO_IMPORT_TAG):Connect(function(instance: Folder | ModuleScript)
	if instance:IsA("Folder") then
		local found = table.find(noImportFolders, instance)
		if found then
			table.remove(noImportFolders, found)
		end

		for _, descendant in instance:GetDescendants() do
			if descendant:IsA("ModuleScript") and not descendant:HasTag(NO_IMPORT_TAG) then
				addItem(descendant)
			end
		end
	elseif instance:IsA("ModuleScript") then
		addItem(instance)
	end
end))
table.insert(pluginGlobalIndex.Connections, CollectionService:GetInstanceAddedSignal(IMPORTABLE_TAG):Connect(function(instance: Instance)
	if instance:IsA("ModuleScript") then
		return
	end

	addItem(instance)
end))
table.insert(pluginGlobalIndex.Connections, CollectionService:GetInstanceRemovedSignal(IMPORTABLE_TAG):Connect(function(instance: Instance)
	if instance:IsA("ModuleScript") then
		return
	end

	removeItem(instance)
end))

for _, origin in moduleScriptOrigins do
	for _, moduleScript in origin:GetDescendants() do
		if moduleScript:IsA("ModuleScript") then
			addItem(moduleScript)
		end
	end
	table.insert(pluginGlobalIndex.Connections, origin.DescendantAdded:Connect(function(descendant)
		if descendant:IsA("ModuleScript") then
			addItem(descendant)
		end
	end))
	table.insert(pluginGlobalIndex.Connections, origin.DescendantRemoving:Connect(function(descendant)
		if descendant:IsA("ModuleScript") then
			removeItem(descendant)
		end
	end))
end

local duplicateModules: { [string]: { ModuleScript } } = {}
-- Imports the service/modules script. importType 1 -> Service, 2 -> ModuleScript, 3 -> Instance
local function import(document: ScriptDocument, importName: string, importType: number): number?
	local currentScript: LuaSourceContainer = document:GetScript()
	local documentLines: { string } = string.split(document:GetText(), "\n")

	local lastServiceImportGroup: Types.lastImportGroup = { group = {}, all = {}, prePadding = 0 }
	local lastModuleImportGroup: Types.lastImportGroup = { group = {}, all = {}, prePadding = 0 }
	local lastInstanceImportGroup: Types.lastImportGroup = { group = {}, all = {}, prePadding = 0 }

	local serviceGroupImportStartLine: number = 1
	local selectedLine: number = document:GetSelection()
	local commentBlocks: number = 0

	for lineNumber, lineContents in documentLines do
		if lineNumber > MAXIMUM_LINES_TO_PARSE or lineNumber > selectedLine then
			break
		end

		if (not lastModuleImportGroup.startLine and not lastServiceImportGroup.startLine) and (commentBlocks > 0 or string.sub(lineContents, 1, 2) == "--") then
			if string.find(lineContents, "%-%-%[%[") then
				commentBlocks += 1
			end
			if string.find(lineContents, "%]%]") then
				commentBlocks -= 1
			end
			serviceGroupImportStartLine += 1
			continue
		end

		local serviceMatch = string.match(lineContents, 'local[%s%w_]+%s=%sgame:GetService%("%w+"%)')
		local moduleMatch = string.match(lineContents, 'local[%s%w_]+%s=%srequire%([%w%p%s]+%)') or string.match(lineContents, 'require%([%w%p%s]+%)')
		local existingImport = serviceMatch or moduleMatch
		-- If neither module & service are imported on this line but a local exists,
		if not existingImport then
			-- check if it's an instance import
			local instanceName = string.find(lineContents, "local") and Transformers.getNameOfImportedItemFromDeclaration(lineContents)
			if instanceName then
				for _, instance in instances do
					if instance.Name == instanceName then
						existingImport = lineContents
						break
					end
				end
			end
		end
		if not existingImport then
			continue
		end
		if string.match(string.sub(lineContents, 1, 1), "%s+") then
			continue
		end
		-- Check if the line contains what we already imported to avoid double importing
		if string.find(Transformers.asNeutralImportName(existingImport), "^" .. Transformers.asNeutralImportName(importName)) then
			return
		end

		-- Otherwise, build up the groups.
		local lastImportGroup = if serviceMatch then lastServiceImportGroup elseif moduleMatch then lastModuleImportGroup else lastInstanceImportGroup
		if not lastImportGroup.startLine then
			lastImportGroup.startLine = lineNumber
		else
			if lastImportGroup.startLine + #lastImportGroup.group < lineNumber then
				-- If the next group is separated from the last, restart from next group
				lastImportGroup.startLine = lineNumber
				lastImportGroup.group = {}
			end
		end

		table.insert(lastImportGroup.all, existingImport)
		table.insert(lastImportGroup.group, existingImport)
	end

	local function importService(serviceName: string?): ()
		if not serviceName then
			return
		end

		-- Import service. No collateral import checks necessary.
		lastServiceImportGroup.receivedImport = true
		table.insert(lastServiceImportGroup.group, `local {Transformers.processVariableName(serviceName, 1)} = game:GetService("{serviceName}")`)

		if not lastServiceImportGroup.startLine then
			lastServiceImportGroup.startLine = serviceGroupImportStartLine
		end

		if #lastServiceImportGroup.group == 1 and serviceGroupImportStartLine > 1 then
			lastServiceImportGroup.prePadding = 1
		end
	end

	if importType == 1 then
		importService(importName)
	elseif importType == 2 then
		-- Import module. Duplicate import checks necessary
		local importModuleScript: ModuleScript
		-- check if module is a duplicate import attempt
		if string.sub(importName, -2, -2) == DUPLICATE_MODULE_IDENTIFIER then
			-- this does mean there is a limit of 9 duplicates
			importModuleScript = duplicateModules[string.sub(importName, 1, -3)][tonumber(string.sub(importName, -1, -1)) :: number]
		else
			local loweredImportName = string.lower(importName)
			for _, moduleScript in modules do
				if string.lower(moduleScript.Name) == loweredImportName then
					importModuleScript = moduleScript
					break
				end
			end
		end

		lastModuleImportGroup.receivedImport = true
		local path, noService = Transformers.transformInstancePathToValid(importModuleScript, currentScript)
		if not noService then
			importService(Transformers.getNeededServiceNameToImportFromModuleOrInstance(importModuleScript, lastServiceImportGroup))
		end

		table.insert(lastModuleImportGroup.group, `local {Transformers.processVariableName(importName, importType)} = require({path})`)

		if not lastModuleImportGroup.startLine then
			-- place after last service import group
			lastModuleImportGroup.startLine = (lastServiceImportGroup.startLine or serviceGroupImportStartLine) + #lastServiceImportGroup.group
			if lastModuleImportGroup.startLine ~= 1 and #lastModuleImportGroup.group == 1 then
				if not (lastServiceImportGroup.receivedImport and #lastServiceImportGroup.group == 1) then
					if documentLines[lastModuleImportGroup.startLine :: any - 1] ~= "" then
						lastModuleImportGroup.prePadding = 1
					end
				end
			end
		end
	elseif importType == 3 then
		local importInstance
		local loweredImportName = string.lower(importName)
		for _, instance in instances do
			if string.lower(instance.Name) == loweredImportName then
				importInstance = instance
				break
			end
		end

		lastInstanceImportGroup.receivedImport = true
		local path, noService = Transformers.transformInstancePathToValid(importInstance, currentScript)
		if not noService then
			importService(Transformers.getNeededServiceNameToImportFromModuleOrInstance(importInstance, lastServiceImportGroup))
		end

		table.insert(lastInstanceImportGroup.group, `local {Transformers.processVariableName(importName, importType)} = {path}`)

		if not lastInstanceImportGroup.startLine then
			local startLine
			if lastModuleImportGroup.startLine then
				startLine = lastModuleImportGroup.startLine + #lastModuleImportGroup.group
			else
				startLine = (lastServiceImportGroup.startLine or serviceGroupImportStartLine) + #lastServiceImportGroup.group
			end
			lastInstanceImportGroup.startLine = startLine

			if startLine ~= 1 and #lastInstanceImportGroup.group == 1 then
				if not (lastServiceImportGroup.receivedImport and #lastServiceImportGroup.group == 1) then
					if documentLines[startLine - 1] ~= "" then
						lastInstanceImportGroup.prePadding = 1
					end
				end
			end
		end
	end

	table.sort(lastServiceImportGroup.group, function(a, b) return #a < #b end)
	table.sort(lastInstanceImportGroup.group, function(a, b) return #a < #b end)

	local numberOfLines: number = document:GetLineCount()
	local lastDocumentLineCount: number = numberOfLines
	local serviceImportMovedLine: boolean = false

	for i = 1, 3 do
		local lastImportGroup: Types.lastImportGroup = if i == 1 then lastServiceImportGroup elseif i == 2 then lastModuleImportGroup else lastInstanceImportGroup
		if not lastImportGroup.receivedImport then
			continue
		end

		local lineCount: number = i == 1 and numberOfLines or document:GetLineCount()
		local lineCountDifference: number = lineCount - lastDocumentLineCount
		local text: string = string.rep("\n", lastImportGroup.prePadding) .. table.concat(lastImportGroup.group, "\n")
		local importGroupStartLine: number = math.min(lastImportGroup.startLine :: number + lineCountDifference, lineCount)
		if serviceImportMovedLine and lastServiceImportGroup.receivedImport and #lastImportGroup.group == 1 then
			-- if double import (service & module | service & instance), decrease by 1 if service caused a line to move
			-- so that the +1 lines are accounted for and the 2nd import is not offset
			importGroupStartLine -= 1
		end

		local nextLineNumber: number = importGroupStartLine + #lastImportGroup.group - 1
		local nextLine: string? = documentLines[nextLineNumber]

		if nextLine and #nextLine > 0 and not (i == importType and string.find(nextLine, importName)) then
			-- if there exists content at point of import, move it up so it doesn't get erased
			if i == 1 then
				serviceImportMovedLine = true
			end
			-- space a line from import but don't create an extra space if it's an instance import
			text = text .. (i == 3 and "\n" or "\n\n") .. nextLine .. "\n"
		else
			-- space a line from import
			text ..= "\n\n"
		end

		lastDocumentLineCount = document:GetLineCount()

		document:EditTextAsync(text, importGroupStartLine, 1, math.min(importGroupStartLine + #lastImportGroup.group, lastDocumentLineCount), 1)
		documentLines = string.split(document:GetText(), "\n")
	end

	-- return the number of lines added to the document
	return document:GetLineCount() - numberOfLines
end

local function generateAutoCompleteSuggestion(item: string | ModuleScript | Instance, locatorPosition: Types.subRange, length: number, duplicateIndex: number?, currentScript: LuaSourceContainer?): Types.autoCompleteSuggestion
	local isService = typeof(item) == "string"
	local isModule = not isService and (item :: Instance):IsA("ModuleScript")

	local itemName: string
	local kind: Enum.CompletionItemKind
	local itemType: "service" | "module" | "instance"
	local detailStart: "Get Service:" | "Require:" | "Import:"
	local documentationEnd: string
	local prefix: string

	if isService then
		itemName = services[item :: string]
		kind = Enum.CompletionItemKind.Text
		itemType = "service"
		detailStart = "Get Service:"
		documentationEnd = "."
		prefix = SERVICE_PREFIX
	elseif isModule then
		itemName = (item :: ModuleScript).Name .. (duplicateIndex and (DUPLICATE_MODULE_IDENTIFIER .. duplicateIndex) or "")
		kind = Enum.CompletionItemKind.Text
		itemType = "module"
		detailStart = "Require:"
		documentationEnd = ` from {Transformers.transformInstancePathToValid(item :: ModuleScript, currentScript :: LuaSourceContainer)}.`
		prefix = MODULE_PREFIX
	else
		itemName = (item :: Instance).Name
		kind = Enum.CompletionItemKind.EnumMember
		itemType = "instance"
		detailStart = "Import:"
		documentationEnd = ` from {Transformers.transformInstancePathToValid(item :: ModuleScript, currentScript :: LuaSourceContainer)}.`
		prefix = INSTANCE_PREFIX
	end

	local suggestion: Types.autoCompleteSuggestion = {
		label = itemName;
		detail = `{detailStart} {itemName}`;
		kind = kind;
		documentation = { value = `Imports the {itemType} [{itemName}]{documentationEnd}` };
		textEdit = {
			insert = {
				start = {
					character = locatorPosition.character - length;
					line = locatorPosition.line
				};
				["end"] = {
					character = locatorPosition.character;
					line = locatorPosition.line
				}
			};
			replace = {
				start = {
					character = locatorPosition.character - length;
					line = locatorPosition.line
				};
				["end"] = {
					character = locatorPosition.character;
					line = locatorPosition.line
				}
			}
		} :: Types.textEdit
	}
	suggestion.textEdit.newText = prefix .. itemName

	return suggestion
end

pcall(function()
	ScriptEditorService:DeregisterAutocompleteCallback("AutoComplete")
end)
ScriptEditorService:RegisterAutocompleteCallback("AutoComplete", 1, function(locator, existingSuggestions)
	local document: ScriptDocument = locator.textDocument.document
	if document:IsCommandBar() then return existingSuggestions end

	local currentLine: string = document:GetText(locator.position.line, 1, locator.position.line, locator.position.character)

	local suggestionCharacter: string? = pluginGlobalIndex.SuggestionCharacter
	local currentWordStart: number?, currentWordEnd: number?, currentWord: string

	if suggestionCharacter and suggestionCharacter ~= "" then
		currentWordStart, currentWordEnd = string.find(currentLine, suggestionCharacter .. "([^" .. suggestionCharacter .. "]*)$")
		if not currentWordEnd or locator.position.character <= currentWordEnd then
			return existingSuggestions
		end
		currentWord = string.lower(string.sub(currentLine, currentWordStart :: number + 1, currentWordEnd))
	else
		currentWordStart, currentWordEnd = string.find(currentLine, "%w+$")
		if not (currentWordStart and currentWordEnd) or #existingSuggestions.items == 0 then
			return existingSuggestions
		end

		if currentWordStart > 1 then
			local previousCharacter = string.sub(currentLine, currentWordStart - 1, currentWordStart - 1)
			if previousCharacter ~= "(" and not string.find(previousCharacter, "%s") then
				return existingSuggestions
			end
		end

		currentWord = string.lower(string.sub(currentLine, currentWordStart, currentWordEnd))
	end

	local currentWordLength: number = string.len(currentWord)
	local documentLines: { string } = string.split(document:GetText(), "\n")

	-- Gather already imported
	local alreadyImported: { [string]: number } = {}
	for line, lineContents in documentLines do
		if line > MAXIMUM_LINES_TO_PARSE or line > locator.position.line :: number then
			break
		end

		local serviceImport: string? = string.match(lineContents, '%(%s*"(.-)"%s*%)')
		if serviceImport then
			alreadyImported[string.lower(serviceImport)] = line
		else
			local moduleImport = string.find(lineContents, "require") and Transformers.getNameOfImportedItemFromDeclaration(lineContents)
			if moduleImport then
				local variableName = Transformers.getVariableNameFromLine(lineContents)
				if variableName ~= moduleImport and string.find(variableName, "_") then
					alreadyImported[variableName] = line
				else
					alreadyImported[moduleImport] = line
				end
				continue
			end

			local instanceImport = string.find(lineContents, "local") and Transformers.getNameOfImportedItemFromDeclaration(lineContents)
			if instanceImport then
				alreadyImported[instanceImport] = line
			end
		end
	end

	local currentScript: LuaSourceContainer = locator.textDocument.script
	local isInServerContext = currentScript:IsDescendantOf(ServerScriptService)
		or currentScript:IsDescendantOf(ServerStorage)

	-- Services
	for lower_serviceName in services do
		if string.find(lower_serviceName, currentWord) and not alreadyImported[lower_serviceName] then
			table.insert(existingSuggestions.items, generateAutoCompleteSuggestion(lower_serviceName, locator.position :: Types.subRange, currentWordLength))
		end
	end

	-- ModuleScripts
	duplicateModules = {}
	local clientOnlyModules = CollectionService:GetTagged(CLIENT_ONLY_TAG)
	for _, moduleScript in modules do
		local moduleScriptName: string = moduleScript.Name
		local neutralName: string = Transformers.asNeutralImportName(moduleScriptName)

		if moduleScript == currentScript or not string.find(string.lower(moduleScriptName), currentWord) then
			continue
		end

		if isInServerContext then
			if moduleScript:HasTag(CLIENT_ONLY_TAG) then
				continue
			else
				local skip: boolean = false
				for _, clientOnlyModule in clientOnlyModules do
					if moduleScript:IsDescendantOf(clientOnlyModule) then
						skip = true
						break
					end
				end
				if skip then
					continue
				end
			end
		elseif moduleScript:IsDescendantOf(ServerScriptService) or moduleScript:IsDescendantOf(ServerStorage) then
			continue
		end

		local duplicateIndex: number?
		if duplicateModules[moduleScriptName] then
			duplicateIndex = #duplicateModules[moduleScriptName] + 1
			table.insert(duplicateModules[moduleScriptName], moduleScript)

			if alreadyImported[moduleScriptName .. "_" .. duplicateIndex :: number] or alreadyImported[neutralName .. "_" .. duplicateIndex :: number] then
				continue
			end
		else
			duplicateModules[moduleScriptName] = {}

			if alreadyImported[moduleScriptName] or alreadyImported[neutralName] then
				continue
			end
		end

		table.insert(existingSuggestions.items, generateAutoCompleteSuggestion(moduleScript, locator.position :: Types.subRange, currentWordLength, duplicateIndex, currentScript))
	end

	-- Instances
	for _, instance in instances do
		if alreadyImported[instance.Name] then
			continue
		end
		if (instance:IsDescendantOf(ServerScriptService) or instance:IsDescendantOf(ServerStorage)) and not isInServerContext then
			continue
		end

		table.insert(existingSuggestions.items, generateAutoCompleteSuggestion(instance, locator.position :: Types.subRange, currentWordLength, nil, currentScript))
	end

	return existingSuggestions
end)

table.insert(pluginGlobalIndex.Connections, ScriptEditorService.TextDocumentDidChange:Connect(function(document, changesArray: { { ["text"]: string, ["range"]: { ["end"]: Types.subRange, ["start"]: Types.subRange } } })
	if document:IsCommandBar() then
		return
	end

	local change = changesArray[1]
	local textLength: number = string.len(change.text)

	if textLength <= 1 then
		-- check if typing normally
		return
	end

	local firstThreeCharacters: string = string.sub(change.text, 1, 3)
	local importType: number
	if firstThreeCharacters == SERVICE_PREFIX then
		importType = 1
	elseif firstThreeCharacters == MODULE_PREFIX then
		importType = 2
	elseif firstThreeCharacters == INSTANCE_PREFIX then
		importType = 3
	else
		return
	end

	local importWhat: string = string.sub(change.text, 4, textLength)
	local lines: { string } = string.split(document:GetText(), "\n")
	local selectedLine: number = document:GetSelection()

	if selectedLine ~= change.range.start.line then
		-- Undo check 1
		return
	end
	for line, lineContents in lines do
		if line > MAXIMUM_LINES_TO_PARSE or line > selectedLine then
			break
		end

		if string.find(lineContents, `local {Transformers.processVariableName(importWhat, importType)} =`, 1, true) then
			-- Undo check 2
			return
		end
	end

	local padding: number? = import(document, importWhat, importType)
	if not padding then
		return
	end

	local currentLineContents: string = lines[change.range.start.line]
	local newLine: number = change.range.start.line + padding
	local findStart: number?, findEnd: number? = string.find("^" .. Transformers.asNeutralImportName(currentLineContents), Transformers.asNeutralImportName(change.text))
	if not findStart or not findEnd then
		return
	end

	local startCharacterOffset: number = 2
	if pluginGlobalIndex.SuggestionCharacter and pluginGlobalIndex.SuggestionCharacter ~= "" then
		startCharacterOffset = 3
	end

	document:EditTextAsync(
		string.sub(currentLineContents, 1, findStart - startCharacterOffset)
			.. Transformers.processVariableName(importWhat, importType)
			.. string.sub(currentLineContents, findEnd),
		newLine,
		1,
		newLine,
		string.len(string.split(document:GetText(), "\n")[newLine]) + 1
	)

	document:RequestSetSelectionAsync(newLine, string.len(string.sub(currentLineContents, 1, findStart - startCharacterOffset) .. importWhat) + 1)
end))

table.insert(pluginGlobalIndex.Connections, plugin.Unloading:Once(function()
	print("Disabled AutoComplete+")

	for _, Connection in pluginGlobalIndex.Connections do
		Connection:Disconnect()
	end
	pluginGlobalIndex.Connections = {}
	ScriptEditorService:DeregisterAutocompleteCallback("AutoComplete")
end))

UI.Setup()


--!strict

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

local pluginGlobalIndex: Types.pluginGlobalIndex = _G["AutoComplete+"]

local module = {}

function module.setupDropper(DropperButton: TextButton, DropperOptionTemplate: TextButton, SettingName: string): ()
	local DropperOptions: { TextButton } = {}

	local function ClearOptions(): ()
		for _, DropperOption in DropperOptions do
			DropperOption:Destroy()
		end

		DropperButton:RemoveTag("Active")
	end

	DropperButton.MouseButton1Click:Connect(function()
		if DropperButton:HasTag("Active") then
			ClearOptions()
			return
		end

		DropperButton:AddTag("Active")

		local Choices: { string } = string.split(DropperButton:GetAttribute("Options"), ",")

		for i, Choice in Choices do
			local DropperOption = DropperOptionTemplate:Clone()
			DropperOption.Text = Choice
			DropperOption.ZIndex = 9
			DropperOption.Position = UDim2.fromScale(0, i)
			DropperOption.Parent = DropperButton.Parent

			DropperOption.MouseButton1Click:Connect(function()
				ClearOptions()

				pluginGlobalIndex[SettingName] = Choice
				pluginGlobalIndex.plugin:SetSetting(SettingName, Choice);
				(DropperButton.Parent :: TextLabel).Text = Choice
			end)

			table.insert(DropperOptions, DropperOption)
		end
	end)
end

return module


--!strict

local Types = require(script.Parent.Types)

local pluginGlobalIndex: Types.pluginGlobalIndex = _G["AutoComplete+"]

local module = {}

local blacklistedCharacters: { [string]: true } = {}
for _, character in { " ", "-", "+", ",", ".", "/", "%", "$", "£", "&", "*", "(", ")", "'", "[", "]", "{", "}", "#", "?", "!", "~", "@", "=", ":", ";" } do
	blacklistedCharacters[character] = true
end
function module.asNeutralImportName(importName: string): string
	local result: { string } = table.create(#importName)
	for i, letter in string.split(importName, "") do
		result[i] = blacklistedCharacters[letter] and "_" or letter
	end

	return table.concat(result)
end

function module.processVariableName(variableDeclaration: string, importType: number): string
	local newVariableDeclaration: string = module.asNeutralImportName(variableDeclaration)
	if tonumber(string.sub(newVariableDeclaration, 1, 1)) then
		newVariableDeclaration = "_" .. string.sub(newVariableDeclaration, 2)
	end

	local importStyle = if importType == 1 then pluginGlobalIndex.ServiceImportStyle elseif importType == 2 then pluginGlobalIndex.ModuleImportStyle else pluginGlobalIndex.InstanceImportStyle
	-- PascalCase is default -> we do nothing (assume the instance name is in PascalCase)

	if importStyle == "camelCase" then
		return string.lower(string.sub(newVariableDeclaration, 1, 1)) .. string.sub(newVariableDeclaration, 2)
	elseif importStyle == "snake_case" then
		local spaced = string.gsub(newVariableDeclaration, "(%u)", " %1")
		local stringTable = string.split(spaced, " ")
		local result = table.create(#stringTable)

		for i, character in stringTable do
			result[i] = string.lower(character)
		end

		return table.concat(result, "_", 2)
	end

	return newVariableDeclaration
end

-- for when an instance's path needs its characters transformed to allow for it to exist in a script
function module.transformInstancePathToValid(instance: ModuleScript | Instance, importingScript: LuaSourceContainer): (string, boolean)
	local newImportPath: string = ""
	local lastAncestor: Instance

	local didNotUseService: boolean = false
	local prefix: string = "script"
	if instance:IsDescendantOf(importingScript) then
		didNotUseService = true
		lastAncestor = importingScript
	elseif instance:IsDescendantOf(importingScript.Parent :: any) then
		didNotUseService = true
		lastAncestor = importingScript.Parent :: any
		prefix = "script.Parent"
	else
		lastAncestor = game[string.split(instance:GetFullName(), ".")[1]]
	end

	local lastImportWasTransformed: boolean = false
	local parent: Instance = instance
	while parent ~= lastAncestor do
		local parentName: string = parent.Name
		local parentNameAsNeutral = module.asNeutralImportName(parentName)

		if parentNameAsNeutral ~= parentName or tonumber(string.sub(parentName, 1, 1)) then
			if not lastImportWasTransformed and parent ~= instance then
				newImportPath = "[\"" .. parentName .. "\"]" .. "." .. newImportPath
			else
				newImportPath = "[\"" .. parentName .. "\"]" .. newImportPath
			end

			lastImportWasTransformed = true
		else
			if lastImportWasTransformed or parent == instance then
				newImportPath = parentName .. newImportPath
			else
				newImportPath = parentName .. "." .. newImportPath
			end

			lastImportWasTransformed = false
		end

		parent = parent.Parent :: ModuleScript | Instance
	end

	local lastAncestorName = if didNotUseService then prefix else module.processVariableName(lastAncestor.Name, 1)
	if lastImportWasTransformed then
		newImportPath = lastAncestorName .. newImportPath
	else
		newImportPath = lastAncestorName .. "." .. newImportPath
	end

	return newImportPath, didNotUseService
end

function module.getNameOfImportedItemFromDeclaration(declaration: string): string
	local itemNameResult: { string } = {}

	local function walkBackUntil(terminator: string, offset: number): ()
		-- Walk backwards until a terminator, collecting characters
		local i: number = #declaration - offset
		while i > 0 do
			local character: string = string.sub(declaration, i, i)
			if character == terminator then
				break
			end
			table.insert(itemNameResult, 1, character)
			i -= 1
		end
	end

	local offset: number = 0
	if string.sub(declaration, -1) == ")" then
		offset = 1
	end
	if string.sub(declaration, -2, -2) ~= "]" then
		-- .NAME
		walkBackUntil(".", offset)
	else
		-- ["NAME"]
		walkBackUntil("\"", offset + 2)
	end

	return table.concat(itemNameResult)
end

function module.getNeededServiceNameToImportFromModuleOrInstance(instance: ModuleScript | Instance, lastServiceImportGroup: Types.lastImportGroup): string?
	local serviceName: string = string.split(instance:GetFullName(), ".")[1]
	local neutralServiceImport: string = module.asNeutralImportName(`local {module.processVariableName(serviceName, 1)} = game:GetService("{serviceName}")`)

	for _, alreadyImportedService in lastServiceImportGroup.all do
		if string.find(alreadyImportedService, serviceName) then
			return nil
		end
	end

	return serviceName
end

function module.getVariableNameFromLine(lineContents: string): string
	local variableName: { string } = {}

	for i = 7, #lineContents do
		local character: string = string.sub(lineContents, i, i)
		if character == " " or character == "=" then
			break
		end

		variableName[i - 6] = character
	end

	return table.concat(variableName)
end

return module


--!strict

local module = {}

export type ImportStyle = "PascalCase" | "camelCase" | "snake_case"
export type pluginGlobalIndex = {
	["ServiceImportStyle"]: ImportStyle,
	["ModuleImportStyle"]: ImportStyle,
	["InstanceImportStyle"]: ImportStyle,

	["SuggestionCharacter"]: string?,
	["SnippetCharacter"]: string?,
	["Connections"]: { RBXScriptConnection };
	["plugin"]: typeof(plugin)
}
export type subRange = { ["character"]: number, ["line"]: number }
export type textEdit = {
	["insert"]: {
		["end"]: subRange;
		["start"]: subRange
	};
	["replace"]: {
		["end"]: subRange;
		["start"]: subRange
	};
	["newText"]: string
}
export type autoCompleteSuggestion = {
	["codeSample"]: string?;
	["detail"]: string;
	["documentation"]: { ["value"]: string };
	["kind"]: Enum.CompletionItemKind;
	["label"]: string;
	["learnMoreLink"]: string?;
	["overloads"]: number?;
	["preselect"]: boolean?;
	["tags"]: { Enum.CompletionItemTag }?;
	["textEdit"]: textEdit
}
export type lastImportGroup = {
	["group"]: { string };
	["all"]: { string };
	["startLine"]: number?;
	["receivedImport"]: true?;
	["prePadding"]: number
}

return module


--!strict

local Types = require(script.Parent.Types)
local Dropper = require(script.Components.Dropper)

local DropperOptionTemplate = script.Templates.DropperOptionTemplate

local pluginGlobalIndex: Types.pluginGlobalIndex = _G["AutoComplete+"]

local module = {}

function module.Setup(): ()
	local DockWidgetPluginGui = pluginGlobalIndex.plugin:CreateDockWidgetPluginGui(
		"StylePicker",
		DockWidgetPluginGuiInfo.new(
			Enum.InitialDockState.Float,
			false,
			true,
			200,
			150,
			200,
			150
		)
	)

	local UI = script.Frame
	local SuggestionCharacterTextBox = UI.SuggestionCharacter.TextBox

	SuggestionCharacterTextBox.Text = pluginGlobalIndex.SuggestionCharacter or ""

	table.insert(pluginGlobalIndex.Connections, UI.Title.Close.MouseButton1Click:Connect(function()
		DockWidgetPluginGui.Enabled = false
	end))

	table.insert(pluginGlobalIndex.Connections, SuggestionCharacterTextBox:GetPropertyChangedSignal("Text"):Connect(function()
		SuggestionCharacterTextBox.Text = string.sub(SuggestionCharacterTextBox.Text, #SuggestionCharacterTextBox.Text)

		local newSuggestionCharacter: string? = SuggestionCharacterTextBox.Text
		if newSuggestionCharacter == "" then
			newSuggestionCharacter = nil
		end

		pluginGlobalIndex.SuggestionCharacter = newSuggestionCharacter
		pluginGlobalIndex.plugin:SetSetting("SuggestionCharacter", newSuggestionCharacter)
	end))

	UI.ImportCasing.Service.TextLabel.Text = pluginGlobalIndex.ServiceImportStyle
	Dropper.setupDropper(UI.ImportCasing.Service.TextLabel.Dropper, DropperOptionTemplate, "ServiceImportStyle")

	UI.ImportCasing.Module.TextLabel.Text = pluginGlobalIndex.ModuleImportStyle
	Dropper.setupDropper(UI.ImportCasing.Module.TextLabel.Dropper, DropperOptionTemplate, "ModuleImportStyle")

	UI.ImportCasing.Instance.TextLabel.Text = pluginGlobalIndex.InstanceImportStyle
	Dropper.setupDropper(UI.ImportCasing.Instance.TextLabel.Dropper, DropperOptionTemplate, "InstanceImportStyle")

	DockWidgetPluginGui.ZIndexBehavior = Enum.ZIndexBehavior.Global
	UI.Parent = DockWidgetPluginGui

	local Button = pluginGlobalIndex.plugin:CreateToolbar("AutoComplete+"):CreateButton("AutoComplete+", "Open Menu", "rbxassetid://111947305844735")
	Button.Click:Connect(function()
		DockWidgetPluginGui.Enabled = not DockWidgetPluginGui.Enabled
	end)
end

return module


--!optimize 2

local threads = {}
local function reusableThreadCall(callback, thread, ...)
	callback(...)
	table.insert(threads, thread)
end
local function reusableThread()
	while true do
		reusableThreadCall(coroutine.yield())
	end
end

return {
	Spawn = function(callback, ...)
		local length = #threads
		if length == 0 then
			local thread = coroutine.create(reusableThread)
			coroutine.resume(thread)
			return task.spawn(thread, callback, thread, ...)
		else
			local thread = threads[length]
			threads[length] = nil -- Remove from free threads list.
			return task.spawn(thread, callback, thread, ...)
		end
	end,
	Defer = function(callback, ...)
		local length = #threads
		if length == 0 then
			local thread = coroutine.create(reusableThread)
			coroutine.resume(thread)
			return task.defer(thread, callback, thread, ...)
		else
			local thread = threads[length]
			threads[length] = nil -- Remove from free threads list.
			return task.defer(thread, callback, thread, ...)
		end
	end,
	Delay = function(delay, callback, ...)
		local length = #threads
		if length == 0 then
			local thread = coroutine.create(reusableThread)
			coroutine.resume(thread)
			return task.delay(delay, thread, callback, thread, ...)
		else
			local thread = threads[length]
			threads[length] = nil -- Remove from free threads list.
			return task.delay(delay, thread, callback, thread, ...)
		end
	end
}


--!optimize 2
--!native

--[[
	S8		Minimum: -128			Maximum: 127
	S16		Minimum: -32768			Maximum: 32767
	S24		Minimum: -8388608		Maximum: 8388607
	S32		Minimum: -2147483648	Maximum: 2147483647

	U8		Minimum: 0				Maximum: 255
	U16		Minimum: 0				Maximum: 65535
	U24		Minimum: 0				Maximum: 16777215
	U32		Minimum: 0				Maximum: 4294967295

	F16		±2048					[65520]
	F24		±262144					[4294959104]
	F32		±16777216				[170141183460469231731687303715884105728]
	F64		±9007199254740992		[huge]
]]

-- Types.
export type Cursor = {
	Buffer:					buffer,
	BufferLength:			number,
	BufferOffset:			number,
	Instances:				{Instance},
	InstancesOffset:		number,
}

-- Signal types from Signal+.
export type Connection = {
	Signal: Signal,
	Connected: boolean,
	Disconnect: typeof(
		-- Removes the connection from the signal.
		-- The connection’s data remains.
		function(connection: Connection) end
	)
}
export type Signal<Parameters...> = {
	Connect: typeof(
		-- Connects a function.
		function(signal: Signal<Parameters...>, callback: (Parameters...) -> ()): Connection end
	),
	Once: typeof(
		-- Connects a function, then auto-disconnects after the first call.
		function(signal: Signal<Parameters...>, callback: (Parameters...) -> ()): Connection end
	),
	Wait: typeof(
		-- Yields the calling thread until the next fire.
		function(signal: Signal<Parameters...>): Parameters... end
	),

	Fire: typeof(
		-- Runs all connected functions and resumes all waiting threads.
		function(signal: Signal<Parameters...>, ...: Parameters...) end
	),

	DisconnectAll: typeof(
		-- Erases all connections.
		-- Much faster than calling Disconnect on each.
		function(signal: Signal<Parameters...>) end
	),
	Destroy: typeof(
		-- Erases all connections and methods.
		-- To fully erase, also remove all references to the signal.
		function(signal: Signal<Parameters...>) end
	)
}

-- Varables
local activeCursor: Cursor
local activeBuffer: buffer
local bufferLength: number
local bufferOffset: number
local instances: {Instance}
local instancesOffset: number
local dataTypes = {}
local reads = {}
local writes = {}
local anyreads = {}	:: {[any]: () -> any}
local anywrites = {} :: {[any]: (any) -> ()}

-- Functions.
local function allocate(bytes: number)
	local targetLength = bufferOffset + bytes
	if bufferLength < targetLength then
		while true do bufferLength *= 2 if bufferLength >= targetLength then break end end
		local newBuffer = buffer.create(bufferLength)
		buffer.copy(newBuffer, 0, activeBuffer, 0, bufferOffset)
		activeCursor.Buffer = newBuffer
		activeBuffer = newBuffer
	end
end

local function readS8(): number local value = buffer.readi8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
local function writeS8(value: number) buffer.writei8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
local function readS16(): number local value = buffer.readi16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
local function writeS16(value: number) buffer.writei16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
local function readS24(): number local value = buffer.readbits(activeBuffer, bufferOffset*8, 24) - 8388608 bufferOffset += 3 return value end
local function writeS24(value: number) buffer.writebits(activeBuffer, bufferOffset*8, 24, value + 8388608) bufferOffset += 3 end
local function readS32(): number local value = buffer.readi32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
local function writeS32(value: number) buffer.writei32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
local function readU8(): number local value = buffer.readu8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
local function writeU8(value: number) buffer.writeu8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
local function readU16(): number local value = buffer.readu16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
local function writeU16(value: number) buffer.writeu16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
local function readU24(): number local value = buffer.readbits(activeBuffer, bufferOffset*8, 24) bufferOffset += 3 return value end
local function writeU24(value: number) buffer.writebits(activeBuffer, bufferOffset*8, 24, value) bufferOffset += 3 end
local function readU32(): number local value = buffer.readu32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
local function writeU32(value: number) buffer.writeu32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
local function readF32(): number local value = buffer.readf32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
local function writeF32(value: number) buffer.writef32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
local function readF64(): number local value = buffer.readf64(activeBuffer, bufferOffset) bufferOffset += 8 return value end
local function writeF64(value: number) buffer.writef64(activeBuffer, bufferOffset, value) bufferOffset += 8 end
local function readString(length: number) local value = buffer.readstring(activeBuffer, bufferOffset, length) bufferOffset += length return value end
local function writeString(value: string) buffer.writestring(activeBuffer, bufferOffset, value) bufferOffset += #value end
local function readBuffer(length: number) local value = buffer.create(length) buffer.copy(value, 0, activeBuffer, bufferOffset, length) bufferOffset += length return value end
local function writeBuffer(value: buffer) buffer.copy(activeBuffer, bufferOffset, value) bufferOffset += buffer.len(value) end
local function readInstance() instancesOffset += 1 return instances[instancesOffset] end
local function writeInstance(value) instancesOffset += 1 instances[instancesOffset] = value end

local function readF16(): number
	local bitOffset = bufferOffset*8
	bufferOffset += 2
	local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 10)
	local exponent = buffer.readbits(activeBuffer, bitOffset + 10, 5)
	local sign = buffer.readbits(activeBuffer, bitOffset + 15, 1)
	if mantissa == 0b0000000000 then
		if exponent == 0b00000 then return 0 end
		if exponent == 0b11111 then return if sign == 0 then math.huge else -math.huge end
	elseif exponent == 0b11111 then return 0/0 end
	if sign == 0 then
		return (mantissa/1024 + 1)*2^(exponent - 15)
	else
		return -(mantissa/1024 + 1)*2^(exponent - 15)
	end
end
local function writeF16(value: number)
	local bitOffset = bufferOffset*8
	bufferOffset += 2
	if value == 0 then
		buffer.writebits(activeBuffer, bitOffset, 16, 0b0_00000_0000000000)
	elseif value >= 65520 then
		buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000000)
	elseif value <= -65520 then
		buffer.writebits(activeBuffer, bitOffset, 16, 0b1_11111_0000000000)
	elseif value ~= value then
		buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000001)
	else
		local sign = 0
		if value < 0 then sign = 1 value = -value end
		local mantissa, exponent = math.frexp(value)
		buffer.writebits(activeBuffer, bitOffset + 0, 10, mantissa*2048 - 1023.5)
		buffer.writebits(activeBuffer, bitOffset + 10, 5, exponent + 14)
		buffer.writebits(activeBuffer, bitOffset + 15, 1, sign)
	end
end

local function readF24(): number
	local bitOffset = bufferOffset*8
	bufferOffset += 3
	local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 17)
	local exponent = buffer.readbits(activeBuffer, bitOffset + 17, 6)
	local sign = buffer.readbits(activeBuffer, bitOffset + 23, 1)
	if mantissa == 0b00000000000000000 then
		if exponent == 0b000000 then return 0 end
		if exponent == 0b111111 then return if sign == 0 then math.huge else -math.huge end
	elseif exponent == 0b111111 then return 0/0 end
	if sign == 0 then
		return (mantissa/131072 + 1)*2^(exponent - 31)
	else
		return -(mantissa/131072 + 1)*2^(exponent - 31)
	end
end
local function writeF24(value: number)
	local bitOffset = bufferOffset*8
	bufferOffset += 3
	if value == 0 then
		buffer.writebits(activeBuffer, bitOffset, 24, 0b0_000000_00000000000000000)
	elseif value >= 4294959104 then
		buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000000)
	elseif value <= -4294959104 then
		buffer.writebits(activeBuffer, bitOffset, 24, 0b1_111111_00000000000000000)
	elseif value ~= value then
		buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000001)
	else
		local sign = 0
		if value < 0 then sign = 1 value = -value end
		local mantissa, exponent = math.frexp(value)
		buffer.writebits(activeBuffer, bitOffset + 0, 17, mantissa*262144 - 131071.5)
		buffer.writebits(activeBuffer, bitOffset + 17, 6, exponent + 30)
		buffer.writebits(activeBuffer, bitOffset + 23, 1, sign)
	end
end

-- Read/writes.
dataTypes.Any = "Any" :: any
reads.Any = function() return anyreads[readU8()]() end
writes.Any = function(value: any)
	local write = anywrites[typeof(value)]
	if not write then error("Data type '"..typeof(value).."' not supported.") end
	write(value)
end

dataTypes.Nil = ("Nil" :: any) :: nil
reads.Nil = function() return nil end
writes.Nil = function(value: nil) end

dataTypes.NumberS8 = ("NumberS8" :: any) :: number
reads.NumberS8 = function() return readS8() end
writes.NumberS8 = function(value: number) allocate(1) writeS8(value) end

dataTypes.NumberS16 = ("NumberS16" :: any) :: number
reads.NumberS16 = function() return readS16() end
writes.NumberS16 = function(value: number) allocate(2) writeS16(value) end

dataTypes.NumberS24 = ("NumberS24" :: any) :: number
reads.NumberS24 = function() return readS24() end
writes.NumberS24 = function(value: number) allocate(3) writeS24(value) end

dataTypes.NumberS32 = ("NumberS32" :: any) :: number
reads.NumberS32 = function() return readS32() end
writes.NumberS32 = function(value: number) allocate(4) writeS32(value) end

dataTypes.NumberU8 = ("NumberU8" :: any) :: number
reads.NumberU8 = function() return readU8() end
writes.NumberU8 = function(value: number) allocate(1) writeU8(value) end

dataTypes.NumberU16 = ("NumberU16" :: any) :: number
reads.NumberU16 = function() return readU16() end
writes.NumberU16 = function(value: number) allocate(2) writeU16(value) end

dataTypes.NumberU24 = ("NumberU24" :: any) :: number
reads.NumberU24 = function() return readU24() end
writes.NumberU24 = function(value: number) allocate(3) writeU24(value) end

dataTypes.NumberU32 = ("NumberU32" :: any) :: number
reads.NumberU32 = function() return readU32() end
writes.NumberU32 = function(value: number) allocate(4) writeU32(value) end

dataTypes.NumberF16 = ("NumberF16" :: any) :: number
reads.NumberF16 = function() return readF16() end
writes.NumberF16 = function(value: number) allocate(2) writeF16(value) end

dataTypes.NumberF24 = ("NumberF24" :: any) :: number
reads.NumberF24 = function() return readF24() end
writes.NumberF24 = function(value: number) allocate(3) writeF24(value) end

dataTypes.NumberF32 = ("NumberF32" :: any) :: number
reads.NumberF32 = function() return readF32() end
writes.NumberF32 = function(value: number) allocate(4) writeF32(value) end

dataTypes.NumberF64 = ("NumberF64" :: any) :: number
reads.NumberF64 = function() return readF64() end
writes.NumberF64 = function(value: number) allocate(8) writeF64(value) end

dataTypes.String = ("String" :: any) :: string
reads.String = function() return readString(readU8()) end
writes.String = function(value: string) local length = #value allocate(1 + length) writeU8(length) writeString(value) end

dataTypes.StringLong = ("StringLong" :: any) :: string
reads.StringLong = function() return readString(readU16()) end
writes.StringLong = function(value: string) local length = #value allocate(2 + length) writeU16(length) writeString(value) end

dataTypes.Buffer = ("Buffer" :: any) :: buffer
reads.Buffer = function() return readBuffer(readU8()) end
writes.Buffer = function(value: buffer) local length = buffer.len(value) allocate(1 + length) writeU8(length) writeBuffer(value) end

dataTypes.BufferLong = ("BufferLong" :: any) :: buffer
reads.BufferLong = function() return readBuffer(readU16()) end
writes.BufferLong = function(value: buffer) local length = buffer.len(value) allocate(2 + length) writeU16(length) writeBuffer(value) end

dataTypes.Instance = ("Instance" :: any) :: Instance
reads.Instance = function() return readInstance() end
writes.Instance = function(value: Instance) writeInstance(value) end

dataTypes.Boolean8 = ("Boolean8" :: any) :: boolean
reads.Boolean8 = function() return readU8() == 1 end
writes.Boolean8 = function(value: boolean) allocate(1) writeU8(if value then 1 else 0) end

dataTypes.NumberRange = ("NumberRange" :: any) :: NumberRange
reads.NumberRange = function() return NumberRange.new(readF32(), readF32()) end
writes.NumberRange = function(value: NumberRange) allocate(8) writeF32(value.Min) writeF32(value.Max) end

dataTypes.BrickColor = ("BrickColor" :: any) :: BrickColor
reads.BrickColor = function() return BrickColor.new(readU16()) end
writes.BrickColor = function(value: BrickColor) allocate(2) writeU16(value.Number) end

dataTypes.Color3 = ("Color3" :: any) :: Color3
reads.Color3 = function() return Color3.fromRGB(readU8(), readU8(), readU8()) end
writes.Color3 = function(value: Color3) allocate(3) writeU8(value.R*255 + 0.5)  writeU8(value.G*255 + 0.5)  writeU8(value.B*255 + 0.5) end

dataTypes.UDim = ("UDim" :: any) :: UDim
reads.UDim = function() return UDim.new(readS16()/1000, readS16()) end
writes.UDim = function(value: UDim) allocate(4) writeS16(value.Scale*1000) writeS16(value.Offset) end

dataTypes.UDim2 = ("UDim2" :: any) :: UDim2
reads.UDim2 = function() return UDim2.new(readS16()/1000, readS16(), readS16()/1000, readS16()) end
writes.UDim2 = function(value: UDim2) allocate(8) writeS16(value.X.Scale*1000) writeS16(value.X.Offset) writeS16(value.Y.Scale*1000) writeS16(value.Y.Offset) end

dataTypes.Rect = ("Rect" :: any) :: Rect
reads.Rect = function() return Rect.new(readF32(), readF32(), readF32(), readF32()) end
writes.Rect = function(value: Rect) allocate(16) writeF32(value.Min.X) writeF32(value.Min.Y) writeF32(value.Max.X) writeF32(value.Max.Y) end

dataTypes.Vector2S16 = ("Vector2S16" :: any) :: Vector2
reads.Vector2S16 = function() return Vector2.new(readS16(), readS16()) end
writes.Vector2S16 = function(value: Vector2) allocate(4) writeS16(value.X) writeS16(value.Y) end

dataTypes.Vector2F24 = ("Vector2F24" :: any) :: Vector2
reads.Vector2F24 = function() return Vector2.new(readF24(), readF24()) end
writes.Vector2F24 = function(value: Vector2) allocate(6) writeF24(value.X) writeF24(value.Y) end

dataTypes.Vector2F32 = ("Vector2F32" :: any) :: Vector2
reads.Vector2F32 = function() return Vector2.new(readF32(), readF32()) end
writes.Vector2F32 = function(value: Vector2) allocate(8) writeF32(value.X) writeF32(value.Y) end

dataTypes.Vector3S16 = ("Vector3S16" :: any) :: Vector3
reads.Vector3S16 = function() return vector.create(readS16(), readS16(), readS16()) end
writes.Vector3S16 = function(value: Vector3) allocate(6) writeS16(value.X) writeS16(value.Y) writeS16(value.Z) end

dataTypes.Vector3F24 = ("Vector3F24" :: any) :: Vector3
reads.Vector3F24 = function() return vector.create(readF24(), readF24(), readF24()) end
writes.Vector3F24 = function(value: Vector3) allocate(9) writeF24(value.X) writeF24(value.Y) writeF24(value.Z) end

dataTypes.Vector3F32 = ("Vector3F32" :: any) :: Vector3
reads.Vector3F32 = function() return vector.create(readF32(), readF32(), readF32()) end
writes.Vector3F32 = function(value: Vector3) allocate(12) writeF32(value.X) writeF32(value.Y) writeF32(value.Z) end

dataTypes.NumberU4 = ("NumberU4" :: any) :: {number}
reads.NumberU4 = function()
	local bitOffset = bufferOffset*8
	bufferOffset += 1
	return {
		buffer.readbits(activeBuffer, bitOffset + 0, 4),
		buffer.readbits(activeBuffer, bitOffset + 4, 4)
	}
end
writes.NumberU4 = function(value: {number})
	allocate(1)
	local bitOffset = bufferOffset*8
	bufferOffset += 1
	buffer.writebits(activeBuffer, bitOffset + 0, 4, value[1])
	buffer.writebits(activeBuffer, bitOffset + 4, 4, value[2])
end

dataTypes.BooleanNumber = ("BooleanNumber" :: any) :: {Boolean: boolean, Number: number}
reads.BooleanNumber = function()
	local bitOffset = bufferOffset*8
	bufferOffset += 1
	return {
		Boolean = buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
		Number = buffer.readbits(activeBuffer, bitOffset + 1, 7),
	}
end
writes.BooleanNumber = function(value: {Boolean: boolean, Number: number})
	allocate(1)
	local bitOffset = bufferOffset*8
	bufferOffset += 1
	buffer.writebits(activeBuffer, bitOffset + 0, 1, if value.Boolean then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 1, 7, value.Number)
end

dataTypes.Boolean1 = ("Boolean1" :: any) :: {boolean}
reads.Boolean1 = function()
	local bitOffset = bufferOffset*8
	bufferOffset += 1
	return {
		buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 1, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 2, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 3, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 4, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 5, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 6, 1) == 1,
		buffer.readbits(activeBuffer, bitOffset + 7, 1) == 1,
	}
end
writes.Boolean1 = function(value: {boolean})
	allocate(1)
	local bitOffset = bufferOffset*8
	bufferOffset += 1
	buffer.writebits(activeBuffer, bitOffset + 0, 1, if value[1] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 1, 1, if value[2] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 2, 1, if value[3] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 3, 1, if value[4] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 4, 1, if value[5] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 5, 1, if value[6] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 6, 1, if value[7] then 1 else 0)
	buffer.writebits(activeBuffer, bitOffset + 7, 1, if value[8] then 1 else 0)
end

dataTypes.CFrameF24U8 = ("CFrameF24U8" :: any) :: CFrame
reads.CFrameF24U8 = function()
	return CFrame.Angles(readU8()/40.58451048843331, readU8()/40.58451048843331, readU8()/40.58451048843331)
		+ vector.create(readF24(), readF24(), readF24())
end
writes.CFrameF24U8 = function(value: CFrame)
	local rx, ry, rz = value:ToEulerAnglesXYZ()
	allocate(12)
	writeU8(rx*40.58451048843331 + 0.5) writeU8(ry*40.58451048843331 + 0.5) writeU8(rz*40.58451048843331 + 0.5)
	writeF24(value.X) writeF24(value.Y) writeF24(value.Z)
end

dataTypes.CFrameF32U8 = ("CFrameF32U8" :: any) :: CFrame
reads.CFrameF32U8 = function()
	return CFrame.fromEulerAnglesXYZ(readU8()/40.58451048843331, readU8()/40.58451048843331, readU8()/40.58451048843331)
		+ vector.create(readF32(), readF32(), readF32())
end
writes.CFrameF32U8 = function(value: CFrame)
	local rx, ry, rz = value:ToEulerAnglesXYZ()
	allocate(15)
	writeU8(rx*40.58451048843331 + 0.5) writeU8(ry*40.58451048843331 + 0.5) writeU8(rz*40.58451048843331 + 0.5)
	writeF32(value.X) writeF32(value.Y) writeF32(value.Z)
end

dataTypes.CFrameF32U16 = ("CFrameF32U16" :: any) :: CFrame
reads.CFrameF32U16 = function()
	return CFrame.fromEulerAnglesXYZ(readU16()/10430.219195527361, readU16()/10430.219195527361, readU16()/10430.219195527361)
		+ vector.create(readF32(), readF32(), readF32())
end
writes.CFrameF32U16 = function(value: CFrame)
	local rx, ry, rz = value:ToEulerAnglesXYZ()
	allocate(18)
	writeU16(rx*10430.219195527361 + 0.5) writeU16(ry*10430.219195527361 + 0.5) writeU16(rz*10430.219195527361 + 0.5)
	writeF32(value.X) writeF32(value.Y) writeF32(value.Z)
end

dataTypes.Region3 = ("Region3" :: any) :: Region3
reads.Region3 = function()
	return Region3.new(
		vector.create(readF32(), readF32(), readF32()),
		vector.create(readF32(), readF32(), readF32())
	)
end
writes.Region3 = function(value: Region3)
	local halfSize = value.Size/2
	local position = value.CFrame.Position
	local minimum = position - halfSize
	local maximum = position + halfSize
	allocate(24)
	writeF32(minimum.X) writeF32(minimum.Y) writeF32(minimum.Z)
	writeF32(maximum.X) writeF32(maximum.Y) writeF32(maximum.Z)
end

dataTypes.NumberSequence = ("NumberSequence" :: any) :: NumberSequence
reads.NumberSequence = function()
	local length = readU8()
	local keypoints = table.create(length)
	for index = 1, length, 1 do
		keypoints[index] = NumberSequenceKeypoint.new(readU8()/255, readU8()/255, readU8()/255)
	end
	return NumberSequence.new(keypoints)
end
writes.NumberSequence = function(value: NumberSequence)
	local length = #value.Keypoints
	allocate(1 + length*3)
	writeU8(length)
	for index, keypoint in value.Keypoints do
		writeU8(keypoint.Time*255 + 0.5) writeU8(keypoint.Value*255 + 0.5) writeU8(keypoint.Envelope*255 + 0.5)
	end
end

dataTypes.ColorSequence = ("ColorSequence" :: any) :: ColorSequence
reads.ColorSequence = function()
	local length = readU8()
	local keypoints = table.create(length)
	for index = 1, length, 1 do
		keypoints[index] = ColorSequenceKeypoint.new(readU8()/255, Color3.fromRGB(readU8(), readU8(), readU8()))
	end
	return ColorSequence.new(keypoints)
end
writes.ColorSequence = function(value: ColorSequence)
	local length = #value.Keypoints
	allocate(1 + length*4)
	writeU8(length)
	for index, keypoint in value.Keypoints do
		writeU8(keypoint.Time*255 + 0.5)
		writeU8(keypoint.Value.R*255 + 0.5) writeU8(keypoint.Value.G*255 + 0.5) writeU8(keypoint.Value.B*255 + 0.5)
	end
end

local characterIndices = {}
local characters = require(script.Characters)
for index, value in characters do characterIndices[value] = index end
local characterBits = math.ceil(math.log(#characters + 1, 2))
local characterBytes = characterBits/8
dataTypes.Characters = ("Characters" :: any) :: string
reads.Characters = function()
	local length = readU8()
	local characterArray = table.create(length)
	local bitOffset = bufferOffset*8
	bufferOffset += math.ceil(length*characterBytes)
	for index = 1, length do
		table.insert(characterArray, characters[buffer.readbits(activeBuffer, bitOffset, characterBits)])
		bitOffset += characterBits
	end
	return table.concat(characterArray)
end
writes.Characters = function(value: string)
	local length = #value
	local bytes = math.ceil(length*characterBytes)
	allocate(1 + bytes)
	writeU8(length)
	local bitOffset = bufferOffset*8
	for index = 1, length do
		buffer.writebits(activeBuffer, bitOffset, characterBits, characterIndices[value:sub(index, index)])
		bitOffset += characterBits
	end
	bufferOffset += bytes
end

local enumIndices = {}
local enums = require(script.Enums)
for index, static in enums do enumIndices[static] = index end
dataTypes.EnumItem = ("EnumItem" :: any) :: EnumItem
reads.EnumItem = function() return enums[readU8()]:FromValue(readU16()) end
writes.EnumItem = function(value: EnumItem) allocate(3) writeU8(enumIndices[value.EnumType]) writeU16(value.Value) end

local staticIndices = {}
local statics = require(script.Static1)
for index, static in statics do staticIndices[static] = index end
dataTypes.Static1 = ("Static1" :: any) :: any
reads.Static1 = function() return statics[readU8()] end
writes.Static1 = function(value: any) allocate(1) writeU8(staticIndices[value] or 0) end

local staticIndices = {}
local statics = require(script.Static2)
for index, static in statics do staticIndices[static] = index end
dataTypes.Static2 = ("Static2" :: any) :: any
reads.Static2 = function() return statics[readU8()] end
writes.Static2 = function(value: any) allocate(1) writeU8(staticIndices[value] or 0) end

local staticIndices = {}
local statics = require(script.Static3)
for index, static in statics do staticIndices[static] = index end
dataTypes.Static3 = ("Static3" :: any) :: any
reads.Static3 = function() return statics[readU8()] end
writes.Static3 = function(value: any) allocate(1) writeU8(staticIndices[value] or 0) end

-- Any Types
anyreads[0] = function() return nil end
anywrites["nil"] = function(value: nil) allocate(1) writeU8(0) end

anyreads[1] = function() return -readU8() end
anyreads[2] = function() return -readU16() end
anyreads[3] = function() return -readU24() end
anyreads[4] = function() return -readU32() end
anyreads[5] = function() return readU8() end
anyreads[6] = function() return readU16() end
anyreads[7] = function() return readU24() end
anyreads[8] = function() return readU32() end
anyreads[9] = function() return readF32() end
anyreads[10] = function() return readF64() end
anywrites.number = function(value: number)
	if value%1 == 0 then
		if value < 0 then
			if value > -256 then
				allocate(2) writeU8(1) writeU8(-value)
			elseif value > -65536 then
				allocate(3) writeU8(2) writeU16(-value)
			elseif value > -16777216 then
				allocate(4) writeU8(3) writeU24(-value)
			elseif value > -4294967296 then
				allocate(5) writeU8(4) writeU32(-value)
			else
				allocate(9) writeU8(10) writeF64(value)
			end
		else
			if value < 256 then
				allocate(2) writeU8(5) writeU8(value)
			elseif value < 65536 then
				allocate(3) writeU8(6) writeU16(value)
			elseif value < 16777216 then
				allocate(4) writeU8(7) writeU24(value)
			elseif value < 4294967296 then
				allocate(5) writeU8(8) writeU32(value)
			else
				allocate(9) writeU8(10) writeF64(value)
			end
		end
	elseif value > -1048576 and value < 1048576 then
		allocate(5) writeU8(9) writeF32(value)
	else
		allocate(9) writeU8(10) writeF64(value)
	end
end

anyreads[11] = function() return readString(readU8()) end
anywrites.string = function(value: string) local length = #value allocate(2 + length) writeU8(11) writeU8(length) writeString(value) end

anyreads[12] = function() return readBuffer(readU8()) end
anywrites.buffer = function(value: buffer) local length = buffer.len(value) allocate(2 + length) writeU8(12) writeU8(length) writeBuffer(value) end

anyreads[13] = function() return readInstance() end
anywrites.Instance = function(value: Instance) allocate(1) writeU8(13) writeInstance(value) end

anyreads[14] = function() return readU8() == 1 end
anywrites.boolean = function(value: boolean) allocate(2) writeU8(14) writeU8(if value then 1 else 0) end

anyreads[15] = function() return NumberRange.new(readF32(), readF32()) end
anywrites.NumberRange = function(value: NumberRange) allocate(9) writeU8(15) writeF32(value.Min) writeF32(value.Max) end

anyreads[16] = function() return BrickColor.new(readU16()) end
anywrites.BrickColor = function(value: BrickColor) allocate(3) writeU8(16) writeU16(value.Number) end

anyreads[17] = function() return Color3.fromRGB(readU8(), readU8(), readU8()) end
anywrites.Color3 = function(value: Color3) allocate(4) writeU8(17) writeU8(value.R*255 + 0.5)  writeU8(value.G*255 + 0.5)  writeU8(value.B*255 + 0.5) end

anyreads[18] = function() return UDim.new(readS16()/1000, readS16()) end
anywrites.UDim = function(value: UDim) allocate(5) writeU8(18) writeS16(value.Scale*1000) writeS16(value.Offset) end

anyreads[19] = function() return UDim2.new(readS16()/1000, readS16(), readS16()/1000, readS16()) end
anywrites.UDim2 = function(value: UDim2) allocate(9) writeU8(19) writeS16(value.X.Scale*1000) writeS16(value.X.Offset) writeS16(value.Y.Scale*1000) writeS16(value.Y.Offset) end

anyreads[20] = function() return Rect.new(readF32(), readF32(), readF32(), readF32()) end
anywrites.Rect = function(value: Rect) allocate(17) writeU8(20) writeF32(value.Min.X) writeF32(value.Min.Y) writeF32(value.Max.X) writeF32(value.Max.Y) end

anyreads[21] = function() return Vector2.new(readF32(), readF32()) end
anywrites.Vector2 = function(value: Vector2) allocate(9) writeU8(21) writeF32(value.X) writeF32(value.Y) end

anyreads[22] = function() return vector.create(readF32(), readF32(), readF32()) end
anywrites.Vector3 = function(value: Vector3) allocate(13) writeU8(22) writeF32(value.X) writeF32(value.Y) writeF32(value.Z) end

anyreads[23] = function()
	return CFrame.Angles(readU16()/10430.219195527361, readU16()/10430.219195527361, readU16()/10430.219195527361)
		+ vector.create(readF32(), readF32(), readF32())
end
anywrites.CFrame = function(value: CFrame)
	local rx, ry, rz = value:ToOrientation()
	allocate(19)
	writeU8(23)
	writeU16(rx*10430.219195527361 + 0.5) writeU16(ry*10430.219195527361 + 0.5) writeU16(rz*10430.219195527361 + 0.5)
	writeF32(value.X) writeF32(value.Y) writeF32(value.Z)
end

anyreads[24] = function()
	return Region3.new(
		vector.create(readF32(), readF32(), readF32()),
		vector.create(readF32(), readF32(), readF32())
	)
end
anywrites.Region3 = function(value: Region3)
	local halfSize = value.Size/2
	local position = value.CFrame.Position
	local minimum = position - halfSize
	local maximum = position + halfSize
	allocate(25)
	writeU8(24)
	writeF32(minimum.X) writeF32(minimum.Y) writeF32(minimum.Z)
	writeF32(maximum.X) writeF32(maximum.Y) writeF32(maximum.Z)
end

anyreads[25] = function()
	local length = readU8()
	local keypoints = table.create(length)
	for index = 1, length, 1 do
		keypoints[index] = NumberSequenceKeypoint.new(readU8()/255, readU8()/255, readU8()/255)
	end
	return NumberSequence.new(keypoints)
end
anywrites.NumberSequence = function(value: NumberSequence)
	local length = #value.Keypoints
	allocate(2 + length*3)
	writeU8(25)
	writeU8(length)
	for index, keypoint in value.Keypoints do
		writeU8(keypoint.Time*255 + 0.5) writeU8(keypoint.Value*255 + 0.5) writeU8(keypoint.Envelope*255 + 0.5)
	end
end

anyreads[26] = function()
	local length = readU8()
	local keypoints = table.create(length)
	for index = 1, length, 1 do
		keypoints[index] = ColorSequenceKeypoint.new(readU8()/255, Color3.fromRGB(readU8(), readU8(), readU8()))
	end
	return ColorSequence.new(keypoints)
end
anywrites.ColorSequence = function(value: ColorSequence)
	local length = #value.Keypoints
	allocate(2 + length*4)
	writeU8(26)
	writeU8(length)
	for index, keypoint in value.Keypoints do
		writeU8(keypoint.Time*255 + 0.5)
		writeU8(keypoint.Value.R*255 + 0.5) writeU8(keypoint.Value.G*255 + 0.5) writeU8(keypoint.Value.B*255 + 0.5)
	end
end

anyreads[27] = function()
	return enums[readU8()]:FromValue(readU16())
end
anywrites.EnumItem = function(value: EnumItem)
	allocate(4)
	writeU8(27)
	writeU8(enumIndices[value.EnumType])
	writeU16(value.Value)
end

anyreads[28] = function()
	local value = {}
	while true do
		local typeId = readU8()
		if typeId == 0 then return value else value[anyreads[typeId]()] = anyreads[readU8()]() end
	end
end
anywrites.table = function(value: {[any]: any})
	allocate(1)
	writeU8(28)
	for index, value in value do anywrites[typeof(index)](index) anywrites[typeof(value)](value) end
	allocate(1)
	writeU8(0)
end

return {
	Import = function(cursor: Cursor)
		activeCursor = cursor
		activeBuffer = cursor.Buffer
		bufferLength = cursor.BufferLength
		bufferOffset = cursor.BufferOffset
		instances = cursor.Instances
		instancesOffset = cursor.InstancesOffset
	end,

	Export = function()
		activeCursor.BufferLength = bufferLength
		activeCursor.BufferOffset = bufferOffset
		activeCursor.InstancesOffset = instancesOffset
		return activeCursor
	end,

	Truncate = function()
		local truncatedBuffer = buffer.create(bufferOffset)
		buffer.copy(truncatedBuffer, 0, activeBuffer, 0, bufferOffset)
		if instancesOffset == 0 then return truncatedBuffer else return truncatedBuffer, instances end
	end,

	Ended = function()
		return bufferOffset >= bufferLength
	end,

	DataTypes = dataTypes,
	Reads = reads,
	Writes = writes,
}


--!strict
--v1.2.0 • LDGerrits
--[=[
	@class Bootstrapper
	A lightweight, agnostic module loader and scheduler for Roblox.
	Eliminate load-order bugs and race conditions by organizing your modules into a deterministic pipeline.
	Define exactly what runs, and in what order, every single frame with zero ambiguity.
]=]

local RunService = game:GetService('RunService')

local batchCounter = 0
local moduleRegistry = setmetatable({}, { __mode = 'k' })

export type LoadedModule = { [string]: any }
export type LoadedModules = { LoadedModule }
export type ModulePath = string
export type ModulePaths = { ModulePath }
export type ModuleScripts = { ModuleScript }
export type Errors = { [string]: string }
export type PredicateFn = (module: ModuleScript) -> boolean
export type Signal =
	{ Connect: (any, (...any) -> ...any) -> any }
	| { connect: (any, (...any) -> ...any) -> any }
	| RBXScriptSignal
export type Event = Signal | ((callback: (...any) -> ()) -> Task)
export type Cleanup = () -> ()

export type Task =
	Instance
	| RBXScriptConnection
	| () -> ()
	| { destroy: (unknown) -> () }
	| { Destroy: (unknown) -> () }
	| { disconnect: (unknown) -> () }
	| { Disconnect: (unknown) -> () }
	| { Task }

local function doCleanup(task: Task): ()
	if typeof(task) == 'Instance' then
		task:Destroy()
	elseif typeof(task) == 'RBXScriptConnection' then
		task:Disconnect()
	elseif typeof(task) == 'function' then
		task()
	elseif typeof(task) == 'table' then
		local taskTable = (task :: any) :: {
			Destroy: (...unknown) -> ...unknown?,
			destroy: (...unknown) -> ...unknown?,
			Disconnect: (...unknown) -> ...unknown?,
			disconnect: (...unknown) -> ...unknown?,
		}
		if typeof(taskTable.destroy) == 'function' then
			local t = (taskTable :: any) :: { destroy: (...unknown) -> ...unknown }
			t:destroy()
		elseif typeof(taskTable.Destroy) == 'function' then
			local t = (taskTable :: any) :: { Destroy: (...unknown) -> ...unknown }
			t:Destroy()
		elseif typeof(taskTable.Disconnect) == 'function' then
			local t = (taskTable :: any) :: { Disconnect: (...unknown) -> ...unknown }
			t:Disconnect()
		elseif typeof(taskTable.disconnect) == 'function' then
			local t = (taskTable :: any) :: { disconnect: (...unknown) -> ...unknown }
			t:disconnect()
		end
	end
end

local function traceErrorHandler(e)
	return { msg = tostring(e), trace = debug.traceback('', 2) }
end

local function nonFatalError(messageTemplate: string, trace: string?, ...: any)
	local messageText = string.format(messageTemplate, ...)
	messageText = `[Bootstrapper] {messageText}`

	if trace and trace ~= '' then
		messageText ..= `\n---- Stack trace ----\n{trace}`
	end

	local formattedString = messageText:gsub('\n', '\n    ')

	task.spawn(error, formattedString, 0)
end

local function loadModule(value: any, errors: Errors?): (LoadedModule?, string?)
	if type(value) == 'table' then
		return value, moduleRegistry[value] or 'UnknownModule'
	end

	local identity: string
	local errorKey: string
	local sourceLabel: string

	if typeof(value) == 'Instance' and value:IsA('ModuleScript') then
		identity = value.Name
		errorKey = identity
		sourceLabel = 'ModuleScript'
	elseif type(value) == 'string' then
		identity = value:match('([^/]+)$') or value
		errorKey = value
		sourceLabel = 'string path'
	else
		return nil, nil
	end

	debug.setmemorycategory(identity)
	local success, result = pcall(require, value)
	debug.resetmemorycategory()

	if not success then
		local errMsg = tostring(result)
		if errors then
			errors[errorKey] = errMsg
		end
		nonFatalError("Failed to require %s '%s':\n%s", nil, sourceLabel, errorKey, errMsg)
		return nil, errorKey
	end

	if type(result) ~= 'table' then
		local errMsg = `Module must return a table; got {type(result)}`
		if errors then
			errors[errorKey] = errMsg
		end
		nonFatalError("Invalid %s '%s':\n%s", nil, sourceLabel, errorKey, errMsg)
		return nil, errorKey
	end

	moduleRegistry[result] = identity
	return result, identity
end

local function loadModules(
	modules: { Instance | ModulePath },
	shouldSort: boolean,
	predicate: PredicateFn?
): (LoadedModules, Errors?)
	local loadQueue = {}

	for _, item in modules do
		local isModuleScript = typeof(item) == 'Instance' and item:IsA('ModuleScript')
		local isModulePath = type(item) == 'string'

		if not (isModuleScript or isModulePath) then
			continue
		end

		if isModuleScript and predicate and not predicate(item :: ModuleScript) then
			continue
		end

		local identity = if isModuleScript
			then (item :: ModuleScript).Name
			else ((item :: ModulePath):match('([^/]+)$') or item)

		table.insert(loadQueue, {
			identity = identity,
			value = item,
			fullName = if isModuleScript then (item :: ModuleScript):GetFullName() else item,
		})
	end

	if shouldSort then
		table.sort(loadQueue, function(a, b)
			if a.identity == b.identity then
				return a.fullName < b.fullName
			end
			return a.identity < b.identity
		end)
	end

	local loadedModules: LoadedModules = {}
	local errors: Errors = {}
	for _, entry in loadQueue do
		local result, _ = loadModule(entry.value, errors)
		if result then
			table.insert(loadedModules, result)
		end
	end

	return loadedModules, next(errors) ~= nil and errors or nil
end

local function filterModules(modules: LoadedModules | ModuleScripts | ModulePaths, methodName: string)
	local loadedModules, callbacks, identities, count = {}, {}, {}, 0
	for _, item in modules :: any do
		local module, identity = loadModule(item)
		if not module then
			continue
		end

		local callback = module[methodName]
		if type(callback) == 'function' then
			count += 1
			loadedModules[count] = module
			callbacks[count] = callback
			identities[count] = identity :: string
		end
	end
	return loadedModules, callbacks, identities, count
end

local function parseMethodName(methodName: string): (string, boolean)
	local length = #methodName
	if length <= 1 then
		return methodName, true
	end

	local prefix = string.sub(methodName, 1, 1)
	if prefix == '.' then
		return string.sub(methodName, 2), false
	elseif prefix == ':' then
		return string.sub(methodName, 2), true
	end

	return methodName, true
end

local function dispatch(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	args: { [number]: any, n: number },
	mode: 'Sync' | 'Async' | 'Concurrent'
): (LoadedModules, Errors?)
	local parsedMethodName, includeSelf = parseMethodName(methodName)
	local loadedModules: LoadedModules = {}
	local errors: Errors = {}
	local errorSuffix = if mode == 'Sync' then '' else `({mode})`

	local function execute(item)
		local module, identity = loadModule(item, errors)
		if not module then
			return
		end

		local fn = module[parsedMethodName]
		if type(fn) ~= 'function' then
			return
		end

		debug.setmemorycategory(identity :: string)
		local success, err
		if includeSelf then
			success, err = xpcall(fn, traceErrorHandler, module, table.unpack(args, 1, args.n))
		else
			success, err = xpcall(fn, traceErrorHandler, table.unpack(args, 1, args.n))
		end
		debug.resetmemorycategory()

		if success then
			table.insert(loadedModules, module)
		else
			errors[identity :: string] = err.msg
			local suffixStr = errorSuffix ~= '' and ` {errorSuffix}` or ''
			nonFatalError('Error in %s:%s()%s\n%s', err.trace, identity :: string, parsedMethodName, suffixStr, err.msg)
		end
	end

	if mode == 'Sync' then
		for _, item in modules :: any do
			execute(item)
		end
	elseif mode == 'Async' then
		task.spawn(function()
			for _, item in modules :: any do
				execute(item)
			end
		end)
	else
		for _, item in modules :: any do
			task.spawn(execute, item)
		end
	end

	return loadedModules, next(errors) ~= nil and errors or nil
end

local function createRunner(
	loadedModules: LoadedModules,
	fns: { any },
	identities: { string },
	count: number,
	includeSelf: boolean
)
	return function(...)
		for i = 1, count do
			debug.setmemorycategory(identities[i])
			local success, err
			if not includeSelf then
				success, err = xpcall(fns[i], traceErrorHandler, ...)
			else
				success, err = xpcall(fns[i], traceErrorHandler, loadedModules[i], ...)
			end
			if not success then
				nonFatalError("Error in module '%s':\n%s", err.trace, identities[i], err.msg)
			end
		end
		debug.resetmemorycategory()
	end
end

local function bindToRunService(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	eventName: string,
	methodName: string,
	override: (number | EnumItem)?
): Cleanup
	local parsedMethodName, includeSelf = parseMethodName(methodName)
	local loadedModules, fns, identities, count = filterModules(modules, parsedMethodName)

	if count == 0 then
		return function() end
	end

	local run = createRunner(loadedModules, fns, identities, count, includeSelf)
	local task: Task
	if eventName == 'RenderStepped' then
		if type(override) == 'number' then
			batchCounter += 1
			local bindName = `Bootstrapper_RenderBatch_{batchCounter}_{parsedMethodName}`
			RunService:BindToRenderStep(bindName, override, run)
			task = function()
				RunService:UnbindFromRenderStep(bindName)
			end
		else
			task = RunService.RenderStepped:Connect(run)
		end
	elseif eventName == 'PreSimulation' then
		if typeof(override) == 'EnumItem' and (override :: EnumItem).EnumType == Enum.StepFrequency then
			task = RunService:BindToSimulation(run, override :: Enum.StepFrequency)
		else
			task = RunService.PreSimulation:Connect(run)
		end
	else
		local event = (RunService :: any)[eventName]
		if event then
			task = event:Connect(run)
		end
	end

	return function()
		doCleanup(task)
	end
end

local Bootstrapper = {}

--[=[
	Creates a predicate function that matches a module's name against a string.
	@function byName
	@within Bootstrapper
	@param matchName string -- The string pattern to match against the module's name.
	@return PredicateFn
]=]
function Bootstrapper.byName(matchName: string): PredicateFn
	return function(moduleScript: ModuleScript)
		return moduleScript.Name:match(matchName) ~= nil
	end
end

--[=[
	Requires all direct child ModuleScripts within a given parent.
	Returns an array of loaded modules to guarantee order execution.
	@yields
	@function loadChildren
	@within Bootstrapper
	@param parent Instance -- The Instance to search within.
	@param predicate: PredicateFn? -- Optional filter to include specific modules (e.g., Bootstrapper.byName("Service$")).
	@return LoadedModules, Errors?
]=]
function Bootstrapper.loadChildren(parent: Instance, predicate: PredicateFn?): (LoadedModules, Errors?)
	return loadModules(parent:GetChildren(), true, predicate)
end

--[=[
	Requires all descendant ModuleScripts within a given parent.
	Returns an array of loaded modules to guarantee order execution.
	@yields
	@function loadDescendants
	@within Bootstrapper
	@param parent Instance -- The Instance to search within.
	@param predicate: PredicateFn? -- Optional filter to include specific modules (e.g., Bootstrapper.byName("Service$")).
	@return LoadedModules, Errors?
]=]
function Bootstrapper.loadDescendants(parent: Instance, predicate: PredicateFn?): (LoadedModules, Errors?)
	return loadModules(parent:GetDescendants(), true, predicate)
end

--[=[
	Requires specific modules in a guaranteed sequence array.
	@yields
	@function loadSequence
	@within Bootstrapper
	@param modules ModuleScripts | ModulePaths -- The array of ModuleScripts or ModulePaths.
	@return LoadedModules, Errors?
]=]
function Bootstrapper.loadSequence(modules: ModuleScripts | ModulePaths): (LoadedModules, Errors?)
	return loadModules(modules, false)
end

--[=[
	Calls methods one-by-one and yields until complete.
	Guarantees that Module A is finished before Module B starts.
	@yields
	@function run
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param ... any -- Additional arguments to pass to the method.
	@return LoadedModules, Errors?
]=]
function Bootstrapper.run(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	...: any
): (LoadedModules, Errors?)
	return dispatch(modules, methodName, table.pack(...), 'Sync')
end

--[=[
	Calls methods one-by-one in a new thread.
	Maintains order but does not yield.
	@function runAsync
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param ... any -- Additional arguments to pass to the method.
]=]
function Bootstrapper.runAsync(modules: LoadedModules | ModuleScripts | ModulePaths, methodName: string, ...: any): ()
	dispatch(modules, methodName, table.pack(...), 'Async')
end

--[=[
	Calls all methods simultaneously using multiple threads.
	Fastest execution, but order of completion is not guaranteed.
	@function runConcurrent
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param ... any -- Additional arguments to pass to the method.
]=]
function Bootstrapper.runConcurrent(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	...: any
): ()
	dispatch(modules, methodName, table.pack(...), 'Concurrent')
end

--[=[
	Binds a method across all modules to an RBXScriptSignal, a custom Signal, or a subscription function.
	@function bindTo
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param event Event -- An RBXScriptSignal, custom Signal or a subscription function.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindTo(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	event: Event
): Cleanup
	local parsedMethodName, includeSelf = parseMethodName(methodName)
	local loadedModules, fns, identities, count = filterModules(modules, parsedMethodName)

	if count == 0 then
		return function() end
	end

	local run = createRunner(loadedModules, fns, identities, count, includeSelf)
	local task: Task
	if type(event) == 'function' then
		task = event(run)
	else
		local connectMethod = (event :: any).Connect or (event :: any).connect
		task = connectMethod(event, run)
	end

	return function()
		doCleanup(task)
	end
end

--[=[
	Binds a method to run at a specific time interval (in seconds) while not allowing drift.
	@function bindToInterval
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param interval number -- Time in seconds between calls.
	@param signal RBXScriptSignal? -- Optional signal to hook (defaults to Heartbeat).
	@return Cleanup -- A function to stop the interval.
]=]
function Bootstrapper.bindToInterval(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	interval: number,
	signal: RBXScriptSignal?
): Cleanup
	assert(interval > 0, `[Bootstrapper] interval must be greater than 0; got {interval}`)
	assert(
		signal ~= RunService.Stepped,
		'[Bootstrapper] bindToInterval cannot be used with Stepped. Use PreSimulation instead.'
	)

	local parsedMethodName, includeSelf = parseMethodName(methodName)
	local loadedModules, fns, identities, count = filterModules(modules, parsedMethodName)

	if count == 0 then
		return function() end
	end

	local run = createRunner(loadedModules, fns, identities, count, includeSelf)
	local startTime = os.clock()
	local nextTick = startTime + interval
	local lastFireTime = startTime
	local connection = ((signal or RunService.Heartbeat) :: RBXScriptSignal):Connect(function()
		local now = os.clock()
		if now >= nextTick then
			local dt = now - lastFireTime
			lastFireTime = now
			local intervalsPassed = math.floor((now - startTime) / interval)
			nextTick = startTime + (interval * (intervalsPassed + 1))
			run(dt)
		end
	end)

	return function()
		doCleanup(connection)
	end
end

--[=[
	Hooks a specified method in all modules to `RunService.Heartbeat`.
	@function bindToHeartbeat
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindToHeartbeat(modules: LoadedModules | ModuleScripts | ModulePaths, methodName: string): Cleanup
	return bindToRunService(modules, 'Heartbeat', methodName)
end

--[=[
	Hooks a specified method in all modules to `RunService.RenderStepped`
	or `RunService:BindToRenderStep` if a priority is given.
	@client
	@function bindToRenderStep
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param priority number? -- Optional execution priority. If provided, uses BindToRenderStep instead of RenderStepped.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindToRenderStep(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	priority: number?
): Cleanup
	assert(RunService:IsClient(), '[Bootstrapper] bindToRenderStep can only be called on the Client.')
	return bindToRunService(modules, 'RenderStepped', methodName, priority)
end

--[=[
	Hooks a specified method in all modules to `RunService.PreSimulation`
	or `RunService:BindToSimulation` if a frequency is given.
	@function bindToPreSimulation
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@param frequency Enum.StepFrequency? -- Optional execution frequency. If provided, uses BindToSimulation instead of PreSimulation.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindToPreSimulation(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string,
	frequency: Enum.StepFrequency?
): Cleanup
	return bindToRunService(modules, 'PreSimulation', methodName, frequency)
end

--[=[
	Hooks a specified method in all modules to `RunService.PostSimulation`.
	@function bindToPostSimulation
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindToPostSimulation(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string
): Cleanup
	return bindToRunService(modules, 'PostSimulation', methodName)
end

--[=[
	Hooks a specified method in all modules to `RunService.PreAnimation`.
	@function bindToPreAnimation
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindToPreAnimation(
	modules: LoadedModules | ModuleScripts | ModulePaths,
	methodName: string
): Cleanup
	return bindToRunService(modules, 'PreAnimation', methodName)
end

--[=[
	Hooks a specified method in all modules to `RunService.PreRender`.
	@client
	@function bindToPreRender
	@within Bootstrapper
	@param modules LoadedModules | ModuleScripts | ModulePaths -- The array of loaded modules, ModuleScripts, or ModulePaths.
	@param methodName string -- The method to call. Prefix with '.' for standard calls; otherwise, the module is implicitly passed as 'self'.
	@return Cleanup -- A cleanup function to disconnect the event.
]=]
function Bootstrapper.bindToPreRender(modules: LoadedModules | ModuleScripts | ModulePaths, methodName: string): Cleanup
	assert(RunService:IsClient(), '[Bootstrapper] bindToPreRender can only be called on the Client.')
	return bindToRunService(modules, 'PreRender', methodName)
end

return Bootstrapper