--// NOTE: THIS IS NOT A *CONFIG/USER* PLUGIN! ANYTHING IN THE MAINMODULE PLUGIN FOLDERS IS ALREADY PART OF/LOADED BY THE SCRIPT! DO NOT ADD THEM TO YOUR CONFIG>PLUGINS FOLDER!

return function(Vargs, GetEnv)
	local server = Vargs.Server;
	local service = Vargs.Service;

	local Settings = server.Settings
	local Functions, Commands, Admin, Anti, Core, HTTP, Logs, Remote, Process, Variables, Deps =
		server.Functions, server.Commands, server.Admin, server.Anti, server.Core, server.HTTP, server.Logs, server.Remote, server.Process, server.Variables, server.Deps

	--// AutoClean
	if Settings.AutoClean then
		service.StartLoop("AUTO_CLEAN", Settings.AutoCleanDelay, Functions.CleanWorkspace, true)
		Logs:AddLog("Script", `Started autoclean with {Settings.AutoCleanDelay}s delay`)
	end

	-- // Add legacy donors
	Variables.OldDonorList = select(2, xpcall(require, warn, server.Deps.LegacyDonors)) or {}
	for _, v in Variables.OldDonorList do
		if v.Id then
			Variables.CachedDonors[tostring(v.Id)] = os.time()
		end
	end

	-- // Backwards compatibility
	local Pcall = server.Pcall
	local function cPcall(func, ...)
		return Pcall(function(...)
			return coroutine.resume(coroutine.create(func), ...)
		end, ...)
	end
	server.cPcall, service.cPcall = cPcall, cPcall
	service.AltUnpack = function(args, shift) -- TODO: Remove. This is not used in Adonis at all and is easily replicatable so you can safely remove it withour fear
		return table.unpack(args, 1 + (shift and shift - 1 or 0), 10 + (shift and shift - 1 or 0))
	end
	service.CloneTable = function(tbl)
		return (getmetatable(tbl) and not pcall(setmetatable(tbl, getmetatable(tbl)))) and setmetatable({}, {__index = function(_, k) return tbl[k] end}) or table.clone(tbl)
	end
	service.GoodSignal = service.Signal
	service.Yield = function()
		local event = service.Signal.new()
		return {
			Release = function(...) event:Fire(...) end;
			Wait = function(...) return event:Wait(...) end;
			Destroy = function() event:Destroy() end;
			Event = event;
		}
	end
	Remote.UnEncrypted = setmetatable({}, { -- TODO: Start adding a server.Messages message and remove later
		__newindex = function(_, ind, val)
			warn("Unencrypted remote commands are deprecated; moving", ind, "to Remote.Commands. Replace `Remote.Unencrypted` with `Remote.Commands`!")
			Remote.Commands[ind] = val
			Logs:AddLog("Script", `Attempted to add {ind} to legacy Remote.Unencrypted. Moving to Remote.Commands`)
		end
	})
	if service.Wrapped(server.Folder) then
		server.Folder:SetSpecial("Shared", server.Shared)
	end
	Functions.GetRandom = function(pLen)
		local random = math.random
		local format = string.format

		local Len = (type(pLen) == "number" and pLen) or random(5,10) --// reru
		local Res = {}
		for Idx = 1, Len do
			Res[Idx] = format("%02x", random(255))
		end
		return table.concat(Res)
	end
	if HTTP.Trello.API then
		HTTP.Trello.API.GenerateRequestID = Functions.GetRandom
	end
	--// Old settings/plugins backwards compatibility. Do not remove this because many games use old loader ranks!
	for _, rank in {"Owners", "HeadAdmins", "Admins", "Moderators", "Creators"} do
		if Settings[rank] and not Settings.CustomRanks[rank] then
			Settings.Ranks[if rank == "Owners" then "HeadAdmins" else rank].Users = Settings[rank]
		end
	end
	if Settings.CustomRanks then
		local Ranks = Settings.Ranks
		for name, users in Settings.CustomRanks do
			if not Ranks[name] then
				Ranks[name] = {
					Level = 1;
					Users = users;
				}
			end
		end
	end
	for k, v in {-- Legacy aliases
		[`{Settings.Prefix}giveppoints <player> <amount>`] = `{Settings.Prefix}clientscript <player> game:GetService("StarterGui"):SetCore("SendNotification", \{Title = "Points Awarded", Text = "You received <amount> points!", Icon = "rbxassetid://155221172"\})`,
		[`{Settings.Prefix}giveplayerpoints <player> <amount>`] = `{Settings.Prefix}clientscript <player> game:GetService("StarterGui"):SetCore("SendNotification", \{Title = "Points Awarded", Text = "You received <amount> points!", Icon = "rbxassetid://155221172"\})`,
		[`{Settings.Prefix}sendplayerpoints <player> <amount>`] = `{Settings.Prefix}clientscript <player> game:GetService("StarterGui"):SetCore("SendNotification", \{Title = "Points Awarded", Text = "You received <amount> points!", Icon = "rbxassetid://155221172"\})`,
		[`{Settings.Prefix}flyclip <player>`] = `{Settings.Prefix}fly <player> true`;
		[`{Settings.Prefix}showlogs true <player>`] = `{Settings.Prefix}showlogs <player> true`; -- TODO: Remove legacy :showlogs aliases, only temporarily here.
		[`{Settings.Prefix}showlogs false <player>`] = `{Settings.Prefix}showlogs <player> false`;
		[`{Settings.Prefix}showlogs yes <player>`] = `{Settings.Prefix}showlogs <player> true`;
		[`{Settings.Prefix}showlogs no <player>`] = `{Settings.Prefix}showlogs <player> false`;
		[`{Settings.Prefix}showcommandlogs true <player>`] = `{Settings.Prefix}showlogs <player> true`;
		[`{Settings.Prefix}showcommandlogs false <player>`] = `{Settings.Prefix}showlogs <player> false`;
		[`{Settings.Prefix}showcommandlogs yes <player>`] = `{Settings.Prefix}showlogs <player> true`;
		[`{Settings.Prefix}showcommandlogs no <player>`] = `{Settings.Prefix}showlogs <player> false`;
	} do
		if not Variables.Aliases[k] then
			Variables.Aliases[k] = v
		end
	end
	HTTP.CheckHttp = function()
		return HTTP.HttpEnabled
	end
	-- TODO: Remove \\/
	service.OwnsAsset = service.CheckAssetOwnership
	Functions.CatchError = server.Pcall

	Logs:AddLog("Script", "Misc Features Module Loaded")
end


-- Trello plugin
type Card = {id: string, name: string, desc: string, labels: {any}?}
type List = {id: string, name: string, cards: {Card}}

return function(Vargs, GetEnv)
	local server = Vargs.Server;
	local service = Vargs.Service;

	local Settings = server.Settings
	local Functions, Commands, Admin, Anti, Core, HTTP, Logs, Remote, Process, Variables, Deps =
		server.Functions, server.Commands, server.Admin, server.Anti, server.Core, server.HTTP, server.Logs, server.Remote, server.Process, server.Variables, server.Deps

	HTTP.Trello.Update = function()
		if not HTTP.Trello.API or not Settings.Trello_Enabled then
			Logs:AddLog("Script", "Not running Trello API update")
			return
		end

		if not HTTP.HttpEnabled then
			warn("Unable to connect to Trello. Make sure HTTP Requests are enabled in Game Settings.")
			Logs:AddLog("Script", "Not running Trello API update due to HTTP request being disabled.")
			return
		else
			local data = {
				Bans = {};
				Mutes = {};
				Music = {};
				Whitelist = {};
				Blacklist = {};
				InsertList = {};
				Agents = {};
				Ranks = {
					["Moderators"] = {},
					["Admins"] = {},
					["HeadAdmins"] = {},
					["Creators"] = {}
				};
			}

			local function grabData(board)
				local lists: {List} = HTTP.Trello.API.getListsAndCards(board, true)
				if #lists == 0 then error("L + ratio") end --TODO: Improve TrelloAPI error handling so we don't need to assume no lists = failed request

				for _, list in lists do
					local foundOverride = false

					for _, override in HTTP.Trello.Overrides do
						if table.find(override.Lists, list.name) then
							foundOverride = true
							Logs:AddLog("Script", `Found override {list.name} for board {board} with {type(list.cards) == "table" and #list.cards or list.cards} cards.`)
							for _, card in list.cards do
								override.Process(card, data)
							end
							break
						end
					end

					-- Allow lists for custom ranks
					if not foundOverride and not data.Ranks[list.name] then
						local rank = Settings.Ranks[list.name]

						if rank and not rank.IsExternal then
							local users = {}
							for _, card in list.cards do
								table.insert(users, card.name)
							end
							data.Ranks[list.name] = users
							Logs:AddLog("Script", `Found custom rank override {list.name} for board {board} with {type(list.cards) == "table" and #list.cards or list.cards} cards.`)
						end
					end
				end
			end

			local success = true
			local boards = {
				Settings.Trello_Primary,
				unpack(Settings.Trello_Secondary)
			}
			for _, v in boards do
				if not v or service.Trim(v) == "" then
					continue
				end
				Logs:AddLog("Script", `Attempting to pool data from Trello board {v}.`)
				local ran, err = pcall(grabData, v)
				if not ran then
					warn("Unable to reach Trello. Ensure your board IDs, Trello key, and token are all correct. If issue persists, try increasing HttpWait in your Adonis settings.")
					Logs:AddLog("Script", `Failed to pool data from Trello board {v} due to {err}.`)
					success = false
					break
				else
					Logs:AddLog("Script", `Successfully pooled data from Trello board {v}.`)
				end
			end

			-- Only replace existing values if all data was fetched successfully
			if success then
				HTTP.Trello.Bans = data.Bans
				HTTP.Trello.Music = data.Music
				HTTP.Trello.InsertList = data.InsertList
				HTTP.Trello.Mutes = data.Mutes
				HTTP.Trello.Agents = data.Agents

				Variables.Blacklist.Lists.Trello = data.Blacklist
				Variables.Whitelist.Lists.Trello = data.Whitelist

				--// Clear any custom ranks that were not fetched from Trello
				for rank, info in Settings.Ranks do
					if rank.IsExternal and not data.Ranks[rank] then
						Settings.Ranks[rank] = nil
					end
				end

				for rank, users in data.Ranks do
					local name = string.format("[Trello] %s", server.Functions.Trim(rank))
					Settings.Ranks[name] = {
						Level = Settings.Ranks[rank].Level or 1;
						Users = users,
						IsExternal = true,
						Hidden = Settings.Trello_HideRanks;
					}
				end

				for _, v in service.GetPlayers() do
					local isBanned, Reason = Admin.CheckBan(v)
					if isBanned then
						v:Kick(string.format("%s | Reason: %s", Variables.BanMessage, (Reason or "No reason provided")))
						continue
					end

					Admin.UpdateCachedLevel(v)
				end

				Logs.AddLog(Logs.Script,{
					Text = "Updated Trello data";
					Desc = "Data was retreived from Trello";
				})
			end
		end
	end

	Remote.Commands.TrelloOperation = function(p: Player, args: {[number]: any})
		local adminLevel = Admin.GetLevel(p)
		local data = args[1]

		if data.Action == "MakeCard" then
			local command = Commands.MakeCard
			if command and Admin.CheckComLevel(adminLevel, command.AdminLevel) then
				local trello = HTTP.Trello.API

				local listName = data.List
				local name = data.Name
				local desc = data.Desc

				for _, overrideList in HTTP.Trello.GetOverrideLists() do
					if service.Trim(string.lower(overrideList)) == service.Trim(string.lower(listName)) then
						Functions.Hint("You cannot create a card in that list", {p}) -- Why??
						return
					end
				end

				local lists = trello.getLists(Settings.Trello_Primary)
				local list = trello.getListObj(lists, listName)
				if list then
					local card = trello.makeCard(list.id, name, desc)
					Functions.Hint(`Made card "{card.name}" in list "{list.name}"`, {p})
					Logs.AddLog(Logs.Script,{
						Text = `{p} performed Trello operation`;
						Desc = "Player created a Trello card";
						Player = p;
					})
				else
					Functions.Hint(`"{listName}" does not exist"`, {p})
				end
			end
		end
	end

	-- // Moderator commands
	Commands.ShowTrelloBansList = {
		Prefix = Settings.Prefix;
		Commands = {"trellobans", "trellobanlist", "syncedtrellobans", "showtrellobans"};
		Args = {};
		Description = "Shows bans synced from Trello.";
		TrelloRequired = true;
		AdminLevel = "Moderators";
		ListUpdater = function(plr: Player)
			local tab = table.create(#HTTP.Trello.Bans + 2)
			tab[1] = `# Banned Users: {#HTTP.Trello.Bans}`
			tab[2] = "―――――――――――――――――――――――"
			for _, banData in HTTP.Trello.Bans do
				table.insert(tab, {
					Text = banData.Name,
					Desc = banData.Reason or "No reason specified",
				})
			end
			return tab
		end;
		Function = function(plr: Player, args: {string})
			local trello = HTTP.Trello.API
			if not Settings.Trello_Enabled or trello == nil then
				Functions.Notification("Trello Synced Ban List", "Trello has not been enabled.", {plr})
			else
				Remote.MakeGui(plr, "List", {
					Title = "Trello Synced Bans List";
					Icon = server.MatIcons.Gavel;
					Tab = Logs.ListUpdaters.ShowTrelloBansList(plr);
					Update = "ShowTrelloBansList";
				})
			end
		end;
	}

	-- // Admin commands
	Commands.TrelloBan = {
		Prefix = Settings.Prefix;
		Commands = {"trelloban"};
		Args = {"player/user", "reason"};
		Description = "Adds a user to the Trello ban list (Trello needs to be configured)";
		Filter = true;
		CrossServerDenied = true;
		TrelloRequired = true;
		AdminLevel = "Admins";
		Function = function(plr: Player, args: {string}, data: {any})
			local trello = HTTP.Trello.API
			if not Settings.Trello_Enabled or trello == nil then
				Functions.Notification("Trelloban", "Trello has not been configured.", {plr}, 10, "MatIcon://Description")
				return
			end

			local lists = trello.getLists(Settings.Trello_Primary)
			local list = trello.getListObj(lists, {"Banlist", "Ban List", "Bans"})

			local level = data.PlayerData.Level
			local reason = string.format("Administrator: %s\nReason: %s", service.FormatPlayer(plr), (args[2] or "N/A"))

			for _, v in service.GetPlayers(plr, args[1], {
				IsKicking = true;
				UseFakePlayer = true;
				})
			do
				if level > Admin.GetLevel(v) then
					trello.makeCard(
						list.id,
						string.format("%s:%d", (v and tostring(v.Name) or tostring(v)), v.UserId),
						reason
					)

					pcall(function() v:Kick(reason) end)
					Functions.LogAdminAction(plr, "Trello Ban", v.Name, `Reason: {args[2] or "N/A"}`)
					Functions.Notification("Notification", `Trello-banned {service.FormatPlayer(v, true)}`, {plr}, 5, "MatIcons://Gavel")
				end
			end

			HTTP.Trello.Update()
		end;
	}

	-- // HeadAdmin commands
	Commands.MakeList = {
		Prefix = Settings.Prefix;
		Commands = {"makelist", "newlist", "newtrellolist", "maketrellolist"};
		Args = {"name"};
		Description = "Adds a list to the Trello board set in Settings. AppKey and Token MUST be set and have write perms for this to work.";
		TrelloRequired = true;
		AdminLevel = "HeadAdmins";
		Function = function(plr: Player, args: {string})
			local trello = HTTP.Trello.API
			if not Settings.Trello_Enabled or trello == nil then return Functions.Hint('Trello has not been configured in settings', {plr}) end

			local list = trello.Boards.MakeList(Settings.Trello_Primary, assert(args[1], "You need to supply a list name."))
			Functions.Hint(`Made list {list.name}`, {plr})
		end
	}

	Commands.ViewList = {
		Prefix = Settings.Prefix;
		Commands = {"viewtrellolist", "viewlist"};
		Args = {"name"};
		Description = "Views the specified Trello list from the primary board set in Settings.";
		TrelloRequired = true;
		AdminLevel = "HeadAdmins";
		Function = function(plr: Player, args: {string})
			local trello = HTTP.Trello.API
			if not Settings.Trello_Enabled or trello == nil then return Functions.Hint('Trello has not been configured in settings', {plr}) end
			local list = assert(trello.Boards.GetList(Settings.Trello_Primary, assert(args[1], "Enter a valid list name")), "List not found.")

			local cards = trello.Lists.GetCards(list.id)
			local temp = table.create(#cards)
			for _, v in cards do
				table.insert(temp, {Text = v.name, Desc = v.desc})
			end
			Remote.MakeGui(plr, "List", {Title = list.name; Tab = temp})
		end
	}

	Commands.MakeCard = {
		Prefix = Settings.Prefix;
		Commands = {"makecard", "maketrellocard", "createcard"};
		Args = {};
		Description = "Opens a gui to make new Trello cards. AppKey and Token MUST be set and have write perms for this to work.";
		TrelloRequired = true;
		AdminLevel = "HeadAdmins";
		Function = function(plr: Player, args: {string})
			Remote.MakeGui(plr, "CreateCard")
		end
	}

	-- // Initialization
	if Settings.Trello_Enabled then
		service.StartLoop("TRELLO_UPDATER", Settings.HttpWait, HTTP.Trello.Update, true)
		Logs:AddLog("Script", `Trello update loop has been started to run every {Settings.HttpWait} seconds.`)

		if not Settings.Trello_AppKey or Settings.Trello_AppKey == "" then
			table.insert(server.Messages, {
				Level = 301;
				Title = "Potential Trello issue!";
				Message = "You have enabled Trello without having an AppKey and thus Trello will likely not work! For better behavior it is adviced to also add a Trello Token. Click to see more";
				Time = 15;
				Icon = "MatIcon://Description";
				onClick = (server.Data and server.Data.NightlyMode) and Core.Bytecode([[
					local window = client.UI.Make("Window", {
						Title = "How to change the Trello AppKey";
						Size = {700,300};
						Icon = "rbxassetid://7510994359";
					})

					window:Add("ImageLabel", {
						Image = "rbxassetid://111103486118064";
					})

					window:Ready()
			]]) or nil})
		end
	end
	Logs:AddLog("Script", "Trello Module Loaded")
end


return function(Vargs, GetEnv)
	local server = Vargs.Server;
	local service = Vargs.Service;

	local Settings = server.Settings
	local Functions, Commands, Admin, Anti, Core, HTTP, Logs, Remote, Process, Variables, Deps =
		server.Functions, server.Commands, server.Admin, server.Anti, server.Core, server.HTTP, server.Logs, server.Remote, server.Process, server.Variables, server.Deps

	local LastDateTime, Messages = "Loading...", {"The messages haven't loaded. Please comeback later..."}
	Variables.UrgentModuleId = Variables.UrgentModuleId or 8096250407

	task.spawn(xpcall, function()
		local Alerts = require(Deps.__URGENT_MESSAGES)
		local assetInfo = type(Variables.UrgentModuleId) == "number" and service.GetProductInfo(Variables.UrgentModuleId, Enum.InfoType.Asset)
		local version = (assetInfo and assetInfo.Created) and assetInfo.Updated
		local lastVersion = Core.GetData("LastAlertVersion")

		if not version or not lastVersion or version ~= lastVersion then
			Logs:AddLog("Script", "Loading alerts from cloud module")
			xpcall(function()
				if not server.Core.SilentStartup then
					print("Requiring Alerts Module by ID; Expand for module URL > ", {URL = `https://www.roblox.com/library/{Variables.UrgentModuleId}/Adonis-Alerts-Module`})
				end

				local alertTab = require(Variables.UrgentModuleId)

				if alertTab then
					Alerts = alertTab
					Logs:AddLog("Script", "Alerts cloud module returned proper data")
					Core.SetData("LastAlertVersion", version)
					Core.SetData("CachedAlerts", service.HttpService:JSONEncode(alertTab))
				end
			end, function(reason)
				warn(`Error occured while requiring the urgent messages module. Reason {reason}`)
				Logs:AddLog("Errors", `Error occured while requiring the urgent messages module. Reason {reason}`)
				table.insert(server.Messages, `Error occured while requiring the urgent messages module. Reason {reason}`)
			end)
		elseif version and lastVersion then
			Logs:AddLog("Script", "Loading alerts from datastore")
			xpcall(function()
				Alerts = service.HttpService:JSONDecode(Core.GetData("CachedAlerts"))
			end, function(reason)
				warn(`Error occured while loading alerts from datastore. Reason {reason}`)
				Logs:AddLog("Errors", `Error occured while loading alerts from datastore. Reason {reason}`)
				table.insert(server.Messages, `Error occured while loading alerts from datastore. Reason {reason}`)
				Core.RemoveData("LastAlertVersion")
				Core.RemoveData("CachedAlerts")
			end)
		end

		local MessageVersion = Alerts.MessageVersion;			--// Message version/number
		local MessageAdminType = Alerts.MessageAdminType;  		--// Minimum admin level to be notified (Or Donors or Players or nil to not notify)
		local MessageDate = Alerts.MessageDate;					--// Time of message creation
		local MessageDuration = Alerts.MessageDuration; 		--// How long should we notify people about this message
		LastDateTime = Alerts.LastDateTime;						--// Last message date and time
		Messages = Alerts.Messages;								--// List of alert messages/lines

		local function checkDoNotify(p, data)
			local lastMessage = data.LastUrgentMessage or 0;

			if lastMessage < MessageVersion and os.time() - MessageDate <= MessageDuration then
				if MessageAdminType == "Players" then
					return true
				elseif MessageAdminType == "Donors" then
					if Admin.CheckDonor(p) then
						return true
					end
				elseif type(MessageAdminType) == "number" and Admin.GetLevel(p) >= MessageAdminType then
					return true
				end
			end
		end

		Variables.UrgentMessageCounter = MessageVersion

		local function onPlayerAdded(p: Player)
			if MessageAdminType then
				local data = Core.GetPlayer(p);
				if checkDoNotify(p, data) then
					data.LastUrgentMessage = MessageVersion;
					task.delay(0.5, Functions.Notification, "Urgent Message!", "Click to view messages", {p}, 20, "MatIcon://Announcement", Core.Bytecode("client.Remote.Send('ProcessCommand',':adonisalerts')"))
				end
			end
		end

		for _, p in service.Players:GetPlayers() do
			task.spawn(pcall, onPlayerAdded, p)
		end

		service.Events.PlayerAdded:Connect(onPlayerAdded)
		Logs:AddLog("Script", "Successfully loaded alerts module data")
	end, warn)

	Commands.UrgentMessages = {
		Prefix = ":";
		Commands = {"adonisalerts", "urgentmessages", "urgentalerts", "adonismessages", "urgentadonismessages", "ulog"};
		Args = {};
		Description = "URGENT ADONIS RELATED MESSAGES";
		AdminLevel = "Players";
		Function = function(plr,args)
			Remote.MakeGui(plr,"List",{
				Title = `URGENT MESSAGES [Recent: {LastDateTime}]`,
				Icon = "rbxassetid://7467273592",
				Table = Messages,
				Font = "Code",
				PageSize = 100;
				Size = {700, 400},
			})
		end;
	};

	Logs:AddLog("Script", "Alerts Module Loaded")
end


--!native
--// File: LinkedList.lua
--// Desc: Provides a more performance-friendly alternative to large tables that get shifted in indexes constantly
--// Author: Coasterteam
local LinkedList = {}
LinkedList.__index = LinkedList
LinkedList.__meta = "DLL"

local LinkNode = {}
LinkNode.__index = LinkNode

function LinkNode.new(data, n, prev)
	local self = setmetatable({}, LinkNode)

	self.data = data
	self.prev = prev
	self.next = n

	return self
end

function LinkNode:setnext(n)
	self.next = n
end

function LinkNode:setprev(p)
	self.prev = p
end

function LinkNode:Destroy()
	for i, _ in self do
		self[i] = nil
	end
	setmetatable(self, nil)
end

--[==[
	LinkedNode end
]==]

function LinkedList.new()
	local self = setmetatable({}, LinkedList)

	self.snode = nil
	self.enode = nil

	self.count = 0

	return self
end

function LinkedList:IsReal()
	--// this is a ghost function
	--// don't remove it
end

function LinkedList:AddStart(data)
	local old = self.snode
	local newNode = LinkNode.new(data)
	newNode:setprev(nil)

	if not old then
		self.enode = newNode
	else
		old:setprev(newNode)
	end

	newNode:setnext(old)
	self.snode = newNode
	self.count += 1
end

function LinkedList:AddEnd(data)
	local old = self.enode
	local newNode = LinkNode.new(data)
	newNode:setnext(nil)

	if not old then
		self.snode = newNode
	else
		old:setnext(newNode)
	end

	newNode:setprev(old)
	self.enode = newNode
	self.count += 1
end

function LinkedList:AddToStartAndRemoveEndIfEnd(data, limit)
	self:AddStart(data)

	if self.count > limit and self.enode then
		self:RemoveNode(self.enode)
	end
end

function LinkedList:AddBetweenNodes(data, left, right)
	if not left or not right then
		return false
	end

	local newNode = LinkNode.new(data)

	newNode:setnext()
	newNode:setprev()
	left:setnext(newNode)
	right:setprev(newNode)
	self.count += 1
end

function LinkedList:RemoveNode(node)
	local data = node.data
	local prev = node.prev
	local nextN = node.next

	if self.snode == node then
		self.snode = nextN
	end
	if self.enode == node then
		self.enode = prev
	end

	if nextN then
		nextN:setprev(prev)
	end
	if prev then
		prev:setnext(nextN)
	end

	node:setnext(nil)
	node:setprev(nil)
	node:Destroy()
	self.count -= 1

	return data
end

function LinkedList:Get(val: any)
	local nodes = table.create(self.count)
	local curr = self.snode

	while curr and type(curr) == "table" do
		if val then
			--// Pass a function and Adonis will pass through the current node into it
			--// Return true to add it to the "list"
			--// Final list will be returned after the call
			--// Passed value:
			--[==[
				Node:
					.data: The data inside of the node
					.next: The next node in the list
					.prev: The previous node in the list
			]==]
			if type(val) == "function" then
				local success, found = pcall(val, curr)
				if success and found then
					table.insert(nodes, curr)
				end
			else
				if curr.data == val then
					table.insert(nodes, curr)
				end
			end
		else
			table.insert(nodes, curr)
		end

		curr = curr.next
	end

	return nodes
end

--// Returns a list of all passing notes with data
function LinkedList:GetAsTable(val: any)
	local nodes = table.create(self.count)
	local curr = self.snode

	while curr and type(curr) == "table" do
		--// Pass a function and Adonis will pass through the current node into it
		--// Return true to add it to the "list"
		--// Final list will be returned after the call
		--// Passed value:
		--[==[
			Node:
				.data: The data inside of the node
				.next: The next node in the list
				.prev: The previous node in the list
		]==]
		if val then
			if type(val) == "function" then
				local success, found = pcall(val, curr)
				if success and found then
					table.insert(nodes, curr.data)
				end
			else
				if curr.data == val then
					table.insert(nodes, curr.data)
				end
			end
		else
			table.insert(nodes, curr.data)
		end

		curr = curr.next
	end

	return nodes
end

function LinkedList:Destroy()
	for k, _ in self do
		self[k] = nil
	end
	setmetatable(self, nil)
end

return LinkedList


--!native
--!optimize 2
--!nolint TableOperations
--# selene: allow(multiple_statements, mixed_table)
--[[
FiOne
Copyright (C) 2021  Rerumu

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
]] --

local lua_wrap_state
local stm_lua_func

-- SETLIST config
local FIELDS_PER_FLUSH = 50

-- remap for better lookup
local OPCODE_RM = {
	-- level 1
	[22] = 18, -- JMP
	[31] = 8, -- FORLOOP
	[33] = 28, -- TFORLOOP
	-- level 2
	[0] = 3, -- MOVE
	[1] = 13, -- LOADK
	[2] = 23, -- LOADBOOL
	[26] = 33, -- TEST
	-- level 3
	[12] = 1, -- ADD
	[13] = 6, -- SUB
	[14] = 10, -- MUL
	[15] = 16, -- DIV
	[16] = 20, -- MOD
	[17] = 26, -- POW
	[18] = 30, -- UNM
	[19] = 36, -- NOT
	-- level 4
	[3] = 0, -- LOADNIL
	[4] = 2, -- GETUPVAL
	[5] = 4, -- GETGLOBAL
	[6] = 7, -- GETTABLE
	[7] = 9, -- SETGLOBAL
	[8] = 12, -- SETUPVAL
	[9] = 14, -- SETTABLE
	[10] = 17, -- NEWTABLE
	[20] = 19, -- LEN
	[21] = 22, -- CONCAT
	[23] = 24, -- EQ
	[24] = 27, -- LT
	[25] = 29, -- LE
	[27] = 32, -- TESTSET
	[32] = 34, -- FORPREP
	[34] = 37, -- SETLIST
	-- level 5
	[11] = 5, -- SELF
	[28] = 11, -- CALL
	[29] = 15, -- TAILCALL
	[30] = 21, -- RETURN
	[35] = 25, -- CLOSE
	[36] = 31, -- CLOSURE
	[37] = 35, -- VARARG
}

-- opcode types for getting values
local OPCODE_T = {
	[0] = "ABC",
	"ABx",
	"ABC",
	"ABC",
	"ABC",
	"ABx",
	"ABC",
	"ABx",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"AsBx",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"AsBx",
	"AsBx",
	"ABC",
	"ABC",
	"ABC",
	"ABx",
	"ABC",
}

local OPCODE_M = {
	[0] = {b = "OpArgR", c = "OpArgN"},
	{b = "OpArgK", c = "OpArgN"},
	{b = "OpArgU", c = "OpArgU"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgU", c = "OpArgN"},
	{b = "OpArgK", c = "OpArgN"},
	{b = "OpArgR", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgN"},
	{b = "OpArgU", c = "OpArgN"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgU", c = "OpArgU"},
	{b = "OpArgR", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgR", c = "OpArgR"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgK", c = "OpArgK"},
	{b = "OpArgR", c = "OpArgU"},
	{b = "OpArgR", c = "OpArgU"},
	{b = "OpArgU", c = "OpArgU"},
	{b = "OpArgU", c = "OpArgU"},
	{b = "OpArgU", c = "OpArgN"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgR", c = "OpArgN"},
	{b = "OpArgN", c = "OpArgU"},
	{b = "OpArgU", c = "OpArgU"},
	{b = "OpArgN", c = "OpArgN"},
	{b = "OpArgU", c = "OpArgN"},
	{b = "OpArgU", c = "OpArgN"},
}

local intiger_types = {
	[1] = buffer.readu8,
	[2] = buffer.readu16,
	[4] = buffer.readu32,
}

local intiger_write_type = {
	[1] = buffer.writeu8,
	[2] = buffer.writeu16,
	[4] = buffer.writeu32,
}

-- int rd_int(string src, int s, int e)
-- @src - Source binary string
-- @s - Start index of a little endian integer
-- @e - End index of the integer
local function rd_int(src, s, e)
	return intiger_types[e - s](src, s)
end

-- number big_endian(string src, int s)
-- @callback - Function to be called after bitswap
-- @byte_count - Lenght of the number
local function big_endian(callback, byte_count)
	return function(src, s, e)
		local e, write = (e or byte_count) * 8, intiger_write_type[e]
		write(src, s, bit32.rshift(bit32.byteswap(rd_int(src, s, e)), 32 - e))
		local n2 = callback(src, s)
		write(src, s, bit32.rshift(bit32.byteswap(rd_int(src, s, e)), 32 - e))

		return n2
	end
end

-- to avoid nested ifs in deserializing
local float_types = {
	[4] = {little = buffer.readf32, big = big_endian(buffer.readf32)},
	[8] = {little = buffer.readf64, big = big_endian(buffer.readf64)},
}

-- byte stm_byte(Stream S)
-- @S - Stream object to read from
local function stm_byte(S)
	local idx = S.index
	local bt = buffer.readu8(S.source, idx)

	S.index = idx + 1
	return bt
end

-- string stm_string(Stream S, int len)
-- @S - Stream object to read from
-- @len - Length of string being read
local function stm_string(S, len)
	local str = buffer.readstring(S.source, S.index, len)

	S.index += len
	return str
end

-- string stm_lstring(Stream S)
-- @S - Stream object to read from
local function stm_lstring(S)
	local len = S:s_szt()
	local str

	if len ~= 0 then str = string.sub(stm_string(S, len), 1, -2) end

	return str
end

-- fn cst_int_rdr(string src, int len, fn func)
-- @len - Length of type for reader
-- @func - Reader callback
local function cst_int_rdr(len, func)
	return function(S)
		local pos = S.index + len
		local int = func(S.source, S.index, pos)
		S.index = pos

		return int
	end
end

-- fn cst_flt_rdr(string src, int len, fn func)
-- @len - Length of type for reader
-- @func - Reader callback
local function cst_flt_rdr(len, func)
	return function(S)
		local flt = func(S.source, S.index)
		S.index = S.index + len

		return flt
	end
end

local function stm_inst_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		local ins = S:s_ins()
		local op = bit32.band(ins, 0x3F)
		local args = OPCODE_T[op]
		local mode = OPCODE_M[op]
		local data = {value = ins, op = OPCODE_RM[op], A = bit32.band(bit32.rshift(ins, 6), 0xFF)}

		if args == "ABC" then
			data.B = bit32.band(bit32.rshift(ins, 23), 0x1FF)
			data.C = bit32.band(bit32.rshift(ins, 14), 0x1FF)
			data.is_KB = mode.b == "OpArgK" and data.B > 0xFF -- post process optimization
			data.is_KC = mode.c == "OpArgK" and data.C > 0xFF

			if op == 10 then -- decode NEWTABLE array size, store it as constant value
				local e = bit32.band(bit32.rshift(data.B, 3), 31)
				if e == 0 then
					data.const = data.B
				else
					data.const = bit32.lshift(bit32.band(data.B, 7) + 8, e - 1)
				end
			end
		elseif args == "ABx" then
			data.Bx = bit32.band(bit32.rshift(ins, 14), 0x3FFFF)
			data.is_K = mode.b == "OpArgK"
		elseif args == "AsBx" then
			data.sBx = bit32.band(bit32.rshift(ins, 14), 0x3FFFF) - 131071
		end

		list[i] = data
	end

	return list
end

local function stm_const_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		local tt = stm_byte(S)
		local k

		if tt == 1 then
			k = stm_byte(S) ~= 0
		elseif tt == 3 then
			k = S:s_num()
		elseif tt == 4 then
			k = stm_lstring(S)
		end

		list[i] = k -- offset +1 during instruction decode
	end

	return list
end

local function stm_sub_list(S, src)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		list[i] = stm_lua_func(S, src) -- offset +1 in CLOSURE
	end

	return list
end

local function stm_line_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do list[i] = S:s_int() end

	return list
end

local function stm_loc_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do list[i] = {varname = stm_lstring(S), startpc = S:s_int(), endpc = S:s_int()} end

	return list
end

local function stm_upval_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do list[i] = stm_lstring(S) end

	return list
end

function stm_lua_func(S, psrc)
	local proto = {}
	local src = stm_lstring(S) or psrc -- source is propagated

	proto.source = src -- source name

	S:s_int() -- line defined
	S:s_int() -- last line defined

	proto.num_upval = stm_byte(S) -- num upvalues
	proto.num_param = stm_byte(S) -- num params

	proto.is_vararg = stm_byte(S) -- vararg flag
	proto.max_stack = stm_byte(S) -- max stack size

	proto.code = stm_inst_list(S)
	proto.const = stm_const_list(S)
	proto.subs = stm_sub_list(S, src)
	proto.lines = stm_line_list(S)

	stm_loc_list(S)
	stm_upval_list(S)

	-- post process optimization
	proto.needs_arg = bit32.band(proto.is_vararg, 0x5) == 0x5
	for _, v in ipairs(proto.code) do
		if v.is_K then
			v.const = proto.const[v.Bx + 1] -- offset for 1 based index
		else
			if v.is_KB then v.const_B = proto.const[v.B - 0xFF] end

			if v.is_KC then v.const_C = proto.const[v.C - 0xFF] end
		end
	end

	return proto
end

local function lua_bc_to_state(src)
	-- func reader
	local rdr_func

	-- header flags
	local little
	local size_int
	local size_szt
	local size_ins
	local size_num
	local flag_int

	-- stream object
	local stream = {
		-- data
		index = 0,
		source = typeof(src) == "buffer" and src or buffer.fromstring(src),
	}

	assert(stm_string(stream, 4) == "\27Lua", "invalid Lua signature")
	assert(stm_byte(stream) == 0x51, "invalid Lua version")
	assert(stm_byte(stream) == 0, "invalid Lua format")

	little = stm_byte(stream) ~= 0
	size_int = stm_byte(stream)
	size_szt = stm_byte(stream)
	size_ins = stm_byte(stream)
	size_num = stm_byte(stream)
	flag_int = stm_byte(stream) ~= 0

	rdr_func = little and rd_int or big_endian(rd_int)
	stream.s_int = cst_int_rdr(size_int, rdr_func)
	stream.s_szt = cst_int_rdr(size_szt, rdr_func)
	stream.s_ins = cst_int_rdr(size_ins, rdr_func)

	if flag_int then
		stream.s_num = cst_int_rdr(size_num, rdr_func)
	elseif float_types[size_num] then
		stream.s_num = cst_flt_rdr(size_num, float_types[size_num][little and "little" or "big"])
	else
		error("unsupported float size")
	end

	return stm_lua_func(stream, "@virtual")
end

local function close_lua_upvalues(list, index)
	for i, uv in pairs(list) do
		if uv.index >= index then
			uv.value = uv.store[uv.index] -- store value
			uv.store = uv
			uv.index = "value" -- self reference
			list[i] = nil
		end
	end
end

local function open_lua_upvalue(list, index, memory)
	local prev = list[index]

	if not prev then
		prev = {index = index, store = memory}
		list[index] = prev
	end

	return prev
end

local function on_lua_error(failed, err)
	local src = failed.source
	local line = failed.lines[failed.pc - 1]

	error(string.format("%s:%i: %s", src, line, err), 0)
end

-- ccuser44 added multithreading support
-- TODO: Create multiple threads for closure and use a module and/or round robin load balancer to distribute usage
local isClient, actorContainer
local function new_threaded_closure(proto, env, upval)
	isClient = isClient or game:GetService("RunService"):IsClient()
	actorContainer = actorContainer or script
	local actor = Instance.new("Actor")
	local clone = script:Clone()
	local runner = script[isClient and "LocalRunner" or "Runner"]:Clone()
	local event = Instance.new("BindableEvent")
	local tag = 0

	for _, v in clone:GetChildren() do
		if v:IsA("Actor") then
			v:Destroy()
		end
	end

	event.Name = "ReturnPass"
	event.Parent = runner
	clone.Parent = runner
	runner.Disabled = false
	runner.Parent = actor
	actor.Name = "MultithreadRunner_"..tostring(math.random())
	actor.Parent = actorContainer

	task.defer(actor.SendMessage, actor, "wrap_state", proto, env, upval)

	task.wait()

	return function(...)
		tag = tag + 1
		local currentTag = tag
		local routine = coroutine.running()
		local connection = event.Event:Connect(function(eventTag, ...)
			if eventTag == currentTag then
				coroutine.resume(routine, ...)
			end
		end)

		task.defer(actor.SendMessage, actor, "run_callback", currentTag, ...)

		local args = table.pack(coroutine.yield())
		connection:Disconnect()

		if not args[1] then
			error(args[2], 2)
		end

		return table.unpack(args, 2, args.n)
	end
end

local function run_lua_func(state, env, upvals)
	local code = state.code
	local subs = state.subs
	local vararg = state.vararg

	local top_index = -1
	local open_list = {}
	local memory = state.memory
	local pc = state.pc

	while true do
		local inst = code[pc]
		local op = inst.op
		pc = pc + 1

		if op < 18 then
			if op < 8 then
				if op < 3 then
					if op < 1 then
						--[[LOADNIL]]
						for i = inst.A, inst.B do memory[i] = nil end
					elseif op > 1 then
						--[[GETUPVAL]]
						local uv = upvals[inst.B]

						memory[inst.A] = uv.store[uv.index]
					else
						--[[ADD]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs + rhs
					end
				elseif op > 3 then
					if op < 6 then
						if op > 4 then
							--[[SELF]]
							local A = inst.A
							local B = inst.B
							local index

							if inst.is_KC then
								index = inst.const_C
							else
								index = memory[inst.C]
							end

							memory[A + 1] = memory[B]
							memory[A] = memory[B][index]
						else
							--[[GETGLOBAL]]
							memory[inst.A] = env[inst.const]
						end
					elseif op > 6 then
						--[[GETTABLE]]
						local index

						if inst.is_KC then
							index = inst.const_C
						else
							index = memory[inst.C]
						end

						memory[inst.A] = memory[inst.B][index]
					else
						--[[SUB]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs - rhs
					end
				else --[[MOVE]]
					memory[inst.A] = memory[inst.B]
				end
			elseif op > 8 then
				if op < 13 then
					if op < 10 then
						--[[SETGLOBAL]]
						env[inst.const] = memory[inst.A]
					elseif op > 10 then
						if op < 12 then
							--[[CALL]]
							local A = inst.A
							local B = inst.B
							local C = inst.C
							local params

							if B == 0 then
								params = top_index - A
							else
								params = B - 1
							end

							local ret_list = table.pack(memory[A](table.unpack(memory, A + 1, A + params)))
							local ret_num = ret_list.n

							if C == 0 then
								top_index = A + ret_num - 1
							else
								ret_num = C - 1
							end

							table.move(ret_list, 1, ret_num, A, memory)
						else
							--[[SETUPVAL]]
							local uv = upvals[inst.B]

							uv.store[uv.index] = memory[inst.A]
						end
					else
						--[[MUL]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs * rhs
					end
				elseif op > 13 then
					if op < 16 then
						if op > 14 then
							--[[TAILCALL]]
							local A = inst.A
							local B = inst.B
							local params

							if B == 0 then
								params = top_index - A
							else
								params = B - 1
							end

							close_lua_upvalues(open_list, 0)

							return memory[A](table.unpack(memory, A + 1, A + params))
						else
							--[[SETTABLE]]
							local index, value

							if inst.is_KB then
								index = inst.const_B
							else
								index = memory[inst.B]
							end

							if inst.is_KC then
								value = inst.const_C
							else
								value = memory[inst.C]
							end

							memory[inst.A][index] = value
						end
					elseif op > 16 then
						--[[NEWTABLE]]
						memory[inst.A] = table.create(inst.const) -- inst.const contains array size
					else
						--[[DIV]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs / rhs
					end
				else
					--[[LOADK]]
					memory[inst.A] = inst.const
				end
			else
				--[[FORLOOP]]
				local A = inst.A
				local step = memory[A + 2]
				local index = memory[A] + step
				local limit = memory[A + 1]
				local loops

				if step >= 0 then
					loops = index <= limit
				else
					loops = index >= limit
				end

				if loops then
					memory[A] = index
					memory[A + 3] = index
					pc = pc + inst.sBx
				end
			end
		elseif op > 18 then
			if op < 28 then
				if op < 23 then
					if op < 20 then
						--[[LEN]]
						memory[inst.A] = #memory[inst.B]
					elseif op > 20 then
						if op < 22 then
							--[[RETURN]]
							local A = inst.A
							local B = inst.B
							local len

							if B == 0 then
								len = top_index - A + 1
							else
								len = B - 1
							end

							close_lua_upvalues(open_list, 0)

							return table.unpack(memory, A, A + len - 1)
						else
							--[[CONCAT]]
							local B, C = inst.B, inst.C
							local success, str = pcall(table.concat, memory, "", B, C)

							if not success then
								str = memory[B]

								for i = B + 1, C do str = str .. memory[i] end
							end

							memory[inst.A] = str
						end
					else
						--[[MOD]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs % rhs
					end
				elseif op > 23 then
					if op < 26 then
						if op > 24 then
							--[[CLOSE]]
							close_lua_upvalues(open_list, inst.A)
						else
							--[[EQ]]
							local lhs, rhs

							if inst.is_KB then
								lhs = inst.const_B
							else
								lhs = memory[inst.B]
							end

							if inst.is_KC then
								rhs = inst.const_C
							else
								rhs = memory[inst.C]
							end

							if (lhs == rhs) == (inst.A ~= 0) then pc = pc + code[pc].sBx end

							pc = pc + 1
						end
					elseif op > 26 then
						--[[LT]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						if (lhs < rhs) == (inst.A ~= 0) then pc = pc + code[pc].sBx end

						pc = pc + 1
					else
						--[[POW]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs ^ rhs
					end
				else
					--[[LOADBOOL]]
					memory[inst.A] = inst.B ~= 0

					if inst.C ~= 0 then pc = pc + 1 end
				end
			elseif op > 28 then
				if op < 33 then
					if op < 30 then
						--[[LE]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						if (lhs <= rhs) == (inst.A ~= 0) then pc = pc + code[pc].sBx end

						pc = pc + 1
					elseif op > 30 then
						if op < 32 then
							--[[CLOSURE]]
							local sub = subs[inst.Bx + 1] -- offset for 1 based index
							local nups = sub.num_upval
							local uvlist

							if nups ~= 0 then
								uvlist = table.create(nups - 1)

								for i = 1, nups do
									local pseudo = code[pc + i - 1]

									if pseudo.op == OPCODE_RM[0] then -- @MOVE
										uvlist[i - 1] = open_lua_upvalue(open_list, pseudo.B, memory)
									elseif pseudo.op == OPCODE_RM[4] then -- @GETUPVAL
										uvlist[i - 1] = upvals[pseudo.B]
									end
								end

								pc = pc + nups
							end

							memory[inst.A] = (sub.source and string.sub(sub.source, 1, 39) == "SUPER_SECRET_ADONIS_MULTITHREAD_PREFIX_") and new_threaded_closure(sub, env, uvlist) or lua_wrap_state(sub, env, uvlist)
						else
							--[[TESTSET]]
							local A = inst.A
							local B = inst.B

							if (not memory[B]) ~= (inst.C ~= 0) then
								memory[A] = memory[B]
								pc = pc + code[pc].sBx
							end
							pc = pc + 1
						end
					else
						--[[UNM]]
						memory[inst.A] = -memory[inst.B]
					end
				elseif op > 33 then
					if op < 36 then
						if op > 34 then
							--[[VARARG]]
							local A = inst.A
							local len = inst.B

							if len == 0 then
								len = vararg.len
								top_index = A + len - 1
							end

							table.move(vararg.list, 1, len, A, memory)
						else
							--[[FORPREP]]
							local A = inst.A
							local init, limit, step

							init = assert(tonumber(memory[A]), "`for` initial value must be a number")
							limit = assert(tonumber(memory[A + 1]), "`for` limit must be a number")
							step = assert(tonumber(memory[A + 2]), "`for` step must be a number")

							memory[A] = init - step
							memory[A + 1] = limit
							memory[A + 2] = step

							pc = pc + inst.sBx
						end
					elseif op > 36 then
						--[[SETLIST]]
						local A = inst.A
						local C = inst.C
						local len = inst.B
						local tab = memory[A]
						local offset

						if len == 0 then len = top_index - A end

						if C == 0 then
							C = inst[pc].value
							pc = pc + 1
						end

						offset = (C - 1) * FIELDS_PER_FLUSH

						table.move(memory, A + 1, A + len, offset + 1, tab)
					else
						--[[NOT]]
						memory[inst.A] = not memory[inst.B]
					end
				else
					--[[TEST]]
					if (not memory[inst.A]) ~= (inst.C ~= 0) then pc = pc + code[pc].sBx end
					pc = pc + 1
				end
			else
				--[[TFORLOOP]]
				local A = inst.A
				local func = memory[A]
				local state = memory[A + 1]
				local index = memory[A + 2]
				local base = A + 3
				local vals

				-- === Luau compatibility - General iteration begin ===
				-- // ccuser44 added support for generic iteration
				-- (Please don't use general iteration in vanilla Lua code)
				if not index and not state and type(func) == "table" then
					local metatable = pcall(getmetatable, func) and getmetatable(func)

					if not (type(metatable) == "table" and rawget(metatable, "__call")) then
						func, state, index = (type(metatable) == "table" and rawget(metatable, "__iter") or next), func, nil
						memory[A], memory[A + 1], memory[A + 2] = func, state, index
					end
				end
				-- === Luau compatibility - General iteration end ===

				vals = {func(state, index)}

				table.move(vals, 1, inst.C, base, memory)

				if memory[base] ~= nil then
					memory[A + 2] = memory[base]
					pc = pc + code[pc].sBx
				end

				pc = pc + 1
			end
		else
			--[[JMP]]
			pc = pc + inst.sBx
		end

		state.pc = pc
	end
end

function lua_wrap_state(proto, env, upval)
	local function wrapped(...)
		local passed = table.pack(...)
		local memory = table.create(proto.max_stack)
		local vararg = {len = 0, list = {}}

		table.move(passed, 1, proto.num_param, 0, memory)

		if proto.num_param < passed.n then
			local start = proto.num_param + 1
			local len = passed.n - proto.num_param

			vararg.len = len
			table.move(passed, start, start + len - 1, 1, vararg.list)
		end

		if proto.needs_arg then
			memory[proto.num_param] = {n = vararg.len, table.unpack(vararg.list, 1, vararg.len)}
		end

		local state = {vararg = vararg, memory = memory, code = proto.code, subs = proto.subs, pc = 1}

		local result = table.pack(pcall(run_lua_func, state, env, upval))

		if result[1] then
			return table.unpack(result, 2, result.n)
		else
			local failed = {pc = state.pc, source = proto.source, lines = proto.lines}

			on_lua_error(failed, result[2])

			return
		end
	end

	return wrapped
end

return setmetatable({
	bc_to_state = lua_bc_to_state,
	wrap_state = lua_wrap_state,
	OPCODE_RM = OPCODE_RM,
	OPCODE_T = OPCODE_T,
	OPCODE_M = OPCODE_M,
}, {__call = function(_, BCode, Env) -- Backwards compatibility for legacy rerubi usage
	return lua_wrap_state(lua_bc_to_state(BCode), Env or {})
end})


--!native
--------------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- API:                                                                       --
--   local Signal = require(THIS MODULE)                                      --
--   local sig = Signal.new()                                                 --
--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --
--   sig:Fire(arg1, arg2, ...)                                                --
--   connection:Disconnect()                                                  --
--   sig:DisconnectAll()                                                      --
--   local arg1, arg2, ... = sig:Wait()                                       --
--                                                                            --
-- Licence:                                                                   --
--   Licenced under the MIT licence.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--------------------------------------------------------------------------------

--[[
MIT License

Copyright (c) 2021 Mark Langen

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

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

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

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread()
	-- Note: We cannot use the initial set of arguments passed to
	-- runEventHandlerInFreeThread for a call to the handler, because those
	-- arguments would stay on the stack for the duration of the thread's
	-- existence, temporarily leaking references. Without access to raw bytecode
	-- there's no way for us to clear the "..." references from the stack.
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection.new(signal, fn)
	return setmetatable({
		_connected = true,
		_signal = signal,
		_fn = fn,
		_next = false,
	}, Connection)
end

function Connection:Disconnect()
	self._connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

-- Make Connection strict
setmetatable(Connection, {
	__index = function(tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(tb, key, value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end
})

-- Signal class
local Signal = {}
Signal.__index = Signal

function Signal.new()
	return setmetatable({
		_handlerListHead = false,
		Event = {},
	}, Signal)
end

function Signal:Connect(fn)
	local connection = Connection.new(self, fn)
	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end
	local signalSelf = self
	function connection:Fire(...)
		signalSelf:Fire(fn)
	end
	function connection:fire(...)
		signalSelf:Fire(fn)
	end
	function connection:disconnect()
		self:Disconnect()
	end
	function connection:wait()
		return signalSelf:Wait()
	end
	return connection
end

function Signal:connect(fn)
	return self:Connect(fn)
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
function Signal:DisconnectAll()
	self._handlerListHead = false
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item._connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
				-- Get the freeRunnerThread to the first yield
				coroutine.resume(freeRunnerThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

function Signal:fire(...)
	self:Fire(...)
end

-- Implement Signal:Wait() in terms of a temporary connection using
-- a Signal:Connect() which disconnects itself.
function Signal:Wait()
	local waitingCoroutine = coroutine.running()
	local cn;
	cn = self:Connect(function(...)
		cn:Disconnect()
		task.spawn(waitingCoroutine, ...)
	end)
	return coroutine.yield()
end

function Signal:wait()
	return self:Wait()
end

-- Implement Signal:Once() in terms of a connection which disconnects
-- itself before running the handler.
function Signal:Once(fn)
	local cn;
	cn = self:Connect(function(...)
		if cn._connected then
			cn:Disconnect()
		end
		fn(...)
	end)
	return cn
end

function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

function Signal:connectOnce(fn)
	return self:Once(fn)
end

function Signal:Destroy()
	self:DisconnectAll()
	setmetatable(self, nil)
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(tb, key, value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end
})

return Signal


--!native
--!optimize 2
--!strict
--[[
	Description: SHA256 digest hash
	Author: XoifailTheGod
	Date: 2024
	Link: https://devforum.roblox.com/t/fastest-sha256-module/3180016
]]
--[=[
	Cryptography library: SHA256

	Sizes:
		Digest: 32 bytes

	Return type: string
	Example usage:
		local Message = buffer.fromstring("Hello World")
		local Result = SHA256(Message)
--]=]

local CONSTANTS = buffer.create(256) do -- CONSTANTS = k
	local RoundConstants = {
		0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
		0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
		0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
		0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
		0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
		0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
		0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
		0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
	}

	for Index, Constant in ipairs(RoundConstants) do
		local BufferOffset = (Index - 1) * 4
		buffer.writeu32(CONSTANTS, BufferOffset, Constant)
	end
end

local function PreProcess(Contents: buffer): (buffer, number)
	local ContentLength = buffer.len(Contents)
	local Padding = -(ContentLength + 9) % 64

	local NewContentLength = ContentLength + 1 + Padding + 8
	local NewContent = buffer.create(NewContentLength)
	buffer.copy(NewContent, 0, Contents)
	buffer.writeu8(NewContent, ContentLength, 128)
	local Length8 = ContentLength * 8
	for Index = 7, 0, -1 do
		local Remainder = Length8 % 256
		buffer.writeu8(NewContent, Index + ContentLength + 1 + Padding, Remainder)
		Length8 = (Length8 - Remainder) / 256
	end

	return NewContent, NewContentLength
end

local OFFSETS = buffer.create(256)
local function DigestBlocks(Blocks: buffer, Length: number): (number, number, number, number, number, number, number, number)
	local A, B, C, D = 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a
	local E, F, G, H = 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19

	local Offsets = OFFSETS
	local Constants = CONSTANTS

	for Offset = 0, Length - 1, 64 do
		for BlockIndex = 0, 60, 4 do
			buffer.writeu32(Offsets, BlockIndex, bit32.byteswap(buffer.readu32(Blocks, Offset + BlockIndex)))
		end

		for Index = 64, 252, 4 do
			local Sub15 = buffer.readu32(Offsets, Index - 60)
			local S0 = bit32.bxor(bit32.rrotate(Sub15, 7), bit32.rrotate(Sub15, 18), bit32.rshift(Sub15, 3))

			local Sub2 = buffer.readu32(Offsets, Index - 8)
			local S1 = bit32.bxor(bit32.rrotate(Sub2, 17), bit32.rrotate(Sub2, 19), bit32.rshift(Sub2, 10))

			local Sub7, Sub16 = buffer.readu32(Offsets, Index - 28), buffer.readu32(Offsets, Index - 64)
			buffer.writeu32(Offsets, Index, (Sub16 + S0 + Sub7 + S1))
		end

		local OldA, OldB, OldC, OldD, OldE, OldF, OldG, OldH = A, B, C, D, E, F, G, H

		for BufferIndex = 0, 252, 4 do
			local S1 = bit32.bxor(bit32.rrotate(E, 6), bit32.rrotate(E, 11), bit32.rrotate(E, 25))
			local Ch = bit32.bxor(bit32.band(E, F), bit32.band(bit32.bnot(E), G))
			local Temp1 = H + S1 + Ch + buffer.readu32(Constants, BufferIndex) + buffer.readu32(Offsets, BufferIndex)
			H, G, F, E, D = G, F, E, D + Temp1, C

			local S0 = bit32.bxor(bit32.rrotate(A, 2), bit32.rrotate(A, 13), bit32.rrotate(A, 22))
			local Maj = bit32.bxor(bit32.band(A, B), bit32.band(A, C), bit32.band(B, C))
			C, B, A = B, A, Temp1 + S0 + Maj
		end

		A, B, C, D, E, F, G, H =
			bit32.bor(A + OldA, 0),
			bit32.bor(B + OldB, 0),
			bit32.bor(C + OldC, 0),
			bit32.bor(D + OldD, 0),
			bit32.bor(E + OldE, 0),
			bit32.bor(F + OldF, 0),
			bit32.bor(G + OldG, 0),
			bit32.bor(H + OldH, 0) :: number
	end

	return A, B, C, D, E, F, G, H
end

local function SHA256(Message: buffer | string): (string, buffer)
	local ProcessedMessage, Length = PreProcess(type(Message) == "string" and buffer.fromstring(Message) or Message)
	local A, B, C, D, E, F, G, H = DigestBlocks(ProcessedMessage, Length)

	local Digest = buffer.create(32)

	buffer.writeu32(Digest, 0, A)
	buffer.writeu32(Digest, 4, B)
	buffer.writeu32(Digest, 8, C)
	buffer.writeu32(Digest, 12, D)
	buffer.writeu32(Digest, 16, E)
	buffer.writeu32(Digest, 20, F)
	buffer.writeu32(Digest, 24, G)
	buffer.writeu32(Digest, 28, H)

	return string.format("%08x%08x%08x%08x%08x%08x%08x%08x", A, B, C, D, E, F, G, H), Digest
end

return {
	SHA256 = SHA256
}


--!native
--[[

	t: a runtime typechecker for Roblox
	by osyrisrblx
	MIT License

]]

local t = {}

function t.type(typeName)
	return function(value)
		local valueType = type(value)
		if valueType == typeName then
			return true
		else
			return false, string.format("%s expected, got %s", typeName, valueType)
		end
	end
end

function t.typeof(typeName)
	return function(value)
		local valueType = typeof(value)
		if valueType == typeName then
			return true
		else
			return false, string.format("%s expected, got %s", typeName, valueType)
		end
	end
end

--[[**
	matches any type except nil

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.any(value)
	if value ~= nil then
		return true
	else
		return false, "any expected, got nil"
	end
end

--Lua primitives

--[[**
	ensures Lua primitive boolean type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.boolean = t.typeof("boolean")

--[[**
	ensures Lua primitive buffer type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.buffer = t.typeof("buffer")

--[[**
	ensures Lua primitive thread type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.thread = t.typeof("thread")

--[[**
	ensures Lua primitive callback type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.callback = t.typeof("function")
t["function"] = t.callback

--[[**
	ensures Lua primitive none type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.none = t.typeof("nil")
t["nil"] = t.none

--[[**
	ensures Lua primitive string type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.string = t.typeof("string")

--[[**
	ensures Lua primitive table type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.table = t.typeof("table")

--[[**
	ensures Lua primitive userdata type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.userdata = t.type("userdata")

--[[**
	ensures Lua primitive vector type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.vector = t.type("vector")

--[[**
	ensures value is a number and non-NaN

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.number(value)
	local valueType = typeof(value)
	if valueType == "number" then
		if value == value then
			return true
		else
			return false, "unexpected NaN value"
		end
	else
		return false, string.format("number expected, got %s", valueType)
	end
end

--[[**
	ensures value is NaN

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.nan(value)
	local valueType = typeof(value)
	if valueType == "number" then
		if value ~= value then
			return true
		else
			return false, "unexpected non-NaN value"
		end
	else
		return false, string.format("number expected, got %s", valueType)
	end
end

-- roblox types

--[[**
	ensures Roblox Axes type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Axes = t.typeof("Axes")

--[[**
	ensures Roblox BrickColor type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.BrickColor = t.typeof("BrickColor")

--[[**
	ensures Roblox CatalogSearchParams type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.CatalogSearchParams = t.typeof("CatalogSearchParams")

--[[**
	ensures Roblox CFrame type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.CFrame = t.typeof("CFrame")

--[[**
	ensures Roblox Color3 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Color3 = t.typeof("Color3")

--[[**
	ensures Roblox ColorSequence type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.ColorSequence = t.typeof("ColorSequence")

--[[**
	ensures Roblox ColorSequenceKeypoint type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.ColorSequenceKeypoint = t.typeof("ColorSequenceKeypoint")

--[[**
	ensures Roblox DateTime type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.DateTime = t.typeof("DateTime")

--[[**
	ensures Roblox DockWidgetPluginGuiInfo type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.DockWidgetPluginGuiInfo = t.typeof("DockWidgetPluginGuiInfo")

--[[**
	ensures Roblox Enum type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Enum = t.typeof("Enum")

--[[**
	ensures Roblox EnumItem type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.EnumItem = t.typeof("EnumItem")

--[[**
	ensures Roblox Enums type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Enums = t.typeof("Enums")

--[[**
	ensures Roblox Faces type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Faces = t.typeof("Faces")

--[[**
	ensures Roblox FloatCurveKey type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.FloatCurveKey = t.typeof("FloatCurveKey")

--[[**
	ensures Roblox Font type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Font = t.typeof("Font")

--[[**
	ensures Roblox Instance type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Instance = t.typeof("Instance")

--[[**
	ensures Roblox NumberRange type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.NumberRange = t.typeof("NumberRange")

--[[**
	ensures Roblox NumberSequence type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.NumberSequence = t.typeof("NumberSequence")

--[[**
	ensures Roblox NumberSequenceKeypoint type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.NumberSequenceKeypoint = t.typeof("NumberSequenceKeypoint")

--[[**
	ensures Roblox OverlapParams type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.OverlapParams = t.typeof("OverlapParams")

--[[**
	ensures Roblox PathWaypoint type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.PathWaypoint = t.typeof("PathWaypoint")

--[[**
	ensures Roblox PhysicalProperties type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.PhysicalProperties = t.typeof("PhysicalProperties")

--[[**
	ensures Roblox Random type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Random = t.typeof("Random")

--[[**
	ensures Roblox Ray type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Ray = t.typeof("Ray")

--[[**
	ensures Roblox RaycastParams type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.RaycastParams = t.typeof("RaycastParams")

--[[**
	ensures Roblox RaycastResult type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.RaycastResult = t.typeof("RaycastResult")

--[[**
	ensures Roblox RBXScriptConnection type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.RBXScriptConnection = t.typeof("RBXScriptConnection")

--[[**
	ensures Roblox RBXScriptSignal type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.RBXScriptSignal = t.typeof("RBXScriptSignal")

--[[**
	ensures Roblox Rect type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Rect = t.typeof("Rect")

--[[**
	ensures Roblox Region3 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Region3 = t.typeof("Region3")

--[[**
	ensures Roblox Region3int16 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Region3int16 = t.typeof("Region3int16")

--[[**
	ensures Roblox TweenInfo type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.TweenInfo = t.typeof("TweenInfo")

--[[**
	ensures Roblox UDim type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.UDim = t.typeof("UDim")

--[[**
	ensures Roblox UDim2 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.UDim2 = t.typeof("UDim2")

--[[**
	ensures Roblox Vector2 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector2 = t.typeof("Vector2")

--[[**
	ensures Roblox Vector2int16 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector2int16 = t.typeof("Vector2int16")

--[[**
	ensures Roblox Vector3 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector3 = t.typeof("Vector3")

--[[**
	ensures Roblox Vector3int16 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector3int16 = t.typeof("Vector3int16")

--[[**
	ensures value is a given literal value

	@param literal The literal to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.literal(...)
	local size = select("#", ...)
	if size == 1 then
		local literal = ...
		return function(value)
			if value ~= literal then
				return false, string.format("expected %s, got %s", tostring(literal), tostring(value))
			end

			return true
		end
	else
		local literals = {}
		for i = 1, size do
			local value = select(i, ...)
			literals[i] = t.literal(value)
		end

		return t.union(table.unpack(literals, 1, size))
	end
end

--[[**
	DEPRECATED
	Please use t.literal
**--]]
t.exactly = t.literal

--[[**
	Returns a t.union of each key in the table as a t.literal

	@param keyTable The table to get keys from

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.keyOf(keyTable)
	local keys = {}
	local length = 0
	for key in keyTable do
		length = length + 1
		keys[length] = key
	end

	return t.literal(table.unpack(keys, 1, length))
end

--[[**
	Returns a t.union of each value in the table as a t.literal

	@param valueTable The table to get values from

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.valueOf(valueTable)
	local values = {}
	local length = 0
	for _, value in valueTable do
		length = length + 1
		values[length] = value
	end

	return t.literal(table.unpack(values, 1, length))
end

--[[**
	ensures value is an integer

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.integer(value)
	local success, errMsg = t.number(value)
	if not success then
		return false, errMsg or ""
	end

	if value % 1 == 0 then
		return true
	else
		return false, string.format("integer expected, got %s", value)
	end
end

--[[**
	ensures value is a number where min <= value

	@param min The minimum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMin(min)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end

		if value >= min then
			return true
		else
			return false, string.format("number >= %s expected, got %s", min, value)
		end
	end
end

--[[**
	ensures value is a number where value <= max

	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMax(max)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg
		end

		if value <= max then
			return true
		else
			return false, string.format("number <= %s expected, got %s", max, value)
		end
	end
end

--[[**
	ensures value is a number where min < value

	@param min The minimum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMinExclusive(min)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end

		if min < value then
			return true
		else
			return false, string.format("number > %s expected, got %s", min, value)
		end
	end
end

--[[**
	ensures value is a number where value < max

	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMaxExclusive(max)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end

		if value < max then
			return true
		else
			return false, string.format("number < %s expected, got %s", max, value)
		end
	end
end

--[[**
	ensures value is a number where value > 0

	@returns A function that will return true iff the condition is passed
**--]]
t.numberPositive = t.numberMinExclusive(0)

--[[**
	ensures value is a number where value < 0

	@returns A function that will return true iff the condition is passed
**--]]
t.numberNegative = t.numberMaxExclusive(0)

--[[**
	ensures value is a number where min <= value <= max

	@param min The minimum to use
	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberConstrained(min, max)
	assert(t.number(min))
	assert(t.number(max))
	local minCheck = t.numberMin(min)
	local maxCheck = t.numberMax(max)

	return function(value)
		local minSuccess, minErrMsg = minCheck(value)
		if not minSuccess then
			return false, minErrMsg or ""
		end

		local maxSuccess, maxErrMsg = maxCheck(value)
		if not maxSuccess then
			return false, maxErrMsg or ""
		end

		return true
	end
end

--[[**
	ensures value is a number where min < value < max

	@param min The minimum to use
	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberConstrainedExclusive(min, max)
	assert(t.number(min))
	assert(t.number(max))
	local minCheck = t.numberMinExclusive(min)
	local maxCheck = t.numberMaxExclusive(max)

	return function(value)
		local minSuccess, minErrMsg = minCheck(value)
		if not minSuccess then
			return false, minErrMsg or ""
		end

		local maxSuccess, maxErrMsg = maxCheck(value)
		if not maxSuccess then
			return false, maxErrMsg or ""
		end

		return true
	end
end

--[[**
	ensures value matches string pattern

	@param string pattern to check against

	@returns A function that will return true iff the condition is passed
**--]]
function t.match(pattern)
	assert(t.string(pattern))
	return function(value)
		local stringSuccess, stringErrMsg = t.string(value)
		if not stringSuccess then
			return false, stringErrMsg
		end

		if string.match(value, pattern) == nil then
			return false, string.format("%q failed to match pattern %q", value, pattern)
		end

		return true
	end
end

--[[**
	ensures value is either nil or passes check

	@param check The check to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.optional(check)
	assert(t.callback(check))
	return function(value)
		if value == nil then
			return true
		end

		local success, errMsg = check(value)
		if success then
			return true
		else
			return false, string.format("(optional) %s", errMsg or "")
		end
	end
end

--[[**
	matches given tuple against tuple type definition

	@param ... The type definition for the tuples

	@returns A function that will return true iff the condition is passed
**--]]
function t.tuple(...)
	local checks = { ... }
	return function(...)
		local args = { ... }
		for i, check in ipairs(checks) do
			local success, errMsg = check(args[i])
			if success == false then
				return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "")
			end
		end

		return true
	end
end

--[[**
	ensures all keys in given table pass check

	@param check The function to use to check the keys

	@returns A function that will return true iff the condition is passed
**--]]
function t.keys(check)
	assert(t.callback(check))
	return function(value)
		local tableSuccess, tableErrMsg = t.table(value)
		if tableSuccess == false then
			return false, tableErrMsg or ""
		end

		for key in value do
			local success, errMsg = check(key)
			if success == false then
				return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "")
			end
		end

		return true
	end
end

--[[**
	ensures all values in given table pass check

	@param check The function to use to check the values

	@returns A function that will return true iff the condition is passed
**--]]
function t.values(check)
	assert(t.callback(check))
	return function(value)
		local tableSuccess, tableErrMsg = t.table(value)
		if tableSuccess == false then
			return false, tableErrMsg or ""
		end

		for key, val in value do
			local success, errMsg = check(val)
			if success == false then
				return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "")
			end
		end

		return true
	end
end

--[[**
	ensures value is a table and all keys pass keyCheck and all values pass valueCheck

	@param keyCheck The function to use to check the keys
	@param valueCheck The function to use to check the values

	@returns A function that will return true iff the condition is passed
**--]]
function t.map(keyCheck, valueCheck)
	assert(t.callback(keyCheck))
	assert(t.callback(valueCheck))
	local keyChecker = t.keys(keyCheck)
	local valueChecker = t.values(valueCheck)

	return function(value)
		local keySuccess, keyErr = keyChecker(value)
		if not keySuccess then
			return false, keyErr or ""
		end

		local valueSuccess, valueErr = valueChecker(value)
		if not valueSuccess then
			return false, valueErr or ""
		end

		return true
	end
end

--[[**
	ensures value is a table and all keys pass valueCheck and all values are true

	@param valueCheck The function to use to check the values

	@returns A function that will return true iff the condition is passed
**--]]
function t.set(valueCheck)
	return t.map(valueCheck, t.literal(true))
end

do
	local arrayKeysCheck = t.keys(t.integer)
--[[**
		ensures value is an array and all values of the array match check

		@param check The check to compare all values with

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.array(check)
		assert(t.callback(check))
		local valuesCheck = t.values(check)

		return function(value)
			local keySuccess, keyErrMsg = arrayKeysCheck(value)
			if keySuccess == false then
				return false, string.format("[array] %s", keyErrMsg or "")
			end

			-- # is unreliable for sparse arrays
			-- Count upwards using ipairs to avoid false positives from the behavior of #
			local arraySize = 0

			for _ in ipairs(value) do
				arraySize = arraySize + 1
			end

			for key in value do
				if key < 1 or key > arraySize then
					return false, string.format("[array] key %s must be sequential", tostring(key))
				end
			end

			local valueSuccess, valueErrMsg = valuesCheck(value)
			if not valueSuccess then
				return false, string.format("[array] %s", valueErrMsg or "")
			end

			return true
		end
	end

--[[**
		ensures value is an array of a strict makeup and size

		@param check The check to compare all values with

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.strictArray(...)
		local valueTypes = { ... }
		assert(t.array(t.callback)(valueTypes))

		return function(value)
			local keySuccess, keyErrMsg = arrayKeysCheck(value)
			if keySuccess == false then
				return false, string.format("[strictArray] %s", keyErrMsg or "")
			end

			-- If there's more than the set array size, disallow
			if #valueTypes < #value then
				return false, string.format("[strictArray] Array size exceeds limit of %d", #valueTypes)
			end

			for idx, typeFn in valueTypes do
				local typeSuccess, typeErrMsg = typeFn(value[idx])
				if not typeSuccess then
					return false, string.format("[strictArray] Array index #%d - %s", idx, typeErrMsg)
				end
			end

			return true
		end
	end
end

do
	local callbackArray = t.array(t.callback)
--[[**
		creates a union type

		@param ... The checks to union

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.union(...)
		local checks = { ... }
		assert(callbackArray(checks))

		return function(value)
			for _, check in ipairs(checks) do
				if check(value) then
					return true
				end
			end

			return false, "bad type for union"
		end
	end

--[[**
		Alias for t.union
	**--]]
	t.some = t.union

--[[**
		creates an intersection type

		@param ... The checks to intersect

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.intersection(...)
		local checks = { ... }
		assert(callbackArray(checks))

		return function(value)
			for _, check in ipairs(checks) do
				local success, errMsg = check(value)
				if not success then
					return false, errMsg or ""
				end
			end

			return true
		end
	end

--[[**
		Alias for t.intersection
	**--]]
	t.every = t.intersection
end

do
	local checkInterface = t.map(t.any, t.callback)
--[[**
		ensures value matches given interface definition

		@param checkTable The interface definition

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.interface(checkTable)
		assert(checkInterface(checkTable))
		return function(value)
			local tableSuccess, tableErrMsg = t.table(value)
			if tableSuccess == false then
				return false, tableErrMsg or ""
			end

			for key, check in pairs(checkTable) do
				local success, errMsg = check(value[key])
				if success == false then
					return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "")
				end
			end

			return true
		end
	end

--[[**
		ensures value matches given interface definition strictly

		@param checkTable The interface definition

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.strictInterface(checkTable)
		assert(checkInterface(checkTable))
		return function(value)
			local tableSuccess, tableErrMsg = t.table(value)
			if tableSuccess == false then
				return false, tableErrMsg or ""
			end

			for key, check in checkTable do
				local success, errMsg = check(value[key])
				if success == false then
					return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "")
				end
			end

			for key in value do
				if not checkTable[key] then
					return false, string.format("[interface] unexpected field %q", tostring(key))
				end
			end

			return true
		end
	end
end

--[[**
	ensure value is an Instance and it's ClassName matches the given ClassName

	@param className The class name to check for

	@returns A function that will return true iff the condition is passed
**--]]
function t.instanceOf(className, childTable)
	assert(t.string(className))

	local childrenCheck
	if childTable ~= nil then
		childrenCheck = t.children(childTable)
	end

	return function(value)
		local instanceSuccess, instanceErrMsg = t.Instance(value)
		if not instanceSuccess then
			return false, instanceErrMsg or ""
		end

		if value.ClassName ~= className then
			return false, string.format("%s expected, got %s", className, value.ClassName)
		end

		if childrenCheck then
			local childrenSuccess, childrenErrMsg = childrenCheck(value)
			if not childrenSuccess then
				return false, childrenErrMsg
			end
		end

		return true
	end
end

t.instance = t.instanceOf

--[[**
	ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison

	@param className The class name to check for

	@returns A function that will return true iff the condition is passed
**--]]
function t.instanceIsA(className, childTable)
	assert(t.string(className))

	local childrenCheck
	if childTable ~= nil then
		childrenCheck = t.children(childTable)
	end

	return function(value)
		local instanceSuccess, instanceErrMsg = t.Instance(value)
		if not instanceSuccess then
			return false, instanceErrMsg or ""
		end

		if not value:IsA(className) then
			return false, string.format("%s expected, got %s", className, value.ClassName)
		end

		if childrenCheck then
			local childrenSuccess, childrenErrMsg = childrenCheck(value)
			if not childrenSuccess then
				return false, childrenErrMsg
			end
		end

		return true
	end
end

--[[**
	ensures value is an enum of the correct type

	@param enum The enum to check

	@returns A function that will return true iff the condition is passed
**--]]
function t.enum(enum)
	assert(t.Enum(enum))
	return function(value)
		local enumItemSuccess, enumItemErrMsg = t.EnumItem(value)
		if not enumItemSuccess then
			return false, enumItemErrMsg
		end

		if value.EnumType == enum then
			return true
		else
			return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType))
		end
	end
end

do
	local checkWrap = t.tuple(t.callback, t.callback)

--[[**
		wraps a callback in an assert with checkArgs

		@param callback The function to wrap
		@param checkArgs The function to use to check arguments in the assert

		@returns A function that first asserts using checkArgs and then calls callback
	**--]]
	function t.wrap(callback, checkArgs)
		assert(checkWrap(callback, checkArgs))
		return function(...)
			assert(checkArgs(...))
			return callback(...)
		end
	end
end

--[[**
	asserts a given check

	@param check The function to wrap with an assert

	@returns A function that simply wraps the given check in an assert
**--]]
function t.strict(check)
	return function(...)
		assert(check(...))
	end
end

do
	local checkChildren = t.map(t.string, t.callback)

--[[**
		Takes a table where keys are child names and values are functions to check the children against.
		Pass an instance tree into the function.
		If at least one child passes each check, the overall check passes.

		Warning! If you pass in a tree with more than one child of the same name, this function will always return false

		@param checkTable The table to check against

		@returns A function that checks an instance tree
	**--]]
	function t.children(checkTable)
		assert(checkChildren(checkTable))

		return function(value)
			local instanceSuccess, instanceErrMsg = t.Instance(value)
			if not instanceSuccess then
				return false, instanceErrMsg or ""
			end

			local childrenByName = {}
			for _, child in ipairs(value:GetChildren()) do
				local name = child.Name
				if checkTable[name] then
					if childrenByName[name] then
						return false, string.format("Cannot process multiple children with the same name %q", name)
					end

					childrenByName[name] = child
				end
			end

			for name, check in checkTable do
				local success, errMsg = check(childrenByName[name])
				if not success then
					return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "")
				end
			end

			return true
		end
	end
end

return t


local FLAG_PATTERN = "%-%-(%w+)"
local FLAG_ALL_IN_ONE_PATTERN = `{FLAG_PATTERN}=(%w+)`

local function parseArgs(args: { string })
	local parsedArgs: { [string]: string | boolean | number } = {}

	local skipNextToken = false

	for index, token in args do
		-- When `--foo bar` is used, these are both individual tokens that we
		-- process at the same time. In those cases, we need to skip the next
		-- token (`bar`) since it has already been picked up as a flag value
		if skipNextToken then
			skipNextToken = false
			continue
		end

		-- handle `--foo=bar` pattern
		local flagName, flagValue = token:match(FLAG_ALL_IN_ONE_PATTERN)
		if flagName and flagValue then
			parsedArgs[flagName] = flagValue
			continue
		end

		flagName = token:match(FLAG_PATTERN)
		if flagName then
			local nextToken = args[index + 1]

			if nextToken then
				-- When processing `--foo` in `--foo --bar` treat it like a boolean
				if nextToken:match(FLAG_PATTERN) then
					parsedArgs[flagName] = true
				else
					parsedArgs[flagName] = nextToken
					skipNextToken = true
				end
			else
				parsedArgs[flagName] = true
			end
		else
			error(`something went wrong: {token}`)
		end
	end

	return parsedArgs
end

return parseArgs


local FlipbookBatteries = require("@luaupkg/flipbook-batteries")
local cli = require("@luaupkg/batteries/cli")
local fs = require("@std/fs")
local path = require("@std/path")
local pp = require("@luaupkg/batteries/pp")
local process = require("@std/process")
local richterm = require("@luaupkg/batteries/richterm")
local toml = require("@luaupkg/batteries/toml")

local buildSystem = require("@scripts/lib/build-system")
local createRojoProjectAsync = require("@scripts/lib/createRojoProjectAsync")
local getStudioPluginsPath = require("@scripts/lib/getStudioPluginsPath")
local project = require("@repo/project")

local copyInto = FlipbookBatteries.copyInto
local dotenv = FlipbookBatteries.dotenv
local run = FlipbookBatteries.run
local watch = FlipbookBatteries.watch

local WALLY_MANIFEST_PATH = path.resolve("wally.toml")

type BuildContext = buildSystem.BuildContext

dotenv()

local args = cli.parser()

args:add("script", "positional", {
	help = "Path to the script being run. Provided automatically by Lute",
})
args:add("subcommand", "positional", {
	help = `Can be "plugin" or "workspace"`,
	default = "plugin",
})

args:add("channel", "option", {
	help = "Channel to build for",
	aliases = { "c" },
	default = "prod",
})
args:add("target", "option", {
	help = "Target to build for",
	aliases = { "t" },
	default = "roblox",
})
args:add("output", "option", {
	help = "Full path to the rbxm file to build",
	aliases = { "o" },
	default = tostring(path.join(getStudioPluginsPath(), project.PLUGIN_FILENAME)),
})
args:add("watch", "flag", {
	help = "Watch for changes and recompile automatically",
	aliases = { "w" },
})
args:add("skip-reload", "flag", {
	help = "Skip reloading the plugin in Roblox Studio",
})
args:add("clean", "flag", {
	help = "Performs a full rebuild of the project",
})

args:parse({ ... })

local subcommand = args:get("subcommand")
assert(
	subcommand == "plugin" or subcommand == "workspace",
	`bad value for subcommand (must be one of "plugin" or "workspace", got "{subcommand}")`
)

local channel = args:get("channel")
assert(channel == "dev" or channel == "prod", `bad value for channel (must be one of "dev" or "prod", got "{channel}")`)

local target = args:get("target")
assert(
	target == "roblox" or target == "rotriever",
	`bad value for target (must be one of "roblox" or "rotriever", got "{target}")`
)

local output = args:get("output")
assert(typeof(output) == "string", `bad value for output (string expected, got {typeof(output)})`)

local function readWallyManifestAsync()
	return toml.deserialize(fs.readfiletostring(WALLY_MANIFEST_PATH))
end

local function getCommitHash()
	local commitHash = run("git", { "rev-parse", "--short", "HEAD" }, {
		captureOutput = true,
	})
	assert(commitHash ~= nil and commitHash ~= "", "commit hash is empty")
	return commitHash
end

-- This environment variable comes from `.env` and is required to be set. This
-- condition just makes sure if it's _not_ set that the user will then go and
-- get their `.env` file in order
if not process.env.BASE_URL then
	error(table.concat({
		"One or more critical environment variables are not set.",
		"Please make sure to copy `.env.template` to `.env`. If you already have a `.env` file, make sure the environment variables from the template match your local copy",
	}, "\n\n"))
end

local context: BuildContext = {
	channel = channel,
	target = target,
	shouldRebuild = args:has("clean"),
	dest = path.join(project.BUILD_PATH, channel, target),
	env = {
		BUILD_VERSION = (readWallyManifestAsync() :: any).package.version,
		BUILD_CHANNEL = if channel == "prod" then "production" else "development",
		BUILD_HASH = getCommitHash(),
		BASE_URL = process.env.BASE_URL,
		LOG_LEVEL = process.env.LOG_LEVEL,
	},
	cache = buildSystem.readBuildCacheAsync(),
}

local function buildPluginBinary()
	if subcommand ~= "plugin" then
		return
	end

	buildSystem.runBuildGroupAsync({
		name = "🔌 plugin binary",
		paths = { context.dest },
		context = context,
		step = function()
			local projectPath = path.resolve(path.join(context.dest, "../default.project.json"))

			createRojoProjectAsync(projectPath, {
				name = "Flipbook",
				tree = {
					["$path"] = tostring(path.resolve(context.dest)),
				},
			})

			local dest = path.resolve(path.join(context.dest, "..", project.PLUGIN_FILENAME))

			run("rojo", {
				"build",
				projectPath,
				"-o",
				dest,
			})

			fs.remove(projectPath)

			if args:has("skip-reload") then
				if output:match(tostring(getStudioPluginsPath())) then
					return
				end
			end

			copyInto(dest, output)
		end,
	})
end

local function build()
	local startTime = os.clock()

	local contextNoCache = table.clone(context)
	contextNoCache.cache = nil :: any
	print(richterm.dim(`build context: {pp(contextNoCache)}`))

	buildSystem.compileAsync(context)
	buildSystem.writeBuildCacheAsync(context.cache)

	buildPluginBinary()

	print(`build completed in {("%.2f"):format(os.clock() - startTime)}s`)
end

build()

if args:has("watch") then
	local function onChanged(filePath: path.path)
		if tostring(filePath):match(tostring(project.WORKSPACE_PATH)) then
			local memberPath = tostring(filePath):match(`({project.WORKSPACE_PATH}/[%w%-]+)/`)
			buildSystem.compileWorkspaceMemberAsync(memberPath, context)
			buildSystem.writeBuildCacheAsync(context.cache)

			buildPluginBinary()
		else
			build()
		end
	end

	watch({
		roots = {
			tostring(project.SOURCE_PATH),
			tostring(project.WORKSPACE_PATH),
		},
		excludedFilePatterns = {
			".*Packages",
			"build",
		},
		onChanged = onChanged,
	})
end