--!optimize 2
--!strict

local RunService = game:GetService("RunService")

export type Trove = {
	Extend: (self: Trove) -> Trove,
	Clone: <T>(self: Trove, instance: T & Instance) -> T,
	Construct: <T, A...>(self: Trove, class: Constructable<T, A...>, A...) -> T,
	Connect: (self: Trove, signal: SignalLike | RBXScriptSignal, fn: (...any) -> ...any) -> ConnectionLike,
	BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	AddPromise: <T>(self: Trove, promise: T & PromiseLike) -> T,
	Add: <T>(self: Trove, object: T & Trackable, cleanupMethod: string?) -> T,
	Remove: <T>(self: Trove, object: T & Trackable) -> boolean,
	Clean: (self: Trove) -> (),
	WrapClean: (self: Trove) -> () -> (),
	AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection,
	Destroy: (self: Trove) -> (),
}

type TroveInternal = Trove & {
	_objects: {any},
	_cleaning: boolean,
	_findAndRemoveFromObjects: (self: TroveInternal, object: any, cleanup: boolean) -> boolean,
	_cleanupObject: (self: TroveInternal, object: any, cleanupMethod: string?) -> (),
}

export type Trackable =
	Instance
	| RBXScriptConnection
	| ConnectionLike
	| PromiseLike
	| thread
	| ((...any) -> ...any)
	| Destroyable
	| DestroyableLowercase
	| Disconnectable
	| DisconnectableLowercase

type ConnectionLike = {
	Connected: boolean,
	Disconnect: (self: ConnectionLike) -> (),
}

type SignalLike = {
	Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
	Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
}

type PromiseLike = {
	getStatus: (self: PromiseLike) -> string,
	finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike,
	cancel: (self: PromiseLike) -> (),
}

type Constructable<T, A...> = {new: (A...) -> T} | (A...) -> T

type Destroyable = {
	Destroy: (self: Destroyable) -> (),
}

type DestroyableLowercase = {
	destroy: (self: DestroyableLowercase) -> (),
}

type Disconnectable = {
	Disconnect: (self: Disconnectable) -> (),
}

type DisconnectableLowercase = {
	disconnect: (self: DisconnectableLowercase) -> (),
}

local FN_MARKER = newproxy()
local THREAD_MARKER = newproxy()
local GENERIC_OBJECT_CLEANUP_METHODS = table.freeze({"Destroy", "Disconnect", "destroy", "disconnect"})

local function GetObjectCleanupFunction(object: any, cleanupMethod: string?)
	local t = typeof(object)

	if t == "function" then
		return FN_MARKER
	elseif t == "thread" then
		return THREAD_MARKER
	end

	if cleanupMethod then
		return cleanupMethod
	end

	if t == "Instance" then
		return "Destroy"
	elseif t == "RBXScriptConnection" then
		return "Disconnect"
	elseif t == "table" then
		for _, genericCleanupMethod in GENERIC_OBJECT_CLEANUP_METHODS do
			if typeof(object[genericCleanupMethod]) == "function" then
				return genericCleanupMethod
			end
		end
	end

	error(`failed to get cleanup function for object {t}: {object}`, 3)
end

local function AssertPromiseLike(object: any)
	if
		typeof(object) ~= "table"
		or typeof(object.getStatus) ~= "function"
		or typeof(object.finally) ~= "function"
		or typeof(object.cancel) ~= "function"
	then
		error("did not receive a promise as an argument", 3)
	end
end

local Trove = {}
Trove.__index = Trove

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

	self._objects = {}
	self._cleaning = false

	return (self :: any) :: Trove
end

function Trove.Add(self: TroveInternal, object: Trackable, cleanupMethod: string?): any
	if self._cleaning then
		error("cannot call trove:Add() while cleaning", 2)
	end

	local cleanup = GetObjectCleanupFunction(object, cleanupMethod)
	table.insert(self._objects, {object, cleanup})

	return object
end

function Trove.Clone(self: TroveInternal, instance: Instance): Instance
	if self._cleaning then
		error("cannot call trove:Clone() while cleaning", 2)
	end

	return self:Add(instance:Clone())
end

function Trove.Construct<T, A...>(self: TroveInternal, class: Constructable<T, A...>, ...: A...)
	if self._cleaning then
		error("Cannot call trove:Construct() while cleaning", 2)
	end

	local object = nil
	local t = type(class)
	if t == "table" then
		object = (class :: any).new(...)
	elseif t == "function" then
		object = (class :: any)(...)
	end

	return self:Add(object)
end

function Trove.Connect(self: TroveInternal, signal: SignalLike, fn: (...any) -> ...any)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end

	return self:Add(signal:Connect(fn))
end

function Trove.BindToRenderStep(self: TroveInternal, name: string, priority: number, fn: (dt: number) -> ())
	if self._cleaning then
		error("cannot call trove:BindToRenderStep() while cleaning", 2)
	end

	RunService:BindToRenderStep(name, priority, fn)

	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

function Trove.AddPromise(self: TroveInternal, promise: PromiseLike)
	if self._cleaning then
		error("cannot call trove:AddPromise() while cleaning", 2)
	end
	AssertPromiseLike(promise)

	if promise:getStatus() == "Started" then
		promise:finally(function()
			if self._cleaning then
				return
			end
			self:_findAndRemoveFromObjects(promise, false)
		end)

		self:Add(promise, "cancel")
	end

	return promise
end

function Trove.Remove(self: TroveInternal, object: Trackable): boolean
	if self._cleaning then
		error("cannot call trove:Remove() while cleaning", 2)
	end

	return self:_findAndRemoveFromObjects(object, true)
end

function Trove.Extend(self: TroveInternal)
	if self._cleaning then
		error("cannot call trove:Extend() while cleaning", 2)
	end

	return self:Construct(Trove)
end

function Trove.Clean(self: TroveInternal)
	if self._cleaning then
		return
	end

	self._cleaning = true

	for _, obj in self._objects do
		self:_cleanupObject(obj[1], obj[2])
	end

	table.clear(self._objects)
	self._cleaning = false
end

function Trove.WrapClean(self: TroveInternal)
	return function()
		self:Clean()
	end
end

function Trove._findAndRemoveFromObjects(self: TroveInternal, object: any, cleanup: boolean): boolean
	local objects = self._objects

	for i, obj in objects do
		if obj[1] == object then
			local n = #objects
			objects[i] = objects[n]
			objects[n] = nil

			if cleanup then
				self:_cleanupObject(obj[1], obj[2])
			end

			return true
		end
	end

	return false
end

function Trove._cleanupObject(_self: TroveInternal, object: any, cleanupMethod: string?)
	if cleanupMethod == FN_MARKER then
		task.spawn(object)
	elseif cleanupMethod == THREAD_MARKER then
		pcall(task.cancel, object)
	else
		object[cleanupMethod](object)
	end
end

function Trove.AttachToInstance(self: TroveInternal, instance: Instance)
	if self._cleaning then
		error("cannot call trove:AttachToInstance() while cleaning", 2)
	elseif not instance:IsDescendantOf(game) then
		error("instance is not a descendant of the game hierarchy", 2)
	end

	return self:Connect(instance.Destroying, function()
		self:Destroy()
	end)
end

function Trove.Destroy(self: TroveInternal)
	self:Clean()
end

return {
	new = Trove.new;
}


--!optimize 2
--!strict

local Duster = require(script.Duster)
local Janitor17 = require(script.Janitor17)
local Janitor18 = require(script.Janitor18)
local Maid = require(script.Maid)
local Trove = require(script.Trove)
local UltraJanitor18 = require(script.UltraJanitor18)

type IProfiler = {
	Begin: (label: string) -> (),
	End: () -> (),

	Start: (label: string) -> (),
	Stop: () -> (),

	Open: (label: string) -> (),
	Close: () -> (),

	Enter: (label: string) -> (),
	Exit: () -> (),

	begin: (label: string) -> (),
	["end"]: () -> (), -- what

	start: (label: string) -> (),
	stop: () -> (),

	open: (label: string) -> (),
	close: () -> (),

	enter: (label: string) -> (),
	exit: () -> (),
}

type IBenchmark<T... = ...nil> = {
	ParameterGenerator: () -> T...?,
	Functions: {[string]: (Profiler: IProfiler, T...) -> ()},
}

local function CreateBenchmark<T...>(
	ParameterGenerator: () -> T...?,
	Functions: {[string]: (Profiler: IProfiler, T...) -> ()}
): IBenchmark<T...>
	return {
		ParameterGenerator = ParameterGenerator;
		Functions = Functions;
	}
end

type BasicClass = {
	CleanupFunction: nil | () -> (),
	AddCleanupFunction: (self: BasicClass, callback: nil | () -> ()) -> BasicClass,
	Destroy: (self: BasicClass) -> (),
}
type Static = {
	ClassName: "BasicClass",
	new: () -> BasicClass,
}
local BasicClass = {} :: BasicClass & Static
BasicClass.ClassName = "BasicClass";
(BasicClass :: any).__index = BasicClass
function BasicClass.new(): BasicClass
	return setmetatable({
		CleanupFunction = nil;
	}, BasicClass) :: never
end
function BasicClass:AddCleanupFunction(callback: nil | () -> ()): BasicClass
	self.CleanupFunction = callback
	return self
end
function BasicClass:Destroy(): ()
	local cleanupFunction = self.CleanupFunction
	if cleanupFunction then
		cleanupFunction()
	end
	table.clear(self)
	setmetatable(self, nil)
end

local function NoOperation(): () end

local DIVIDE_BY = 550

local functionsToAdd = 1_000_000 // DIVIDE_BY
local threadsToAdd = 200_000 // DIVIDE_BY
local classesToAdd = 1_000_000 // DIVIDE_BY
local instancesToAdd = 100_000 // DIVIDE_BY

local Add = UltraJanitor18.new().Add
local Cleanup = UltraJanitor18.new().Cleanup

local Benchmark = CreateBenchmark(function()
	return
end, {
	["Janitor 1.17.0"] = function()
		local janitor = Janitor17.new()

		local functionsJanitor = janitor:Add(Janitor17.new(), "Cleanup")
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation, true)
		end
		local threadsJanitor = janitor:Add(Janitor17.new(), "Cleanup")
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation), true)
		end
		local classesJanitor = janitor:Add(Janitor17.new(), "Cleanup")
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy")
		end
		local instancesJanitor = janitor:Add(Janitor17.new(), "Cleanup")
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy")
		end

		janitor:Destroy()
	end;

	["Janitor 1.18.2"] = function()
		local janitor = Janitor18.new()

		local functionsJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation, true)
		end
		local threadsJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation), true)
		end
		local classesJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy")
		end
		local instancesJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy")
		end

		janitor:Cleanup()
	end;

	["Ultra Janitor 1.18.2"] = function()
		local janitor = UltraJanitor18.new()

		local functionsJanitor = Add(janitor, UltraJanitor18.new(), "Cleanup")
		for _ = 1, functionsToAdd do
			Add(functionsJanitor, NoOperation, true)
		end
		local threadsJanitor = Add(janitor, UltraJanitor18.new(), "Cleanup")
		for _ = 1, threadsToAdd do
			Add(threadsJanitor, task.delay(5, NoOperation), true)
		end
		local classesJanitor = Add(janitor, UltraJanitor18.new(), "Cleanup")
		for _ = 1, classesToAdd do
			Add(classesJanitor, BasicClass.new(), "Destroy")
		end
		local instancesJanitor = Add(janitor, UltraJanitor18.new(), "Cleanup")
		for _ = 1, instancesToAdd do
			Add(instancesJanitor, Instance.new("Folder"), "Destroy")
		end

		Cleanup(janitor)
	end;

	["Janitor 1.18.2 Unsafe"] = function()
		local janitor = Janitor18.new()
		janitor.UnsafeThreadCleanup = true

		local functionsJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation, true)
		end
		local threadsJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation), true)
		end
		local classesJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy")
		end
		local instancesJanitor = janitor:Add(Janitor18.new(), "Cleanup")
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy")
		end

		janitor:Cleanup()
	end;

	-- Lesser beings
	["Trove 1.5.0"] = function()
		local trove = Trove.new()

		local functionsJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation)
		end
		local threadsJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation))
		end
		local classesJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy")
		end
		local instancesJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy")
		end

		trove:Destroy()
	end;

	["Maid"] = function()
		local maid = Maid.new()

		local functionsJanitor = maid:Add(Maid.new())
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation)
		end
		local threadsJanitor = maid:Add(Maid.new())
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation))
		end
		local classesJanitor = maid:Add(Maid.new())
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new())
		end
		local instancesJanitor = maid:Add(Maid.new())
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"))
		end

		maid:Destroy()
	end;

	["Duster"] = function()
		local duster: {Duster.Cleanable} = {}

		local functionsJanitor: {Duster.Cleanable} = {}
		Duster.insert(duster, function()
			Duster.clear(functionsJanitor)
		end)
		for _ = 1, functionsToAdd do
			Duster.insert(functionsJanitor, NoOperation)
		end

		local threadsJanitor: {Duster.Cleanable} = {}
		Duster.insert(duster, function()
			Duster.clear(threadsJanitor)
		end)
		for _ = 1, threadsToAdd do
			Duster.insert(threadsJanitor, task.delay(5, NoOperation))
		end

		local classesJanitor: {Duster.Cleanable} = {}
		Duster.insert(duster, function()
			Duster.clear(classesJanitor)
		end)
		for _ = 1, classesToAdd do
			Duster.insert(classesJanitor, BasicClass.new() :: never)
		end
		local instancesJanitor: {Duster.Cleanable} = {}
		Duster.insert(duster, function()
			Duster.clear(instancesJanitor)
		end)
		for _ = 1, instancesToAdd do
			Duster.insert(instancesJanitor, Instance.new("Folder"))
		end

		Duster.clear(duster)
	end;
})

return Benchmark


--!optimize 2
--!strict

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

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

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

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

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

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

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

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

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

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

	[any]: BooleanOrString,

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

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

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

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

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

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

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

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

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

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

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

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

Janitor.instanceof = Janitor.Is

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

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

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

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

			self[object] = nil
		end

		this[index] = nil
	end

	return self
end

type BooleanOrString = boolean | string

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

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

		this[index] = object
	end

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

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

	self[object] = newMethodName
	return object
end

Private.Add = Add

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

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

Janitor.Get = Get

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

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

	if promiseObject:getStatus() ~= Promise.Status.Started then
		return promiseObject
	end

	local uniqueId = index
	if uniqueId == nil then
		uniqueId = newproxy(false)
	end

	local newPromise = Add(self, Promise.new(function(resolve, _, onCancel)
		if onCancel(function()
			promiseObject:cancel()
		end) then
			return
		end

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

	newPromise:finally(function()
		if Get(self, uniqueId) == newPromise then
			Remove(self, uniqueId)
		end
	end)

	return newPromise :: never
end

Private.Remove = Remove

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

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

	return self
end

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

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

	return self
end

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

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

	return self
end

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

local function Cleanup(self: Private): ()
	if not self.CurrentlyCleaning then
		local suppressInstanceReDestroy = self.SuppressInstanceReDestroy
		local unsafeThreadCleanup = self.UnsafeThreadCleanup

		self.CurrentlyCleaning = nil :: never
		self.SuppressInstanceReDestroy = nil :: never
		self.UnsafeThreadCleanup = nil :: never

		local object, methodName = next(self)

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

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

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

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

		self.CurrentlyCleaning = false
		self.SuppressInstanceReDestroy = suppressInstanceReDestroy
		self.UnsafeThreadCleanup = unsafeThreadCleanup
	end
end

Private.Cleanup = Cleanup

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

Private.__call = Cleanup

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

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

Private.LinkToInstance = LinkToInstance;

(Janitor :: never).LegacyLinkToInstance = LinkToInstance

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

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

	return manualCleanup
end

function Private:__tostring()
	return "Janitor"
end

return Janitor :: Static


--!optimize 2
--!strict

local RunService = game:GetService("RunService")
local Janitor = require(script.Parent.Janitor18)
local Promise = require(script.Promise)

type ConnectionLike = {
	Connected: boolean,
	Disconnect: (self: ConnectionLike) -> (),
}
type SignalLike = {
	Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
	Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
}
type PromiseLike = {
	getStatus: (self: PromiseLike) -> string,
	finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike,
	cancel: (self: PromiseLike) -> (),
}
type Constructable<T, A...> = {new: (A...) -> T} | (A...) -> T
type Destroyable = {
	Destroy: (self: Destroyable) -> (),
}
type DestroyableLowercase = {
	destroy: (self: DestroyableLowercase) -> (),
}
type Disconnectable = {
	Disconnect: (self: Disconnectable) -> (),
}
type DisconnectableLowercase = {
	disconnect: (self: DisconnectableLowercase) -> (),
}

export type Trackable =
	Instance
	| RBXScriptConnection
	| ConnectionLike
	| Promise.TypedPromise<...any>
	| thread
	| ((...any) -> ...any)
	| Destroyable
	| DestroyableLowercase
	| Disconnectable
	| DisconnectableLowercase

export type Trove = {
	Add: <T>(self: Trove, object: T, cleanupMethod: string?) -> T,
	AddPromise: <T...>(self: Trove, promise: Promise.TypedPromise<T...>) -> Promise.TypedPromise<T...>,
	AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection,
	BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	Clean: (self: Trove) -> (),
	Clone: <T>(self: Trove, instance: T & Instance) -> T,
	Connect: (self: Trove, signal: SignalLike | RBXScriptSignal, fn: (...any) -> ...any) -> ConnectionLike,
	Construct: <T, A...>(self: Trove, class: Constructable<T, A...>, A...) -> T,
	Extend: (self: Trove) -> Trove,
	Remove: (self: Trove, object: Trackable) -> boolean,
	WrapClean: (self: Trove) -> () -> (),
	Destroy: (self: Trove) -> (),
}
type Private = {
	Janitor: Janitor.Janitor,

	Add: <T>(self: Private, object: T, cleanupMethod: string?) -> T,
	AddPromise: <T...>(self: Private, promise: Promise.TypedPromise<T...>) -> Promise.TypedPromise<T...>,
	AttachToInstance: (self: Private, instance: Instance) -> RBXScriptConnection,
	BindToRenderStep: (self: Private, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	Clean: (self: Private) -> (),
	Clone: <T>(self: Private, instance: T & Instance) -> T,
	Connect: (self: Private, signal: SignalLike | RBXScriptSignal, fn: (...any) -> ...any) -> ConnectionLike,
	Construct: <T, A...>(self: Private, class: Constructable<T, A...>, A...) -> T,
	Extend: (self: Private) -> Private,
	Remove: (self: Private, object: Trackable) -> boolean,
	WrapClean: (self: Private) -> () -> (),
	Destroy: (self: Private) -> (),
}
type Static = {
	ClassName: "Trove",
	new: () -> Trove,
}

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

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

function Private:Add<T>(object: T, cleanupMethod): T
	return self.Janitor:Add(object, cleanupMethod, object)
end

function Private:AddPromise<T...>(object)
	return self.Janitor:AddPromise(object)
end

function Private:AttachToInstance(object)
	if not object:IsDescendantOf(game) then
		error("instance is not a descendant of the game hierarchy", 2)
	end

	return self.Janitor:LinkToInstance(object, false)
end

function Private:BindToRenderStep(name, priority, callback)
	RunService:BindToRenderStep(name, priority, callback)
	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

function Private:Clean()
	self.Janitor:Cleanup()
end

function Private:Clone<T>(instance)
	local object = instance:Clone()
	return self.Janitor:Add(object, "Destroy", object)
end

function Private:Connect(signal, callback)
	return self:Add((signal :: any):Connect(callback))
end

function Private:Construct<T, A...>(class, ...)
	local object = nil
	local t = type(class)
	if t == "table" then
		object = (class :: any).new(...)
	elseif t == "function" then
		object = (class :: any)(...)
	end

	return self:Add(object)
end

function Private:Extend()
	return self:Add(Trove.new()) :: never
end

function Private:Remove(object)
	if self.Janitor:Get(object) == nil then
		return false
	end

	self.Janitor:Remove(object)
	return self.Janitor:Get(object) == nil
end

function Private:Destroy()
	self:Clean()
end

function Private:WrapClean()
	return function()
		self:Clean()
	end
end

return table.freeze(Trove :: Static)


--!optimize 2
--!strict

-- for reference, unfair in this case means unfair to JANITOR.

local Janitor17 = require(script.Janitor17)
local Janitor18 = require(script.Janitor18)
local Maid = require(script.Maid)
local Trove = require(script.Trove)
local Troveitor = require(script.Troveitor)

type IProfiler = {
	Begin: (label: string) -> (),
	End: () -> (),

	Start: (label: string) -> (),
	Stop: () -> (),

	Open: (label: string) -> (),
	Close: () -> (),

	Enter: (label: string) -> (),
	Exit: () -> (),

	begin: (label: string) -> (),
	["end"]: () -> (), -- what

	start: (label: string) -> (),
	stop: () -> (),

	open: (label: string) -> (),
	close: () -> (),

	enter: (label: string) -> (),
	exit: () -> (),
}

type IBenchmark<T... = ...nil> = {
	ParameterGenerator: () -> T...?,
	Functions: {[string]: (Profiler: IProfiler, T...) -> ()},
}

local function CreateBenchmark<T...>(
	ParameterGenerator: () -> T...?,
	Functions: {[string]: (Profiler: IProfiler, T...) -> ()}
): IBenchmark<T...>
	return {
		ParameterGenerator = ParameterGenerator;
		Functions = Functions;
	}
end

type BasicClass = {
	CleanupFunction: nil | () -> (),
	AddCleanupFunction: (self: BasicClass, callback: nil | () -> ()) -> BasicClass,
	Destroy: (self: BasicClass) -> (),
}
type Static = {
	ClassName: "BasicClass",
	new: () -> BasicClass,
}
local BasicClass = {} :: BasicClass & Static
BasicClass.ClassName = "BasicClass";
(BasicClass :: any).__index = BasicClass
function BasicClass.new(): BasicClass
	return setmetatable({
		CleanupFunction = nil;
	}, BasicClass) :: never
end
function BasicClass:AddCleanupFunction(callback: nil | () -> ()): BasicClass
	self.CleanupFunction = callback
	return self
end
function BasicClass:Destroy(): ()
	local cleanupFunction = self.CleanupFunction
	if cleanupFunction then
		cleanupFunction()
	end
	table.clear(self)
	setmetatable(self, nil)
end

local function NoOperation(): () end

local functionsToAdd = 1_000_000 // 500
local threadsToAdd = 200_000 // 500
local classesToAdd = 1_000_000 // 500
local instancesToAdd = 100_000 // 500

local Benchmark = CreateBenchmark(function()
	return
end, {
	["Janitor 1.17.0"] = function()
		local janitor = Janitor17.new()

		local functionsJanitor = janitor:Add(Janitor17.new(), "Cleanup", "Functions")
		for index = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation, true, index)
		end
		local threadsJanitor = janitor:Add(Janitor17.new(), "Cleanup", "Threads")
		for index = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation), true, index)
		end
		local classesJanitor = janitor:Add(Janitor17.new(), "Cleanup", "Classes")
		for index = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy", index)
		end
		local instancesJanitor = janitor:Add(Janitor17.new(), "Cleanup", "Instances")
		for index = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy", index)
		end

		janitor:Destroy()
	end;

	["Janitor 1.18.0"] = function()
		local janitor = Janitor18.new()

		local functionsJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Functions")
		for index = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation, true, index)
		end
		local threadsJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Threads")
		for index = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation), true, index)
		end
		local classesJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Classes")
		for index = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy", index)
		end
		local instancesJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Instances")
		for index = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy", index)
		end

		janitor:Destroy()
	end;

	["Janitor 1.18.0 Unsafe"] = function()
		local janitor = Janitor18.new()
		janitor.UnsafeThreadCleanup = true

		local functionsJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Functions")
		for index = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation, true, index)
		end
		local threadsJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Threads")
		for index = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation), true, index)
		end
		local classesJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Classes")
		for index = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy", index)
		end
		local instancesJanitor = janitor:Add(Janitor18.new(), "Cleanup", "Instances")
		for index = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy", index)
		end

		janitor:Destroy()
	end;

	["Troveitor"] = function()
		local trove = Troveitor.new()

		local functionsJanitor = trove:Add(Troveitor.new(), "Clean")
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation)
		end
		local threadsJanitor = trove:Add(Troveitor.new(), "Clean")
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation))
		end
		local classesJanitor = trove:Add(Troveitor.new(), "Clean")
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy")
		end
		local instancesJanitor = trove:Add(Troveitor.new(), "Clean")
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy")
		end

		trove:Destroy()
	end;

	-- Lesser beings
	["Trove 1.5.0"] = function()
		local trove = Trove.new()

		local functionsJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation)
		end
		local threadsJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation))
		end
		local classesJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new(), "Destroy")
		end
		local instancesJanitor = trove:Add(Trove.new(), "Clean")
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"), "Destroy")
		end

		trove:Destroy()
	end;

	["Maid"] = function()
		local maid = Maid.new()

		local functionsJanitor = maid:Add(Maid.new())
		for _ = 1, functionsToAdd do
			functionsJanitor:Add(NoOperation)
		end
		local threadsJanitor = maid:Add(Maid.new())
		for _ = 1, threadsToAdd do
			threadsJanitor:Add(task.delay(5, NoOperation))
		end
		local classesJanitor = maid:Add(Maid.new())
		for _ = 1, classesToAdd do
			classesJanitor:Add(BasicClass.new())
		end
		local instancesJanitor = maid:Add(Maid.new())
		for _ = 1, instancesToAdd do
			instancesJanitor:Add(Instance.new("Folder"))
		end

		maid:Destroy()
	end;
})

return Benchmark


--!optimize 2
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Jest = require(ReplicatedStorage.DevPackages.Jest)

local processServiceExists, ProcessService = pcall(function()
	local s = "ProcessService"
	return game:GetService(s)
end)

local status, result = Jest.runCLI(ReplicatedStorage.Janitor, {
	all = true;
	ci = false;
	verbose = true;
}, {ReplicatedStorage.Janitor}):awaitStatus()

if status == "Rejected" then
	print(result)
end

if status == "Resolved" and result.results.numFailedTestSuites == 0 and result.results.numFailedTests == 0 then
	if processServiceExists then
		ProcessService:ExitAsync(0)
	end
end

if processServiceExists then
	ProcessService:ExitAsync(1)
end

return nil


local fs = require("@lune/fs")
local log = require("./libs/log")
local process = require("@lune/process")

type Archs = | "aarch64" | "x86_64"

type Platforms = | "macos" | "linux" | "windows"

local EXE_NAME = "blink"

local RELEASE_DIR = "release"
local PLUGIN_SOURCE_DIR = `plugin/src`
local PLUGIN_RELEASE_DIR = `{RELEASE_DIR}/plugin`

local CLI_PATH = "src/CLI/init.luau"
local BUNDLE_PATH = `{RELEASE_DIR}/blink.luau`
local PESDE_RELEASE_PATH = `{RELEASE_DIR}/blink_pesde.luau`

local BUNDLE_CONFIG = "build/.darklua.json"
local PLUGIN_ROJO_CONFIG = "build/plugin.project.json"

local function build_executable(platform: Platforms, arch: Archs)
	local target = `{platform}-{arch}`
	local exe_name = `{EXE_NAME}{platform == "windows" and ".exe" or ""}`
	local exe_path = `{RELEASE_DIR}/{exe_name}`
	local build_result = process.exec("lune", {
		"build",
		BUNDLE_PATH,
		"--output",
		exe_path,
		"--target",
		target,
	}, { stdio = "forward" })
	assert(build_result.ok, build_result.stderr)

	local archive_result: process.ExecResult
	if platform == "windows" then
		archive_result = process.exec("tar", {
			"-czvf",
			`{RELEASE_DIR}/blink-{target}.tar.gz`,
			"-C",
			RELEASE_DIR,
			exe_name,
		}, { stdio = "forward" })
	else
		archive_result = process.exec("tar", {
			"-cJvf",
			`{RELEASE_DIR}/blink-{target}.tar.xz`,
			"-C",
			RELEASE_DIR,
			exe_name,
		}, { stdio = "forward" })
	end

	assert(archive_result.ok, archive_result.stderr)

	fs.removeFile(exe_path)
	log(`Built executable for {target}`)
end

local function build_pesde()
	local darklua_result =
		process.exec("darklua", { "process", "--config", BUNDLE_CONFIG, CLI_PATH, PESDE_RELEASE_PATH })
	assert(darklua_result.ok, darklua_result.stderr)
	log("Build pesde source")
end

local function build_executables()
	local bundle_result = process.exec("darklua", { "process", "--config", BUNDLE_CONFIG, CLI_PATH, BUNDLE_PATH })
	assert(bundle_result.ok, bundle_result.stderr)
	log("Bundled CLI source code")

	build_executable("macos", "x86_64")
	build_executable("macos", "aarch64")
	build_executable("linux", "x86_64")
	build_executable("linux", "aarch64")
	build_executable("windows", "x86_64")

	fs.removeFile(BUNDLE_PATH)
end

local function build_studio_plugin()
	fs.writeDir(PLUGIN_RELEASE_DIR)

	for _, filename in fs.readDir(PLUGIN_SOURCE_DIR) do
		if string.split(filename, ".")[2] == "rbxmx" then
			fs.copy(`{PLUGIN_SOURCE_DIR}/{filename}`, `{PLUGIN_RELEASE_DIR}/{filename}`)
		end
	end

	local bundle_result = process.exec("darklua", {
		"process",
		"--config",
		BUNDLE_CONFIG,
		`{PLUGIN_SOURCE_DIR}/init.server.luau`,
		`{PLUGIN_RELEASE_DIR}/init.server.luau`,
	})
	assert(bundle_result.ok, bundle_result.stderr)

	local rojo_result = process.exec("rojo", {
		"build",
		PLUGIN_ROJO_CONFIG,
		"--output",
		`{RELEASE_DIR}/blink-plugin.rbxm`,
	})
	assert(rojo_result.ok, rojo_result.stderr)

	log("Built studio plugin")
	fs.removeDir(PLUGIN_RELEASE_DIR)
end

do
	if fs.isDir(RELEASE_DIR) then
		fs.removeDir(RELEASE_DIR)
	end
	fs.writeDir(RELEASE_DIR)
end

local start = os.clock()
build_pesde()
build_executables()
build_studio_plugin()

local elapsed = (os.clock() - start)
print(string.format("Finished build in %.2f seconds", elapsed))


local darklua_version = require("./libs/darklua_version")
local pesde_version = require("./libs/pesde_version")
local stdio = require("@lune/stdio")

local PESDE_CONFIG = "pesde.toml"
local DARKLUA_CONFIG = "build/.darklua.json"

local curr_version = darklua_version.read_from(DARKLUA_CONFIG)
local new_version = stdio.prompt("text" :: "text", `What version is being built? (Current version is {curr_version})`)
darklua_version.write_to(DARKLUA_CONFIG, new_version)
pesde_version.write_to(PESDE_CONFIG, new_version)


local fs = require("@lune/fs")
local serde = require("@lune/serde")
local log = require("./log")

type DarkluaRules =
    | "inject_global_value"
    | "N/A"

type DarkluaConfig = {
    rules: {
        DarkluaRules | {
            rule: DarkluaRules,
            identifier: string,
            value: any
        }
    }
}

local function read_version(config_path: string): string
    local contents = fs.readFile(config_path)
    local config = serde.decode("json", contents) :: DarkluaConfig

    for _, rule in config.rules do
        if type(rule) == "string" then
            continue
        end

        if rule.rule ~= "inject_global_value" then
            continue
        end

        if rule.identifier == "VERSION" then
            return rule.value
        end
    end

    return "?"
end

local function write_version(config_path: string, version: string)
    local contents = fs.readFile(config_path)
    local config = serde.decode("json", contents) :: DarkluaConfig

    for _, rule in config.rules do
        if type(rule) == "string" then
            continue
        end

        if rule.rule ~= "inject_global_value" then
            continue
        end

        if rule.identifier == "VERSION" then
            rule.value = version
        end
    end

    contents = serde.encode("json", config, true)
    fs.writeFile(config_path, contents)
    log(`Updated darklua config version to {version}`)
end

return table.freeze({
    read_from = read_version,
    write_to = write_version
})


local fs = require("@lune/fs")
local log = require("./log")
local serde = require("@lune/serde")

type PesdeConfig = {
	version: string,
}

local function read_version(config_path: string): string
	local contents = fs.readFile(config_path)
	local config = serde.decode("toml", contents) :: PesdeConfig
	return config.version or "?"
end

local function write_version(config_path: string, version: string)
	local contents = fs.readFile(config_path)
	local config = serde.decode("toml", contents) :: PesdeConfig

	config.version = version
	contents = serde.encode("toml", config, true)
	fs.writeFile(config_path, contents)

	log(`Updated pesde config version to {version}`)
end

return table.freeze({
	read_from = read_version,
	write_to = write_version,
})


local RunService = game:GetService("RunService")
if not RunService:IsRunning() then
	print("Press F5 to start the benchmark.")
	while task.wait(9e9) do
	end
end

local Result: StringValue
while Result == nil do
	Result = game:FindFirstChild("Result")
	task.wait(1)
end

print("--RESULTS JSON--")
print(Result.Value)


local Benches = require("./shared/benches")
local Modes = require("./shared/modes")

local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local StarterGui = game:GetService("StarterGui")
local Stats = game:GetService("Stats")

local MAXIMUM_FRAMERATE = 60

local Camera = workspace.CurrentCamera
Camera.FieldOfView = 1
Camera.CFrame = CFrame.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

Players.LocalPlayer.PlayerGui:ClearAllChildren()
Players.LocalPlayer.PlayerScripts:ClearAllChildren()

for _, Item in Enum.CoreGuiType:GetEnumItems() do
	while true do
		local Success = pcall(function()
			StarterGui:SetCoreGuiEnabled(Item, false)
		end)

		if Success then
			break
		end
	end
end

type Benchmark = {
	Sent: number,
	Recieve: number,
	Bandwidth: { number },
	Framerate: { number },
}

type Results = {
	[string]: {
		[string]: Benchmark,
	},
}

local function Percentile(Samples: { number }, Percentile: number)
	assert(
		(Percentile // 1) == Percentile and Percentile >= 0 and Percentile <= 100,
		"Percentile must be an integer between 0 and 100"
	)
	local Index = ((#Samples * (Percentile / 100)) // 1)
	Index = math.max(Index, 1)
	return Samples[Index]
end

local function WaitForPacketsToProcess()
	print("Waiting for packets to be processed.")
	while Stats.DataSendKbps > 0.5 do
		RunService.Heartbeat:Wait()
	end

	task.wait(5)
end

local function RunBenchmark(Tool: string, Bench: string): Benchmark
	local Total = 0
	local Frames = 0
	local Bandwidth = {}
	local Framerates = {}

	local Sent = 0
	local Data = Benches[Bench]
	local Events = Modes[Tool]

	local Event = Events[Bench]
	local Method

	if Tool == "warp" then
		Method = function(Data)
			Event:Fire(true, Data)
		end
	elseif Tool == "roblox" then
		Method = function(Data)
			Event:FireServer(Data)
		end
	else
		Method = Event.Fire or Event.send
	end

	local Connection = RunService.PostSimulation:Connect(function(DeltaTime: number)
		Total += DeltaTime
		Frames += 1

		if Total >= 1 then
			Total -= 1
			local Scale = (MAXIMUM_FRAMERATE / Frames)
			table.insert(Bandwidth, Stats.DataSendKbps * Scale)
			table.insert(Framerates, Frames)
			Frames = 0
		end

		for Index = 1, 1000 do
			Sent += 1
			Method(Data)
		end
	end)

	task.wait(10)
	Connection:Disconnect()
	print(`> Finished running with {Tool}`)

	--> Generate results
	table.sort(Bandwidth)
	table.sort(Framerates, function(a, b)
		return a > b
	end)

	local FrameratePercentiles = {}
	local BandwidthPercentiles = {}

	for _, Percentage in { 50, 0, 80, 90, 95, 100 } do
		table.insert(BandwidthPercentiles, Percentile(Bandwidth, Percentage))
		table.insert(FrameratePercentiles, Percentile(Framerates, Percentage))
	end

	return {
		Sent = Sent,
		Recieve = 0,
		Bandwidth = BandwidthPercentiles,
		Framerate = FrameratePercentiles,
	}
end

local function RunBenchmarks()
	local Results: Results = {}
	for Bench in Benches do
		warn(`Running {Bench} benchmark`)
		Results[Bench] = {}

		for Tool in Modes do
			print(`> Running with {Tool}`)
			Results[Bench][Tool] = RunBenchmark(Tool, Bench)

			--> Give ROBLOX some time to rest in-between tools
			WaitForPacketsToProcess()
		end

		--> Give ROBLOX some time to rest in-between benchmarks
		WaitForPacketsToProcess()
	end

	local Recieved = ReplicatedStorage.Shared.GetRecieved:InvokeServer()
	for Bench, Tools in Recieved do
		for Tool, Recieve in Tools do
			Results[Bench][Tool].Recieve = Recieve
		end
	end

	print("Finished running benchmarks, generating results...")
	ReplicatedStorage.Shared.Generate:FireServer(HttpService:JSONEncode(Results))
end

RunBenchmarks()


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

local Benches = require("./shared/benches")
local Modes = require("./shared/modes")

function CompareTables(a, b)
	if a == b then
		return true
	end

	if type(a) ~= type(b) then
		return false
	end

	if type(a) ~= "table" then
		return false
	end

	local Keys = {}
	for Key, Value in a do
		local SecondaryValue = b[Key]
		if SecondaryValue == nil or not CompareTables(Value, SecondaryValue) then
			return false
		end
		Keys[Key] = true
	end

	for Key, _ in b do
		if not Keys[Key] then
			return false
		end
	end

	return true
end

local function CompareValues(a, b): boolean
	if type(a) == "table" or type(b) == "table" then
		return CompareTables(a, b)
	end
	return (a == b)
end

local Recieved = {}
for Name, Data in Benches do
	local BenchRecieved = {}
	Recieved[Name] = BenchRecieved

	local function OnRecieve(Tool: string)
		BenchRecieved[Tool] = 0

		return function(Player, Recieve)
			BenchRecieved[Tool] += 1
			if BenchRecieved[Tool] > 1 then
				return
			end

			if Tool == "bytenet" then
				Player, Recieve = Recieve, Player
			end

			if not CompareValues(Data, Recieve) then
				warn(`Recieved incorrect data with {Tool} for {Name}`)
			else
				print(`> {Tool} passed {Name} validation!`)
			end
		end
	end

	for Tool, Events in Modes do
		local Event = Events[Name]
		local Callback = OnRecieve(Tool)

		if Tool == "warp" then
			Event:Connect(Callback)
			continue
		elseif Tool == "roblox" then
			Event.OnServerEvent:Connect(Callback)
			continue
		end

		local Method = Event.On or Event.SetCallback or Event.listen
		Method(Callback)
	end
end

ReplicatedStorage.Shared.GetRecieved.OnServerInvoke = function()
	return Recieved
end

ReplicatedStorage.Shared.Generate.OnServerEvent:Connect(function(Player, JSON)
	local OutputJSON = Instance.new("StringValue")
	OutputJSON.Name = "Result"
	OutputJSON.Value = JSON
	OutputJSON.Parent = game
	print("Generated results")
end)


local Array = {}
for Index = 1, 100 do
	table.insert(Array, {
		id = math.random(1, 255),
		x = math.random(1, 255),
		y = math.random(1, 255),
		z = math.random(1, 255),
		orientation = math.random(1, 255),
		animation = math.random(1, 255),
	})
end
return Array


local Benches = {}
for Index, Bench in script:GetChildren() do
	--> Reset random seed to a constant value
	--> This results in the same values being generated on both server and client
	math.randomseed(0)
	Benches[Bench.Name] = require(Bench)
end
return Benches


local RunService = game:GetService("RunService")
if RunService:IsServer() then
	return require("../blink/Server")
elseif RunService:IsClient() then
	return require("../blink/Client")
end


local DISABLED_MODES = {
	warp = true,
}

local Modes = {}
for Index, Mode in script:GetChildren() do
	if DISABLED_MODES[Mode.Name] then
		continue
	end

	Modes[Mode.Name] = require(Mode)
end

return Modes


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

local Benches = require("../benches")

local Events = {}
local IsServer = RunService:IsServer()

for Name in Benches do
	local Event: RemoteEvent
	if IsServer then
		Event = Instance.new("RemoteEvent")
		Event.Name = Name
		Event.Parent = ReplicatedStorage
	else
		Event = ReplicatedStorage:WaitForChild(Name)
	end

	Events[Name] = Event
end

return Events


local RunService = game:GetService("RunService")

local Benches = require("../benches")
local Warp = require("../../../packages/warp")

local Events = {}
local Method = RunService:IsServer() and "Server" or "Client"

for Name in Benches do
	Events[Name] = Warp[Method](Name, {
		interval = 0,
		maxEntrance = math.huge,
	})
end

return Events


local RunService = game:GetService("RunService")
if RunService:IsServer() then
	return require("../zap/Server")
elseif RunService:IsClient() then
	return require("../zap/Client")
end


local Hook = {}

local Brackets = {
	["("] = ")",
	["{"] = "}",
	["["] = "]",
	["<"] = ">",
}

local ClosingBrackets = {
	[")"] = true,
	["}"] = true,
	["]"] = true,
	[">"] = true,
}

local IgnoredCharacters = {
	["\n"] = true,
	[""] = true,
	[")"] = true,
	["}"] = true,
	["]"] = true,
	[">"] = true,
}

local function OnGain(Text: string, Cursor: number): (string, number)
	local Next = string.sub(Text, Cursor, Cursor)
	local Previous = string.sub(Text, Cursor - 1, Cursor - 1)

	local ClosingBracket = Brackets[Previous]
	if ClosingBracket and IgnoredCharacters[Next] then
		Text = string.sub(Text, 1, Cursor - 1) .. ClosingBracket .. string.sub(Text, Cursor)
		return Text, Cursor
	end

	return Text, Cursor
end

local function OnRemove(Text: string, PreviousText: string, Cursor: number): (string, number)
	local Next = string.sub(Text, Cursor, Cursor)
	local Previous = string.sub(PreviousText, Cursor, Cursor)
	if Brackets[Previous] and ClosingBrackets[Next] then
		Text = string.sub(Text, 1, Cursor - 1) .. string.sub(Text, Cursor + 1)
		return Text, Cursor
	end

	return Text, Cursor
end

function Hook.OnSourceChanged(Text: string, PreviousText: string, Cursor: number, Gain: number): (string, number)
	if Gain >= 1 then
		return OnGain(Text, Cursor)
	elseif Gain <= -1 then
		return OnRemove(Text, PreviousText, Cursor)
	end

	return Text, Cursor
end

return Hook


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

local Hook = {}
local IndentKeywords = {
	"{\n",
}

local function ShouldAutoIndent(Text: string, Cursor: number): boolean
	for Index, Keyword in IndentKeywords do
		local Position = (Cursor - #Keyword)
		if Position <= 0 then
			continue
		end

		local Previous = string.sub(Text, Position, Cursor - 1)
		if Previous == Keyword then
			return true
		end
	end

	return false
end

local function GetLineIndentation(Line: string): number
	return #(string.match(Line, "^\t*") :: string)
end

function Hook.OnSourceChanged(Text: string, PreviousText: string, Cursor: number, Gain: number): (string, number)
	if Gain ~= 1 then
		return Text, Cursor
	end

	local CanIndent = false
	local AdditionalIndent = 0
	local Line, Lines = Utility.GetCurrentLine(Text, Cursor)

	local Current = Lines[Line]
	local Previous = Lines[Line - 1]
	local JustReached = (Previous and Current == "")

	if ShouldAutoIndent(Text, Cursor) then
		CanIndent = true
		AdditionalIndent = 1
	elseif JustReached then
		if GetLineIndentation(Previous) > 0 then
			CanIndent = true
		end
	end

	if not CanIndent then
		return Text, Cursor
	end

	--> Update text and cursor
	local NextCharacter = string.sub(Text, Cursor, Cursor)
	if string.gsub(NextCharacter, "%c", "") == "" then
		NextCharacter = nil
	end

	local Indentation = GetLineIndentation(Previous) + AdditionalIndent
	Text = string.sub(Text, 1, Cursor - 1)
		.. string.rep("\t", Indentation)
		.. (NextCharacter and `\n{string.rep("\t", Indentation - 1)}` or "")
		.. string.sub(Text, Cursor)

	return Text, Cursor + Indentation
end

return Hook


--!strict

-- ******************************* --
-- 			AX3NX / AXEN		   --
-- ******************************* --

---- Services ----

local ContentProvider = game:GetService("ContentProvider")
local RunService = game:GetService("RunService")
local TextService = game:GetService("TextService")

---- Imports ----

local State = require("./State")
local Utility = require("@self/Utility")

local Profiler = require("./Profiler")

local Lexer = require("@compiler/Lexer")
local Parser = require("@compiler/Parser")
local Settings = require("@compiler/Settings")

local Error = require("@compiler/Modules/Error")

local StylingHooks: { { OnSourceChanged: (Text: string, PreviousText: string, Cursor: number, Gain: number) -> (string, number) } } =
	{
		require("@self/Styling/AutoIndent"),
		require("@self/Styling/AutoBracket"),
	}

---- Settings ----

local ICONS = {
	Event = "rbxassetid://16506730516",
	Field = "rbxassetid://16506725096",
	Snippet = "rbxassetid://16506712161",
	Keyword = "rbxassetid://16506695241",
	Variable = "rbxassetid://16506719167",
	Primitive = "rbxassetid://16506695241",
}

local THEME = {
	Text = Color3.fromHex("#FFFFFF"),
	Keyword = Color3.fromHex("#6796E6"),
	Primitive = Color3.fromHex("#4EC9B0"),
	Identifier = Color3.fromHex("#9CDCFE"),

	Class = Color3.fromHex("#B5CEA8"),
	Number = Color3.fromHex("#B5CEA8"),
	String = Color3.fromHex("#ADF195"),
	Bracket = Color3.fromHex("#FFFFFF"),
	Boolean = Color3.fromHex("#B5CEA8"),
	Comment = Color3.fromHex("#529955"),

	Error = Color3.fromHex("#FF5050"),
	Warning = Color3.fromHex("#FF5050"),

	Complete = Color3.fromHex("#04385f"),

	Line = {
		Active = Color3.fromHex("FFFFFF"),
		Inactive = Color3.fromHex("787878"),
	},
}
-- This is a comment
local PRIMITIVES = {}
for Primitive in Settings.Primtives do
	table.insert(PRIMITIVES, { Label = Primitive, Insert = Primitive })
end

local SYMBOLS: { [string]: { { Label: string, Insert: string } } } = {
	Event = {},
	Field = {},
	Snippet = {},
	Keyword = {
		{ Label = "set", Insert = "set %s = {}" },
		{ Label = "map", Insert = "map %s = {[]: }" },
		{ Label = "type", Insert = "type %s = " },
		{ Label = "enum", Insert = "enum %s = {}" },
		{ Label = "struct", Insert = "struct %s {}" },
		{ Label = "event", Insert = "event %s {}" },
		{ Label = "function", Insert = "function %s {}" },
		{ Label = "import", Insert = "import %s as " },
		{ Label = "scope", Insert = "scope %s {}" },
		{ Label = "export", Insert = "export " },
	},
	Variable = {},
	Primitive = PRIMITIVES,
}

local BRACKETS = {
	OpenParentheses = true,
	CloseParentheses = true,
	OpenBraces = true,
	CloseBraces = true,
	OpenBrackets = true,
	CloseBrackets = true,
}

local RICH_TEXT_ESCAPES: { [string]: string } = {
	--["&"] = "&",
	["<"] = "<",
	[">"] = ">",
	--["\""] = """,
	--["\'"] = "'",
}

local SCROLL_LINES = 2
local CURSOR_BLINK_RATE = 0.5
local PRE_ALLOCATED_LINES = 2_000

type CompletionItem = {
	Icon: string,
	Label: string,
	Insert: string,
}

---- Constants ----

local Editor = {}

--> Interface Instances
local Container: typeof(script.Parent.Widget) = script.Widget
local EditorContainer = Container.Editor
local CompletionContainer = Container.Completion

local TextContainer = EditorContainer.Text
local LinesContainer = EditorContainer.Lines

local Input = TextContainer.Input
local Cursor = TextContainer.Cursor
local Display = TextContainer.Display
local Selection = TextContainer.Selection
local ErrorSelection = TextContainer.Error

local ErrorContainer = Container.Error
local ErrorText = ErrorContainer.Text

local LineTemplate = LinesContainer.Line:Clone()
local ErrorTemplate = ErrorSelection.Line:Clone()
local SelectionTemplate = Selection.Line:Clone()
local CompletionTemplate = CompletionContainer.Option:Clone()

local TextSize = Input.TextSize
local TextHeight = TextSize * Display.LineHeight

--> Objects
local SourceLexer = Lexer.new("Highlighting")
local SourceParser = Parser.new()

local EditorWidget: DockWidgetPluginGui

---- Variables ----

local Lines = 0
local Scroll = State.new(0)
local Predictions: State.Class<{ Index: number, Items: { CompletionItem } }> = State.new({ Index = 1, Items = {} })

local CursorTimer = 0
local PreviousText = Input.Text
local PreviousLine: TextLabel?
local PreviousCursorPosition: number?
local NavigationPending: { Direction: number, Position: number }?
local CompletionPending: boolean?
local IgnoreCursorChange: boolean?

local AnalysisError: State.Class<Error.Class?> = State.new(nil :: any)
local AbstractSyntaxTree: Parser.Body?

---- Private Functions ----

local function ScrollTowards(Direction: number)
	local Value = Scroll:Get()
	local Maximum = math.max(1, (Input.TextBounds.Y - EditorContainer.AbsoluteSize.Y) // TextHeight + SCROLL_LINES)
	Scroll:Set(math.clamp(Value + (Direction * SCROLL_LINES), 0, Maximum))
end

local function Color(Text: string, Color: Color3): string
	return `<font color="#{Color:ToHex()}">{Text}</font>`
end

local function Escape(Text: string)
	for Symbol, Escape in RICH_TEXT_ESCAPES do
		Text = string.gsub(Text, Symbol, Escape)
	end

	return Text
end

local function ClearChildrenWhichAre(Parent: Instance, Class: string)
	for Index, Child in Parent:GetChildren() do
		if Child:IsA(Class) then
			Child:Destroy()
		end
	end
end

local function GetLineAtPosition(Text: string, Position: number): number
	local _, Count = string.gsub(string.sub(Text, 1, Position - 1), "\n", "")
	return Count + 1
end

local function GetPrediction(): CompletionItem?
	local Items = Predictions:Get()
	local Item = Items.Items[Items.Index]
	if not Item then
		return
	end

	return Item
end

local DOTS = "%.%."
local NUMBER = "%-?%d*%.?%d+"

local function UpdateColors()
	local Source = Input.Text
	local RichText = {}

	--> Render text immediately
	Display.Text = Source
	Display.TextColor3 = THEME.Text

	--> Initiialize lexer
	SourceLexer:Initialize(Source)

	--> Highlight state
	local Keyword = "none"
	local Token = SourceLexer:GetNextToken()

	while Token and (Token.Type ~= "EndOfFile") do
		local Type: Lexer.Types = Token.Type
		local Value = Token.Value

		local String = Escape(Value)
		if Type == "Keyword" or Type == "Import" or Type == "As" then
			Keyword = Value
			String = Color(Value, THEME.Keyword)
		elseif Type == "Primitive" then
			String = Color(Value, THEME.Primitive)
		elseif Type == "Identifier" then
			local LookAhead = SourceLexer:GetNextToken(true)
			local IsField = (LookAhead and LookAhead.Type == "FieldAssign" and Keyword ~= "map")
			String = Color(Value, IsField and THEME.Text or THEME.Identifier)
		elseif Type == "Array" or Type == "Range" then
			--> Exact size array/range
			local Single = string.match(Value, `%[({NUMBER})%]`) or string.match(Value, `%(({NUMBER})%)`)
			local Lower = string.match(Value, `%[({NUMBER}){DOTS}`) or string.match(Value, `%(({NUMBER}){DOTS}`)
			local Upper = string.match(Value, `{DOTS}({NUMBER})%]`) or string.match(Value, `{DOTS}({NUMBER})%)`)

			if Single then
				String = string.gsub(Value, Single, Color(Single, THEME.Number))
			elseif Lower and Upper then
				String =
					`{string.sub(Value, 1, 1)}{Color(Lower, THEME.Number)}..{Color(Upper, THEME.Number)}{string.sub(
						Value,
						#Value,
						#Value
					)}`
			end
		elseif Type == "Component" then
			local Primitive = string.match(Value, "(%w+)") or ""
			String = `{Color("<", THEME.Bracket)}{Color(Primitive, THEME.Primitive)}{Color(">", THEME.Bracket)}`
		elseif BRACKETS[Type] then
			String = Color(Value, THEME.Bracket)
		elseif Type == "Class" then
			String = `({Color(string.sub(Value, 2, #Value - 1), THEME.Class)})`
		elseif Type == "String" then
			String = Color(Value, THEME.String)
		elseif Type == "Boolean" then
			String = Color(Value, THEME.Boolean)
		elseif Type == "Comment" then
			String = Color(Value, THEME.Comment)
		elseif Type == "Unknown" then
			String = Color(Value, THEME.Error)
		end

		table.insert(RichText, String)
		Token = SourceLexer:GetNextToken()
	end

	--> Render highlighted text
	Display.Text = table.concat(RichText)
end

local function UpdateAnalysis()
	local Success, Result = pcall(function()
		return SourceParser:Parse(Editor.GetSource())
	end)

	if Success then
		AnalysisError:Set(nil)
		AbstractSyntaxTree = Result
	end
end

local function UpdateCompletion()
	local Line = Utility.GetLineRangeAtPosition(Input.Text, Input.CursorPosition)
	local Items: { CompletionItem } = {}

	--> Match Keywords
	local function MatchSymbols()
		local Word = Utility.GetWordRangeAtPosition(Line.Text, Input.CursorPosition - Line.Start)
		local Text = Word.Text
		if #Text == 0 then
			return
		end

		--> Match keywords
		if Word.Index == 1 then
			for _, Symbol in SYMBOLS.Keyword do
				local Label = Symbol.Label
				local Stub = string.sub(Label, 1, #Text)
				if Stub ~= Text or Stub == Label then
					continue
				end

				table.insert(Items, {
					Icon = ICONS.Keyword,
					Label = Label,
					Insert = Symbol.Insert,
				})
			end
		end

		--> Match symbols
		local Previous = Utility.GetWordRangeBeforePosition(Line.Text, Word.Start - 1)
		if Previous.Text == "" then
			return
		end

		if string.match(Previous.Text, ":$") or Previous.Text == "=" then
			for _, Symbol in SYMBOLS.Primitive do
				local Label = Symbol.Label
				local Stub = string.sub(Label, 1, #Text)
				if Stub ~= Text or Stub == Label then
					continue
				end

				table.insert(Items, {
					Icon = ICONS.Primitive,
					Label = Label,
					Insert = Label,
				})
			end

			if AbstractSyntaxTree then
				for Symbol in AbstractSyntaxTree.Value.Symbols do
					local Stub = string.sub(Symbol, 1, #Text)
					if Stub ~= Text or Stub == Symbol then
						continue
					end

					table.insert(Items, {
						Icon = ICONS.Variable,
						Label = Symbol,
						Insert = Symbol,
					})
				end
			end
		end
	end

	MatchSymbols()

	--> Update predictions
	Predictions:Set({
		Index = 1,
		Items = Items,
	})
end

local function UpdateHighlightedLine()
	local Line = Utility.GetCurrentLine(Input.Text, Input.CursorPosition)
	Line = math.clamp(Line, 1, PRE_ALLOCATED_LINES)

	local Label: TextLabel = LinesContainer[tostring(Line)]
	if PreviousLine then
		PreviousLine.TextColor3 = THEME.Line.Inactive
	end

	PreviousLine = Label
	Label.TextColor3 = THEME.Line.Active
end

---- Event Functions -----

local function OnErrorEmitted(Object: Error.Class)
	AnalysisError:Set(Object)
end

local function OnPreRender(DeltaTime: number)
	if NavigationPending then
		local Navigation = NavigationPending
		NavigationPending = nil

		local Value = Predictions:Get()
		local Count = #Value.Items
		if Count > 0 then
			--> Cursor should be first for proper autocomplete positioning
			local NavigationPosition = Navigation.Position
			IgnoreCursorChange = true
			Input.CursorPosition = NavigationPosition
			PreviousCursorPosition = NavigationPosition

			Predictions:Set({
				Index = Navigation.Direction == -1 and ((Value.Index - 2) % Count) + 1 or (Value.Index % Count) + 1,
				Items = Value.Items,
			})
		end
	elseif CompletionPending then
		UpdateCompletion()
	end

	CompletionPending = nil

	if Input.CursorPosition == -1 then
		return
	end

	CursorTimer += DeltaTime
	if CursorTimer < CURSOR_BLINK_RATE then
		return
	end

	CursorTimer -= CURSOR_BLINK_RATE
	Cursor.Visible = not Cursor.Visible
end

--> State functions
local function OnLinesChanged(Value: number)
	--> Insert new or delete lines
	local Difference = (Value - Lines)

	if Difference > 0 then
		for Index = (Lines + 1), Value do
			local Name = tostring(Index)
			local Line = LinesContainer[Name]
			Line.TextTransparency = 0
		end
	elseif Difference < 0 then
		for Index = Lines, (Value + 1), -1 do
			local Name = tostring(Index)
			local Line = LinesContainer[Name]
			Line.TextTransparency = 1
		end
	end

	Lines = Value

	--> Update highlighted line
	UpdateHighlightedLine()
end

local function OnScrollChanged(Value: number)
	EditorContainer.Position = UDim2.fromOffset(EditorContainer.Position.X.Offset, TextHeight * -Value)
end

local function OnPredictionsChanged()
	local Value = Predictions:Get()
	local Items = Value.Items
	if #Items == 0 then
		CompletionContainer.Visible = false
		return
	end

	--> Update completion position
	local AbsoluteSize = EditorContainer.AbsoluteSize
	local AbsolutePosition = Cursor.AbsolutePosition
	local HorisontalWidth = math.max(0, (AbsoluteSize.X - CompletionContainer.Size.X.Offset) - 10)
	CompletionContainer.Position =
		UDim2.fromOffset(math.clamp(AbsolutePosition.X, 0, HorisontalWidth), AbsolutePosition.Y + TextSize)

	CompletionContainer.Visible = true
	ClearChildrenWhichAre(CompletionContainer, "Frame")

	for Index, Item in Items do
		local Frame = CompletionTemplate:Clone()
		Frame.Icon.Image = Item.Icon
		Frame.Text.Text = Item.Label

		if Index == Value.Index then
			Frame.BackgroundColor3 = THEME.Complete
		end

		Frame.Parent = CompletionContainer
	end
end

local function OnAnalysisErrorChanged()
	local Error = AnalysisError:Get()

	--> Clear previous error
	ErrorContainer.Visible = false
	ClearChildrenWhichAre(ErrorSelection, "Frame")

	--> No errors
	if not Error then
		return
	end

	local Labels = Error.Labels
	if #Labels == 0 then
		return
	end

	--> Error message
	local Message =
		`{Error.GetNameFromCode(Error.Code)}: {Error.RawMessage} <font transparency="0.5"><i>Blink({Error.Code})</i></font>`
	for _, Label in Labels do
		local Line = Utility.GetCurrentLine(Input.Text, Label.Span.Start)
		Message ..= `\n{Label.Text} [L{Line}]`
	end

	ErrorText.Text = Message

	--> Highlight error
	local Primary = Labels[#Labels]

	local Span = Primary.Span
	local Start = Span.Start
	local Finish = Span.End

	local Text = string.sub(Input.ContentText, 1, Finish)
	local Slices = string.split(Text, "\n")

	local Offset = 0
	for Index, Slice in Slices do
		local First = Offset
		Offset += (#Slice + 1)

		local Line = ErrorTemplate:Clone()
		Line.Size = UDim2.new(1, 0, 0, TextHeight)
		local Fill = Line.Fill

		if Offset < Start then
			Line.Fill.MaxVisibleGraphemes = 0
			Line.Parent = ErrorSelection
			continue
		end

		--> Calculate selection bounds
		local Substring = Slice
		if First < Start then
			Substring = string.sub(Slice, (Start - First), #Slice)
		elseif Index == #Slices then
			Substring = string.sub(Slice, 1, math.max(1, Finish - First))
		end

		--> Empty lines should appear as one space thick selections
		if Substring == "" then
			Substring = " "
		end

		local SelectionBoundsParams = Instance.new("GetTextBoundsParams")
		SelectionBoundsParams.Text = Substring
		SelectionBoundsParams.Size = TextSize
		SelectionBoundsParams.Font = Display.FontFace

		local SelectionBounds = TextService:GetTextBoundsAsync(SelectionBoundsParams)
		Fill.Size = UDim2.new(0, SelectionBounds.X, 1, 0)

		--> Calculate selection offset
		if Start > First then
			local Prefix = string.sub(Slice, 1, (Start - First) - 1)
			if Prefix ~= "" then
				local OffsetBoundsParams = Instance.new("GetTextBoundsParams")
				OffsetBoundsParams.Text = Prefix
				OffsetBoundsParams.Size = TextSize
				OffsetBoundsParams.Font = Display.FontFace

				local OffsetBounds = TextService:GetTextBoundsAsync(OffsetBoundsParams)
				Fill.Position = UDim2.new(0, OffsetBounds.X, 0, 10)
			end
		end

		Line.MouseEnter:Connect(function()
			local Position = EditorWidget:GetRelativeMousePosition()
			ErrorContainer.Visible = true
			ErrorContainer.Position = UDim2.fromOffset(Position.X + 4, Position.Y)
		end)

		Line.MouseLeave:Connect(function()
			ErrorContainer.Visible = false
		end)

		Line.Fill.MaxVisibleGraphemes = #Substring
		Line.Parent = ErrorSelection

		break
	end
end

--> RBXScriptSignal functions

local function OnCursorPositionChanged()
	local CursorPosition = Input.CursorPosition
	if CursorPosition == -1 then
		Cursor.Visible = false
		return
	end

	Profiler.profileBegin("Cursor")

	local Text = string.sub(Input.Text, 1, CursorPosition - 1)
	local Slices = string.split(Text, "\n")

	--> Calculate current end line position
	local Line = Slices[#Slices] or ""
	local GetTextBoundsParams = Instance.new("GetTextBoundsParams")
	GetTextBoundsParams.Text = Line
	GetTextBoundsParams.Size = TextSize
	GetTextBoundsParams.Font = Display.FontFace

	local TextBounds = TextService:GetTextBoundsAsync(GetTextBoundsParams)

	--> Update cursor size and position
	Cursor.Size = UDim2.fromOffset(2, TextSize)
	Cursor.Position = UDim2.fromOffset(TextBounds.X - 1, TextHeight * (#Slices - 1))

	--> Update highlighted line
	Profiler.profileBegin("Highlight LN")
	UpdateHighlightedLine()
	Profiler.profileEnd()

	--> Ignore navigation or completion when cursor restored by OnPreRender
	if IgnoreCursorChange then
		IgnoreCursorChange = nil
	elseif #Predictions:Get().Items > 0 then
		CompletionPending = true

		if PreviousCursorPosition ~= nil then
			local InputText = Input.Text
			local CurrentLine = GetLineAtPosition(InputText, CursorPosition)
			local PreviousLine = GetLineAtPosition(InputText, PreviousCursorPosition)

			if math.abs(CurrentLine - PreviousLine) == 1 and math.abs(CursorPosition - PreviousCursorPosition) > 1 then
				NavigationPending = {
					Direction = CurrentLine > PreviousLine and 1 or -1,
					Position = PreviousCursorPosition,
				}
			end
		end
	end

	PreviousCursorPosition = CursorPosition
	Profiler.profileEnd()
end

local function OnSelectionChanged()
	--> Clear previous selection
	ClearChildrenWhichAre(Selection, "Frame")

	--> Nothing is selected
	local CursorPosition = Input.CursorPosition
	local SelectionStart = Input.SelectionStart
	if CursorPosition == -1 or SelectionStart == -1 then
		return
	end

	local Start = math.min(CursorPosition, SelectionStart)
	local Finish = math.max(CursorPosition, SelectionStart)

	local Text = string.sub(Input.ContentText, 1, Finish - 1)
	local Slices = string.split(Text, "\n")

	local Offset = 0
	for Index, Slice in Slices do
		local First = Offset
		Offset += (#Slice + 1)

		local Line = SelectionTemplate:Clone()
		Line.Size = UDim2.new(1, 0, 0, TextHeight)
		local Fill = Line.Fill

		if Offset < Start then
			Fill.BackgroundTransparency = 1
			Line.Parent = Selection
			continue
		end

		--> Calculate selection bounds
		local Substring = Slice
		if First < Start then
			Substring = string.sub(Slice, (Start - First), #Slice)
		elseif Index == #Slices then
			Substring = string.sub(Slice, 1, math.max(1, Finish - First))
		end

		--> Empty lines should appear as one space thick selections
		if Substring == "" then
			Substring = " "
		end

		local SelectionBoundsParams = Instance.new("GetTextBoundsParams")
		SelectionBoundsParams.Text = Substring
		SelectionBoundsParams.Size = TextSize
		SelectionBoundsParams.Font = Display.FontFace

		local SelectionBounds = TextService:GetTextBoundsAsync(SelectionBoundsParams)
		Fill.Size = UDim2.new(0, SelectionBounds.X, 1, 0)

		--> Calculate selection offset
		if Start > First then
			local Prefix = string.sub(Slice, 1, (Start - First) - 1)
			if Prefix ~= "" then
				local OffsetBoundsParams = Instance.new("GetTextBoundsParams")
				OffsetBoundsParams.Text = Prefix
				OffsetBoundsParams.Size = TextSize
				OffsetBoundsParams.Font = Display.FontFace

				local OffsetBounds = TextService:GetTextBoundsAsync(OffsetBoundsParams)
				Fill.Position = UDim2.new(0, OffsetBounds.X, 0, 0)
			end
		end

		Line.Parent = Selection
	end
end

local function OnSourceChanged()
	NavigationPending = nil
	CompletionPending = nil

	--> Pressing enter inserts a carriage return (ROBLOX please fix)
	local Text = Input.Text
	local Gain = math.sign(#Text - #PreviousText)

	local NoReturnCarriage = string.gsub(Text, "\r", "")
	if NoReturnCarriage ~= Text then
		Input.Text = NoReturnCarriage
		return
	end

	--> Auto complete
	if Gain == 1 then
		Profiler.profileBegin("Insertion")

		local Item = GetPrediction()
		local Cursor = (Input.CursorPosition - 1)
		local Character = string.sub(Text, Cursor, Cursor)

		if Item and Character == "\t" then
			local Insert = Item.Insert
			local Offset = (#Insert + 1)

			local Start, End = string.find(Insert, "%s", 1, true)
			if Start and End then
				Offset = Start
				Insert = string.sub(Insert, 1, Start - 1) .. string.sub(Insert, End + 1)
			end

			--> Move cursor behind tab character
			Cursor -= 1

			local Line = Utility.GetLineRangeAtPosition(Text, Cursor)
			local Position = (Cursor - Line.Start + 1)

			local Word = Utility.GetWordRangeAtPosition(Line.Text, Position)
			local Length = #Word.Text

			Cursor -= Length

			--> Match characters before replacement and after tab (2)
			Text = (string.sub(Text, 1, Cursor) .. Insert .. string.sub(Text, Cursor + Length + 2))

			PreviousText = Text
			Input.Text = Text

			Input.CursorPosition = -1
			Input.CursorPosition = (Cursor + Offset)

			Profiler.profileEnd()
			return
		end

		Profiler.profileEnd()
	end

	--> Run styling hooks
	Profiler.profileBegin("Styling")

	local FinalText, FinalCursor = Text, Input.CursorPosition
	for Index, Hook in StylingHooks do
		FinalText, FinalCursor = Hook.OnSourceChanged(FinalText, PreviousText, FinalCursor, Gain)
	end

	Profiler.profileEnd()

	--> Update previous text
	PreviousText = FinalText

	--> If a styling hook altered the final text then don't do any additional work
	if FinalText ~= Text then
		Input.Text = FinalText
		Input.CursorPosition = FinalCursor

		return
	end

	--> Update line counter
	Profiler.profileBegin("Line counter")

	local Slices = string.split(Text, "\n")
	if #Slices ~= Lines then
		OnLinesChanged(#Slices)
	end

	Profiler.profileEnd()

	--> Perform lexer pass
	Profiler.profileBegin("Analysis")
	UpdateAnalysis()
	Profiler.profileEnd()

	Profiler.profileBegin("Completion")
	UpdateCompletion()
	Profiler.profileEnd()

	Profiler.profileBegin("Highlighting")
	UpdateColors()
	Profiler.profileEnd()

	--> Update cursor after text change
	PreviousCursorPosition = Input.CursorPosition
	OnCursorPositionChanged()
end

--> Create lines in the background
local function GenerateLinesAsync()
	local Tick = os.clock()
	for Index = 1, PRE_ALLOCATED_LINES do
		local Elapsed = (os.clock() - Tick)
		if Elapsed > (1 / 120) then
			task.wait()
			Tick = os.clock()
		end

		local Line = LineTemplate:Clone()
		Line.Name = tostring(Index)
		Line.Text = tostring(Index)
		Line.TextColor3 = THEME.Line.Inactive
		Line.TextTransparency = 1
		Line.Position = UDim2.fromOffset(0, TextHeight * (Index - 1))
		Line.Parent = LinesContainer
	end
end

---- Public Functions ----

function Editor.GetSource(): string
	return Input.Text
end

function Editor.SetSource(Source: string)
	Scroll:Set(0)
	Input.Text = Source
end

---- Initialization ----

function Editor.Initialize(Widget: DockWidgetPluginGui)
	EditorWidget = Widget
	task.spawn(GenerateLinesAsync)

	--> Preload fira sans
	ContentProvider:PreloadAsync({ Display }, function(AssetId: string, AssetFetchStatus: Enum.AssetFetchStatus)
		if AssetId ~= "" and AssetFetchStatus ~= Enum.AssetFetchStatus.Success then
			warn("[BLINK] There was an issue preloading the editors font.")
		end
	end)

	--> Remove templates
	Selection.Line:Destroy()
	ErrorSelection.Line:Destroy()
	LinesContainer.Line:Destroy()
	CompletionContainer.Option:Destroy()

	--> Sort auto complete symbols
	for _, Symbols in SYMBOLS do
		table.sort(Symbols, function(a, b)
			return #a.Label < #b.Label
		end)
	end

	--> Connections
	Error.OnEmit = OnErrorEmitted

	Scroll:OnChange(OnScrollChanged)
	Predictions:OnChange(OnPredictionsChanged)
	AnalysisError:OnChange(OnAnalysisErrorChanged)

	--> Fixes a weird bug with the cursor being moved to character 6000
	Input.Focused:Once(function()
		Input:ReleaseFocus(true)
		RunService.PreRender:Wait()

		Input:CaptureFocus()
		Input.CursorPosition = 1
	end)

	Input:GetPropertyChangedSignal("Text"):Connect(function()
		Profiler.profileBegin("Total")
		OnSourceChanged()
		Profiler.profileEnd()
	end)

	Input:GetPropertyChangedSignal("SelectionStart"):Connect(OnSelectionChanged)
	Input:GetPropertyChangedSignal("CursorPosition"):Connect(OnSelectionChanged)
	Input:GetPropertyChangedSignal("CursorPosition"):Connect(OnCursorPositionChanged)
	Input.InputChanged:Connect(function(InputObject)
		if InputObject.UserInputType == Enum.UserInputType.MouseWheel then
			ScrollTowards(-InputObject.Position.Z)
		end
	end)

	RunService.PreRender:Connect(OnPreRender)
end

---- Connections ----

return Editor


--!strict

---- Services ----

---- Imports ----

---- Settings ----

---- Constants ----

local Profiler = {}

--> Interface Instances
local Container: typeof(script.Parent.Widget) = script.Widget
local DebugContainer = Container.Debug
local MetricTemplate = DebugContainer.Metric:Clone()

type Profile = {
	Label: string,
	Metric: typeof(MetricTemplate),

	Tick: number,
	Time: number,

	Previous: Profile?,
	Children: { Profile },
}

---- Variables ----

local ActiveProfile: Profile?

---- Private Functions ----

local function isActive()
	return DebugContainer.Visible
end

local function formatPercentage(Percentage: number): string
	return `{math.floor(Percentage * 100)} %`
end

local function formatMilliseconds(Seconds: number): string
	return string.format("%.2f ms", Seconds * 1000)
end

local function updatePercentages(Time: number, Children: { Profile })
	for _, Profile in Children do
		Profile.Metric.Percentage.Text = formatPercentage(Profile.Time / Time)
		updatePercentages(Time, Profile.Children)
	end
end

---- Public Functions ----

function Profiler.profileBegin(Label: string)
	if not isActive() then
		return
	end

	local Metric = DebugContainer:FindFirstChild(Label)
	if not Metric then
		local New = MetricTemplate:Clone()
		New.Name = Label
		New.Time.Text = "? ms"
		New.Label.Text = string.upper(Label)
		New.Parent = DebugContainer
		Metric = New
	end

	local Profile: Profile = {
		Label = Label,
		Metric = Metric,

		Tick = os.clock(),
		Time = 0,

		Children = {},
		Previous = ActiveProfile,
	}

	if ActiveProfile then
		table.insert(ActiveProfile.Children, Profile)
	end

	ActiveProfile = Profile
end

function Profiler.profileDiscard()
	if not isActive() then
		return
	end

	assert(ActiveProfile ~= nil, "No active profile.")
	ActiveProfile = ActiveProfile.Previous
end

function Profiler.profileEnd()
	if not isActive() then
		return
	end

	assert(ActiveProfile ~= nil, "No active profile.")
	local Elapsed = (os.clock() - ActiveProfile.Tick)

	ActiveProfile.Time = Elapsed
	ActiveProfile.Metric.Time.Text = formatMilliseconds(Elapsed)

	if not ActiveProfile.Previous then
		updatePercentages(Elapsed, ActiveProfile.Children)
	end

	ActiveProfile = ActiveProfile.Previous
end

function Profiler.toggle()
	DebugContainer.Visible = not DebugContainer.Visible
end

---- Initialization ----

function Profiler.Initialize()
	DebugContainer.Metric:Destroy()
end

---- Connections ----

return Profiler


--!strict

-- ******************************* --
-- 			AX3NX / AXEN		   --
-- ******************************* --

---- Services ----

---- Imports ----

---- Settings ----

---- Constants ----

local State = {}
State.__index = State

export type Class<T> = typeof(setmetatable(
	{} :: {
		Value: T,
		Observers: { [(T) -> ()]: boolean },
	},
	State
))

---- Variables ----

---- Private Functions ----

---- Public Functions ----

function State.new<T>(Value: T): Class<T>
	return setmetatable({
		Value = Value,
		Observers = {},
	}, State)
end

function State.Get<T>(self: Class<T>): T
	return self.Value
end

function State.Set<T>(self: Class<T>, Value: T)
	if self.Value ~= Value or type(Value) == "table" then
		self.Value = Value
		self:_updateObservers()
	end
end

function State.OnChange<T>(self: Class<T>, Observer: (T) -> ()): () -> ()
	self.Observers[Observer] = true
	task.defer(function()
		self:_updateObservers()
	end)

	return function()
		self.Observers[Observer] = nil
	end
end

function State._updateObservers<T>(self: Class<T>)
	for Observer in self.Observers do
		task.spawn(Observer, self.Value)
	end
end

---- Initialization ----

---- Connections ----

return State


local Table = {}

function Table.MergeArrays(a: { any }, b: { any }): { any }
	local Array = table.create(#a + #b)

	for _, Element in a do
		table.insert(Array, Element)
	end

	for _, Element in b do
		table.insert(Array, Element)
	end

	return Array
end

function Table.MergeDictionaries(a: { [any]: any }, b: { [any]: any }): { [any]: any }
	local Dictionary = table.clone(a)
	for Key, Value in b do
		if Dictionary[Key] then
			warn(`Key "{Key}" already exists in the first dictionary.`)
			continue
		end

		Dictionary[Key] = Value
	end
	return Dictionary
end

function Table.GetDictionaryKeys(Dictionary: { [any]: any }): { any }
	local Keys = {}
	for Key in Dictionary do
		table.insert(Keys, Key)
	end
	return Keys
end

return Table


--!strict

-- ******************************* --
-- 			AX3NX / AXEN		   --
-- ******************************* --

---- Services ----

local RunService = game:GetService("RunService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local Selection = game:GetService("Selection")
local ServerStorage = game:GetService("ServerStorage")
local StudioService = game:GetService("StudioService")
local TweenService = game:GetService("TweenService")

if RunService:IsRunning() then
	return
end

---- Imports ----

local Editor = require("@self/Editor")
local Profiler = require("@self/Profiler")
local State = require("@self/State")

local Generator = require("@compiler/Generator")
local Parser = require("@compiler/Parser")

---- Settings ----

local FILES_FOLDER = "BLINK_CONFIGURATION_FILES"
local TEMPLATE_FILE = {
	Name = "Template",
	Source = table.concat({
		"type Example = u8",
		"event MyEvent {",
		"\tFrom: Server,",
		"\tType: Reliable,",
		"\tCall: SingleSync,",
		"\tData: Example",
		"}",
	}, "\n"),
}

local ERROR_WIDGET = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 500, 240, 300, 240)

local EDITOR_WIDGET = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Left, false, false, 360, 400, 300, 400)

local SAVE_COLOR = Color3.fromRGB(0, 100, 0)
local BUTTON_COLOR = Color3.fromRGB(30, 30, 30)
local EXPAND_TWEEN = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)

---- Constants ----

local Toolbar = plugin:CreateToolbar("Blink Suite")
local EditorButton = Toolbar:CreateButton("Editor", "Opens the configuration editor", "rbxassetid://118423040995617")
EditorButton.ClickableWhenViewportHidden = true

local Template = script.Widget
local FileTemplate = Template.Side.Content.Top.Files.File:Clone()

--> Remove templates
--Template.Editor.Text.Line:Destroy()
Template.Side.Content.Top.Files.File:Destroy()

local ErrorWidget = plugin:CreateDockWidgetPluginGui("Generation Error", ERROR_WIDGET)
ErrorWidget.Name = "Generation Error"
ErrorWidget.Title = "Error"
ErrorWidget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

local ErrorInterface = script.Error:Clone()
ErrorInterface.Visible = true
ErrorInterface.Parent = ErrorWidget

local ErrorText: TextLabel = ErrorInterface.Text

local EditorWidget = plugin:CreateDockWidgetPluginGui("Configuration Editor", EDITOR_WIDGET)
EditorWidget.Name = "Blink Editor"
EditorWidget.Title = "Configuration Editor"
EditorWidget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

local EditorInterface = script.Widget
EditorInterface.Size = UDim2.fromScale(1, 1)
EditorInterface.Parent = EditorWidget

local Side = EditorInterface.Side
local Overlay = EditorInterface.Overlay
local QuickActions = EditorInterface.Quick

local QuickSave = QuickActions.Save
local QuickClear = QuickActions.Clear

local Content = Side.Content
local Expand = Side.Expand
local Settings = Side.Settings

local Top = Content.Top
local Bottom = Content.Bottom

local Files = Top.Files
local Import = Top.Title.Import
local Search = Top.Title.Search.Input

local Prompt = Bottom.Save
local Buttons = Bottom.Buttons

local GeneratePrompt = Bottom.Generate
local GenerateButtons = GeneratePrompt.Buttons
local Hint = GeneratePrompt.Hint
local Generate = GenerateButtons.Generate
local CancelGenerate = GenerateButtons.Cancel

local Input = Prompt.Input
local Save = Buttons.Save
local Cancel = Buttons.Cancel

--> Tweens
local Tweens = {
	Editor = {
		Expand = {
			Side = TweenService:Create(Side, EXPAND_TWEEN, { Position = UDim2.new() }),
			Content = TweenService:Create(Content, EXPAND_TWEEN, { GroupTransparency = 0 }),
			Overlay = TweenService:Create(Overlay, EXPAND_TWEEN, { BackgroundTransparency = 0.5 }),
			Settings = TweenService:Create(Settings, EXPAND_TWEEN, { ImageTransparency = 1 }),
		},

		Retract = {
			Side = TweenService:Create(Side, EXPAND_TWEEN, { Position = Side.Position }),
			Content = TweenService:Create(Content, EXPAND_TWEEN, { GroupTransparency = 1 }),
			Overlay = TweenService:Create(Overlay, EXPAND_TWEEN, { BackgroundTransparency = 1 }),
			Settings = TweenService:Create(Settings, EXPAND_TWEEN, { ImageTransparency = 0 }),
		},
	},
}

---- Variables ----

local Saving = false
local IsFirstTimeOpening = true

local Expanded = State.new(false)
local Selected: State.Class<Instance?> = State.new(nil :: any)
local Generating: State.Class<StringValue?> = State.new(nil :: any)

local Editing: string?

local ProfilerTick = 0
local ProfilerClicks = 0

---- Private Functions ----

--> Interface utility functions

local function ShowError(Error: string)
	local Start = string.find(Error, "<font", 1, true)
	ErrorText.Text = string.sub(Error, Start or 1, #Error)
	ErrorText.RichText = true
	ErrorWidget.Enabled = true
end

local function PlayTweens(Tweens: { [string]: Tween })
	for _, Tween in Tweens do
		Tween:Play()
	end
end

local function CreateModuleScript(Name: string, Source: string, Parent: Instance)
	local ModuleScript = Instance.new("ModuleScript")
	ModuleScript.Name = Name
	ModuleScript.Parent = Parent

	--> Update source after parenting to DM
	ScriptEditorService:UpdateSourceAsync(ModuleScript, function()
		return Source
	end)
end

local function RequestScriptPermissions(): boolean
	local Success = pcall(function()
		local Script = Instance.new("Script", script)
		Script:Destroy()
	end)

	return Success
end

local function ClearChildrenWhichAre(Parent: Instance, Class: string)
	for Index, Child in Parent:GetChildren() do
		if Child:IsA(Class) then
			Child:Destroy()
		end
	end
end

--> Files utility functions

local function GetSaveFolder(): Folder
	local SaveFolder = ServerStorage:FindFirstChild(FILES_FOLDER)
	if not SaveFolder then
		local Folder = Instance.new("Folder")
		Folder.Name = FILES_FOLDER
		Folder.Parent = ServerStorage
		SaveFolder = Folder
	end

	return SaveFolder
end

local function GenerateFile(File: StringValue, Directory: Instance)
	local Source = File.Value
	local SourceParser = Parser.new()
	local Success, Error, AbstractSyntaxTree = pcall(function()
		return nil, SourceParser:Parse(Source)
	end)

	if not Success then
		ShowError(tostring(Error))
		return
	end

	if not RequestScriptPermissions() then
		ShowError("File generation failed, plugin doesn't have script inject permissions.")
		warn("[BLINK]: File generation failed, plugin doesn't have script inject permissions.")
		return
	end

	local ServerSource = Generator.Generate("Server", AbstractSyntaxTree)
	local ClientSource = Generator.Generate("Client", AbstractSyntaxTree)
	local TypesSource = Generator.GenerateShared(AbstractSyntaxTree)

	local BlinkFolder: Folder = Directory:FindFirstChild("Blink") :: Folder
	if not BlinkFolder then
		local Folder = Instance.new("Folder")
		Folder.Name = "Blink"
		Folder.Parent = Directory
		BlinkFolder = Folder
	end

	--> Clear previous files
	BlinkFolder:ClearAllChildren()

	--> Generate output files
	CreateModuleScript("Types", TypesSource, BlinkFolder)
	CreateModuleScript("Server", ServerSource, BlinkFolder)
	CreateModuleScript("Client", ClientSource, BlinkFolder)
end

local function LoadFile(File: StringValue)
	Expanded:Set(false)
	Editing = File.Name
	Editor.SetSource(File.Value)
end

local function LoadFiles()
	--> Remove previous files
	Generating:Set()
	ClearChildrenWhichAre(Files, "Frame")

	--> Load new files
	local FileInstances = GetSaveFolder():GetChildren()
	table.sort(FileInstances, function(a, b)
		return a.Name < b.Name
	end)

	for _, File: StringValue in FileInstances :: { any } do
		local Name = File.Name
		local Frame = FileTemplate:Clone()
		local Buttons = Frame.Buttons

		Frame.Name = Name
		Frame.Title.Text = Name

		Buttons.Edit.MouseButton1Click:Connect(function()
			LoadFile(File)
		end)

		Buttons.Delete.MouseButton1Click:Connect(function()
			File:Destroy()
			LoadFiles()
		end)

		Buttons.Generate.MouseButton1Click:Connect(function()
			if GeneratePrompt.Visible then
				return
			end

			Generating:Set(File)
		end)

		Frame.Parent = Files
	end
end

local function SaveFile(Name: string, Source: string)
	local SaveFolder = GetSaveFolder()
	local SaveInstance: StringValue? = SaveFolder:FindFirstChild(Name) :: StringValue
	if not SaveInstance then
		local NewSaveInstance = Instance.new("StringValue")
		NewSaveInstance.Name = Name
		NewSaveInstance.Value = Source
		NewSaveInstance.Parent = SaveFolder
		return
	end

	SaveInstance.Value = Source
end

local function CreateTemplateFile()
	if ServerStorage:FindFirstChild(FILES_FOLDER) then
		return
	end

	SaveFile(TEMPLATE_FILE.Name, TEMPLATE_FILE.Source)
	LoadFile(GetSaveFolder():FindFirstChild(TEMPLATE_FILE.Name) :: StringValue)
end

local function HandleFirstTimeOpen()
	if IsFirstTimeOpening == false then
		return
	end

	CreateTemplateFile()
	LoadFiles()
	IsFirstTimeOpening = false
end

---- Public Functions ----

local function OnSearch(PressedEnter: boolean)
	if not PressedEnter then
		return
	end

	local Query = Search.Text
	for _, Frame in Files:GetChildren() do
		if not Frame:IsA("Frame") then
			continue
		end

		if Query == "" then
			Frame.Visible = true
			continue
		end

		Frame.Visible = (string.find(Frame.Name, Query) ~= nil)
	end
end

local function OnSaveCompleted()
	Saving = false
	Prompt.Visible = false
	Cancel.Visible = false
	Save.BackgroundColor3 = BUTTON_COLOR
end

local function OnSaveActivated()
	if Saving then
		local Name = Input.Text
		if Name == "" then
			return
		end

		SaveFile(Name, Editor.GetSource())
		LoadFiles()
		OnSaveCompleted()

		return
	end

	Saving = true
	Input.Text = Editing or ""
	Prompt.Visible = true
	Cancel.Visible = true
	Save.BackgroundColor3 = SAVE_COLOR
end

local function OnExpandActivated()
	HandleFirstTimeOpen()
	Expanded:Set(not Expanded:Get())
end

local function OnEditorButtonClicked()
	HandleFirstTimeOpen()
	EditorWidget.Enabled = not EditorWidget.Enabled
end

local function OnEditorWidgetEnabled()
	EditorButton:SetActive(EditorWidget.Enabled)
end

local function OnQuickSave()
	if Expanded:Get() then
		return
	end

	if not Editing then
		Expanded:Set(true)
		OnSaveActivated()
		return
	end

	SaveFile(Editing, Editor.GetSource())
end

local function OnQuickClear()
	if Expanded:Get() then
		return
	end

	Editor.SetSource("")
end

local function OnImport()
	local File: File? = StudioService:PromptImportFile({ "blink", "txt" })
	if not File then
		return
	end

	Editing = nil
	Expanded:Set(false)
	Editor.SetSource(File:GetBinaryContents())
end

local function OnExpandRightClicked()
	if ProfilerClicks == 0 then
		ProfilerTick = os.clock()
		ProfilerClicks += 1
		return
	end

	local Elapsed = (os.clock() - ProfilerTick)
	if Elapsed > 1 then
		ProfilerTick = os.clock()
		return
	end

	ProfilerTick = 0
	ProfilerClicks = 0
	Profiler.toggle()
end

---- Initialization ----

Editor.Initialize(EditorWidget)
Profiler.Initialize()

---- Connections ----

Expanded:OnChange(function(Value)
	--Content.Visible = Value
	PlayTweens(Value and Tweens.Editor.Expand or Tweens.Editor.Retract)
end)

Selected:OnChange(function(Value: Instance?)
	Generate.TextTransparency = Value and 0 or 0.5
	Hint.Text = Value and `<b>Selected</b>\n{Value:GetFullName()}`
		or "Select the location in which to generate the output scripts."
end)

Generating:OnChange(function(File: StringValue?)
	GeneratePrompt.Visible = (File ~= nil)
end)

Selection.SelectionChanged:Connect(function()
	local Instances = Selection:Get()
	Selected:Set(Instances[1])
end)

Generate.MouseButton1Click:Connect(function()
	local File = Generating:Get()
	if not File then
		return
	end

	local Directory = Selected:Get()
	if not Directory then
		return
	end

	Generating:Set()
	GenerateFile(File, Directory)
end)

CancelGenerate.MouseButton1Click:Connect(function()
	Generating:Set()
end)

Import.MouseButton1Click:Connect(OnImport)
Search.FocusLost:Connect(OnSearch)
Save.MouseButton1Click:Connect(OnSaveActivated)
Cancel.MouseButton1Click:Connect(OnSaveCompleted)
Expand.MouseButton1Click:Connect(OnExpandActivated)
Expand.MouseButton2Click:Connect(OnExpandRightClicked)
QuickSave.MouseButton1Click:Connect(OnQuickSave)
QuickClear.MouseButton1Click:Connect(OnQuickClear)
EditorButton.Click:Connect(OnEditorButtonClicked)
EditorWidget:GetPropertyChangedSignal("Enabled"):Connect(OnEditorWidgetEnabled)


--!strict

---- Imports ----

local PathParser = require("../../Modules/Path")
local fs = require("@lune/fs")

---- Settings ----

local EXTENSIONS = { "", ".txt", ".blink" }

return function(Path: string): string
	local Directory, Filename = PathParser.Components(Path)
	Directory = Directory or "./"
	assert(Filename, `Unable to parse filename from {Path}, path may be malformed!`)

	for _, Extension in EXTENSIONS do
		local Temporary = `{Directory}{Filename}{Extension}`
		if fs.isFile(Temporary) then
			return Temporary
		end
	end

	return Path
end


--!strict

---- Imports ----

local dateTime = require("@lune/datetime")
local fs = require("@lune/fs")
local stdio = require("@lune/stdio")
local task = require("@lune/task")

local Compile = require("./Compile")
local GetDefinitionFilePath = require("./GetDefinitionFilePath")
local PathParser = require("../../Modules/Path")

---- Settings ----

type DateTime = dateTime.DateTime

local IMPORT_PATTERN = `import "([%w%/%.-_]+)"`
local WATCH_OPTIONS: Compile.CompileOptions = {
	Debug = false,
	Silent = true,
	Compact = false,
	YesToEverything = true,
	OutputAbstractSyntaxTree = false,
}

---- Constants ----

local Utility = {}

---- Variables ----

---- Private Functions ----

local function Color(Color: stdio.Color, Text: string): string
	return `{stdio.color(Color)}{Text}{stdio.color("reset")}`
end

local function BuildImportsArray(Entry: string, PreviousArray: { string }?): { string }
	local Array = PreviousArray or {}
	table.insert(Array, Entry)

	local Source = fs.readFile(Entry)
	local Directory = PathParser.Directory(Entry)
	local Iterator = string.gmatch(Source, IMPORT_PATTERN)

	while true do
		local Path = Iterator()
		if not Path then
			break
		end

		Path = `{Directory}{Path}`
		Path = GetDefinitionFilePath(Path)

		if not fs.isFile(Path) then
			error(`No file to import at "{Path}"`)
		end

		--> Prevent repeats
		if table.find(Array, Path) then
			continue
		end

		BuildImportsArray(Path, Array)
	end

	return Array
end

---- Public Functions ----

function Utility.Watch(Path: string)
	local FilePath = GetDefinitionFilePath(Path)
	local Imports: { string }

	_G.WATCH_THREAD = true

	local function Traverse()
		--> Resolve imports
		local OldImports = Imports and #Imports or 0
		Imports = BuildImportsArray(FilePath)

		if OldImports ~= #Imports then
			print(
				`Blink is watching for changes:\n\tEntry: {Color("yellow", FilePath)}\n\tImports: {Color(
					"yellow",
					tostring(#Imports - 1)
				)}`
			)
		end

		return true
	end

	local function Recompile()
		Traverse()
		Compile.Compile(Path, WATCH_OPTIONS)
	end

	--> Initial traversal
	local InitialSuccess, Why = pcall(Traverse)
	if not InitialSuccess then
		warn(`There was an error while trying to start the watcher thread:\n{Why}`)
		return
	end

	--> Watch loop
	local Timestamps: { [string]: number } = {}

	while true do
		local FileChanged = false
		for _, File in Imports do
			local Metadata = fs.metadata(File)

			--> Make sure file still exists
			if not Metadata.exists then
				continue
			end

			local ModifiedAt = Metadata.modifiedAt and Metadata.modifiedAt.unixTimestampMillis or 0
			local LastModifiedA = Timestamps[File]
			Timestamps[File] = ModifiedAt

			if ModifiedAt ~= LastModifiedA then
				FileChanged = true
			end
		end

		if FileChanged then
			pcall(Recompile)
		end

		task.wait(1)
	end
end

---- Initialization ----

---- Connections ----

return Utility