local MarketplaceService = game:GetService("MarketplaceService")
local Rorooms = script.Parent.Parent.Parent.Parent
local PlayerCharacterComponent = require(Rorooms.SourceCode.Server.Components.PlayerCharacterComponent)
local PlayerDataStoreService = require(Rorooms.SourceCode.Server.PlayerDataStore.PlayerDataStoreService)
local t = require(Rorooms.Parent.t)
local Config = require(Rorooms.Config).Config
local GamepassesHandler = require(Rorooms.SourceCode.Shared.ExtPackages.GamepassesHandler)
local Future = require(Rorooms.Parent.Future)

local EmotesService = {
	Name = "EmotesService",
	Client = {},
}

function EmotesService.Client:PlayEmote(Player: Player, EmoteId: string)
	assert(t.tuple(t.instanceOf("Player")(Player), t.strict(EmoteId)))

	return self.Server:PlayEmote(Player, EmoteId)
end

function EmotesService:PlayEmote(Player: Player, EmoteId: string)
	local Emote = Config.Systems.Emotes.Emotes[EmoteId]
	if Emote then
		local CanUse, FailureReason = self:CanPlayerUseEmote(Player, EmoteId, Emote)
		if CanUse then
			local Character = Player.Character
			if Character then
				local CharacterClass = PlayerCharacterComponent:FromInstance(Character)
				if CharacterClass then
					CharacterClass:PlayEmote(EmoteId)
					return true
				end
			end
		else
			return CanUse, FailureReason
		end
	else
		return false, "Emote does not exist."
	end

	return false, "Error occurred."
end

function EmotesService:CanPlayerUseEmote(Player: Player, EmoteId: string, Emote): (boolean, string?)
	if Emote.CallbackRequirement ~= nil then
		local CanUse, FailureReason = Emote.CallbackRequirement(Player, EmoteId, Emote)

		if (not CanUse) and (FailureReason == nil) then
			FailureReason = `Insuffient requirements to use {Emote.DisplayName} emote.`
		end

		if not CanUse then
			return false, FailureReason
		end
	end

	if Emote.LevelRequirement ~= nil then
		local Profile = PlayerDataStoreService:GetProfile(Player.UserId)
		if Profile ~= nil then
			if Profile.Data.Level < Emote.LevelRequirement then
				return false, `Emote requires level {Emote.LevelRequirement}.`
			end
		else
			return false, "Data error."
		end
	end

	if Emote.GamepassRequirement ~= nil then
		local OwnsGamepass = GamepassesHandler:PlayerOwnsGamepass(Player, Emote.GamepassRequirement)

		if OwnsGamepass then
			return true
		else
			Future.Try(function()
				MarketplaceService:PromptGamePassPurchase(Player, Emote.GamepassRequirement)
			end)

			return false
		end
	end

	return true
end

return EmotesService


local MarketplaceService = game:GetService("MarketplaceService")
local Rorooms = script.Parent.Parent.Parent.Parent
local PlayerDataStoreService = require(Rorooms.SourceCode.Server.PlayerDataStore.PlayerDataStoreService)
local t = require(Rorooms.Parent.t)
local Config = require(Rorooms.Config).Config
local Future = require(Rorooms.Parent.Future)
local GamepassesHandler = require(Rorooms.SourceCode.Shared.ExtPackages.GamepassesHandler)

local ItemsService = {
	Name = "ItemsService",
	Client = {},
}

function ItemsService.Client:ToggleEquipItem(Player: Player, ItemId: string, Hold: boolean?)
	assert(t.tuple(t.instanceOf("Player")(Player), t.string(ItemId)))

	return self.Server:ToggleEquipItemForPlayer(Player, ItemId, Hold)
end

function ItemsService:ToggleEquipItemForPlayer(Player: Player, ItemId: string, Hold: boolean?)
	if self:_PlayerHasItem(Player, ItemId) then
		return self:TakeItemFromPlayer(Player, ItemId)
	else
		if #self:_FindItemsInPlayer(Player) < Config.Systems.Items.MaxItemsEquippable then
			return self:GiveItemToPlayer(Player, ItemId, false, Hold)
		end
	end

	return false
end

function ItemsService:GiveItemToPlayer(Player: Player, ItemId: string, BypassRequirement: boolean?, Hold: boolean?)
	local Backpack = Player:FindFirstChild("Backpack")
	local Character = Player.Character
	if not Backpack then
		return
	end
	if not Character then
		return
	end

	local Item = Config.Systems.Items.Items[ItemId]
	if Item and Item.Tool then
		local AbleToEquip, FailureReason = self:CanPlayerAccessItem(Player, ItemId, BypassRequirement)

		if AbleToEquip then
			local ItemTool = Item.Tool:Clone()
			ItemTool.Parent = Backpack

			if Hold then
				local Humanoid = Character:FindFirstChildOfClass("Humanoid")
				if Humanoid then
					Humanoid:EquipTool(ItemTool)
				end
			end
		end
		return AbleToEquip, FailureReason
	end

	return false
end

function ItemsService:CanPlayerAccessItem(Player: Player, ItemId: string, Bypass: boolean?)
	if Bypass then
		return true
	end

	local Item = Config.Systems.Items.Items[ItemId]
	if Item ~= nil then
		if Item.CallbackRequirement then
			local AbleToEquip, FailureReason = Item.CallbackRequirement(Player, ItemId, Item)

			if (not AbleToEquip) and (FailureReason == nil) then
				FailureReason = "Insuffient requirements to equip " .. Item.DisplayName .. "."
			end

			return AbleToEquip, FailureReason
		end

		if Item.GamepassRequirement ~= nil then
			local OwnsGamepass = GamepassesHandler:PlayerOwnsGamepass(Player, Item.GamepassRequirement)

			if OwnsGamepass then
				return true
			else
				Future.Try(function()
					MarketplaceService:PromptGamePassPurchase(Player, Item.GamepassRequirement)
				end)

				return false
			end
		end

		if Item.LevelRequirement ~= nil then
			local Profile = PlayerDataStoreService:GetProfile(Player.UserId)
			if Profile then
				if Profile.Data.Level < Item.LevelRequirement then
					return false, Item.DisplayName .. " item requires level " .. Item.LevelRequirement .. "."
				end
			end
		end
	end

	return true
end

function ItemsService:TakeItemFromPlayer(Player: Player, ItemId: string)
	for _, ItemTool in ipairs(self:_FindItemsInPlayer(Player)) do
		if ItemTool:GetAttribute("RR_ItemId") == ItemId then
			ItemTool:Destroy()
		end
	end
end

function ItemsService:_FindItemsInPlayer(Player: Player)
	if not Player.Backpack or not Player.Character then
		return
	end
	local FoundItems = {}
	local function ScanDirectory(Directory: Instance)
		for _, Child in ipairs(Directory:GetChildren()) do
			if Child:IsA("Tool") and Child:GetAttribute("RR_ItemId") then
				table.insert(FoundItems, Child)
			end
		end
	end
	ScanDirectory(Player.Backpack)
	ScanDirectory(Player.Character)
	return FoundItems
end

function ItemsService:_PlayerHasItem(Player: Player, ItemId: string)
	local function ScanDirectory(Directory: Instance)
		for _, Child in ipairs(Directory:GetChildren()) do
			if Child:IsA("Tool") and Child:GetAttribute("RR_ItemId", ItemId) == ItemId then
				return true
			end
		end

		return false
	end

	if Player.Character ~= nil then
		return ScanDirectory(Player.Backpack) or ScanDirectory(Player.Character)
	end

	return false
end

for ItemId, Item in pairs(Config.Systems.Items.Items) do
	if Item.Tool then
		Item.Tool:SetAttribute("RR_ItemId", ItemId)
		Item.Tool.CanBeDropped = false
	else
		warn("No tool defined for " .. ItemId)
	end
end

return ItemsService


local SoundService = game:GetService("SoundService")

local Rorooms = script.Parent.Parent.Parent.Parent
local Knit = require(Rorooms.Parent.Knit)
local Config = require(Rorooms.Config).Config

local MusicService = {
	Name = "MusicService",
	Client = {
		CurrentSong = Knit.CreateProperty(),
	},

	CurrentIndex = 0,
	Songs = Config.Systems.Music.SoundGroup:GetChildren(),
}

function MusicService:PlayNextSong()
	if self.CurrentEndConnection then
		self.CurrentEndConnection:Disconnect()
		self.CurrentEndConnection = nil
	end
	if self.SongCopy then
		self.SongCopy:Destroy()
	end
	self.Client.CurrentSong:Set(nil)

	self.CurrentIndex += 1
	if not self.Songs[self.CurrentIndex] then
		self.CurrentIndex = 1
	end

	self.CurrentSong = self.Songs[self.CurrentIndex]
	if self.CurrentSong then
		self.Client.CurrentSong:Set(self.CurrentSong)
		self.SongCopy = self.CurrentSong:Clone()
		self.SongCopy.Parent = SoundService
		self.SongCopy.Volume = 0
		self.SongCopy:Play()
		self.CurrentEndConnection = self.SongCopy.Ended:Connect(function()
			self:PlayNextSong()
		end)
	end
end

function MusicService:KnitStart()
	self:PlayNextSong()
end

for _, Song in ipairs(MusicService.Songs) do
	if not Song:IsA("Sound") then
		Song:Destroy()
	end
	Song.Looped = false
	Song.PlayOnRemove = false
	Song.Playing = false
end

return MusicService


export type PlayerData = {
	Level: number,
	XP: number,
	MinutesSpent: number,
	Profile: {
		Nickname: string,
		Bio: string,
		Role: string?,
	},
}

local Template: PlayerData = {
	Level = 1,
	XP = 0,
	MinutesSpent = 0,
	Profile = {
		Nickname = "",
		Bio = "",
		Role = nil,
	},
}

return Template


local Players = game:GetService("Players")
local Rorooms = script.Parent.Parent.Parent.Parent
local PlayerDataStoreService = require(script.Parent.PlayerDataStoreService)
local XPToLevelUp = require(Rorooms.SourceCode.Shared.XPToLevelUp)
local Knit = require(Rorooms.Parent.Knit)
local DataTemplate = require(Rorooms.SourceCode.Server.PlayerDataStore.DataTemplate)
local Config = require(Rorooms.Config).Config
local SessionStore = require(Rorooms.SourceCode.Shared.ExtPackages.SessionStore)

local LevelingService = {
	Name = script.Name,
	Client = {
		Level = Knit.CreateProperty(DataTemplate.Level),
		XPMultipliers = Knit.CreateProperty({}),
	},
}

function LevelingService:_SetLevel(Player: Player, Level: number)
	return PlayerDataStoreService:UpdateData(Player, function(Data)
		Data.Level = Level
		return Data
	end)
end

function LevelingService:ChangeXP(Player: Player, Amount: number): (boolean, number?)
	Amount = math.floor(Amount)

	local Profile = PlayerDataStoreService:GetProfile(Player.UserId)
	if Profile then
		local MaxAllowedXP = (
			XPToLevelUp(Profile.Data.Level)
			+ XPToLevelUp(Profile.Data.Level + 1)
			+ XPToLevelUp(Profile.Data.Level + 2)
		)

		if Amount > MaxAllowedXP then
			return false, MaxAllowedXP
		end

		local Success = PlayerDataStoreService:UpdateData(Player, function(Data)
			local RequiredXPToLevel = XPToLevelUp(Data.Level)
			if (Data.XP + Amount) > RequiredXPToLevel then
				local RemainingXP = (Data.XP + Amount) - RequiredXPToLevel
				Data.Level += 1
				Data.XP = 0
				self:ChangeXP(Player, RemainingXP)
			elseif (Data.XP + Amount) == RequiredXPToLevel then
				Data.Level += 1
				Data.XP = 0
			else
				Data.XP += Amount
			end

			return Data
		end)
		if Success then
			self:_SetLevel(Player, Profile.Data.Level)
		end

		return true
	end

	return false
end

function LevelingService:SetXPMultiplier(Player: Player, Name: string, MultiplierAddon: number | nil)
	local Profile = PlayerDataStoreService:GetProfile(Player.UserId)
	local SessionProfile = SessionStore:GetProfileSafe(Player)
	if Profile then
		SessionProfile.Data.XPMultipliers[Name] = MultiplierAddon
		self.Client.XPMultipliers:SetFor(Player, SessionProfile.XPMultipliers)
	end
end

function LevelingService:_UpdateAllFriendMultipliers()
	for _, Player in ipairs(Players:GetPlayers()) do
		task.spawn(function()
			local FriendsInGame = false
			for _, OtherPlayer in ipairs(Players:GetPlayers()) do
				if OtherPlayer ~= Player and OtherPlayer:IsFriendsWith(Player.UserId) then
					FriendsInGame = true
					break
				end
			end
			local BaseMultiplier = 0.5
			self:SetXPMultiplier(Player, "Friends", (FriendsInGame and BaseMultiplier) or 0)
		end)
	end
end

function LevelingService:KnitStart()
	PlayerDataStoreService.ProfileLoaded:Connect(function(Profile: PlayerDataStoreService.Profile)
		self:_UpdateAllFriendMultipliers()

		Profile.Player:SetAttribute("RR_Level", Profile.Data.Level)
		self.Client.Level:SetFor(Profile.Player, Profile.Data.Level)
	end)

	task.spawn(function()
		while task.wait(1 * 60) do
			for _, Profile in pairs(PlayerDataStoreService:GetProfiles()) do
				local function CalculateTotal(BaseNum: number, MultiplierAddons: { [any]: any })
					local Total = BaseNum
					local TotalMultiplier = 1
					for _, MultiplierAddon in pairs(MultiplierAddons) do
						TotalMultiplier += MultiplierAddon
					end
					Total *= TotalMultiplier
					return Total
				end

				local SessionProfile = SessionStore:GetProfileSafe(Profile.Player)
				local XPTotal = CalculateTotal(Config.Systems.Leveling.XPPerMinute, SessionProfile.Data.XPMultipliers)
				self:ChangeXP(Profile.Player, XPTotal)
			end
		end
	end)

	PlayerDataStoreService.DataUpdated:Connect(
		function(
			Player: Player,
			OldData: PlayerDataStoreService.ProfileData,
			NewData: PlayerDataStoreService.ProfileData
		)
			if OldData.Level ~= NewData.Level then
				self.Client.Level:SetFor(Player, NewData.Level)
				Player:SetAttribute("RR_Level", NewData.Level)
			end
		end
	)
end

return LevelingService


local Players = game:GetService("Players")

local Rorooms = script.Parent.Parent.Parent.Parent
local ProfileService = require(Rorooms.Parent.ProfileService)
local Signal = require(Rorooms.Parent.Signal)
local DataTemplate = require(script.Parent.DataTemplate)
local DeepCopy = require(Rorooms.SourceCode.Shared.ExtPackages.DeepCopy)
local DeepEquals = require(Rorooms.SourceCode.Shared.ExtPackages.DeepEquals)

local KICK_MESSAGE = "Data failure. Please rejoin."

export type ProfileData = typeof(DataTemplate)
export type Profile = ProfileService.Profile<ProfileData, { Player: Player }, {}> & {
	Player: Player?,
}

local PlayerDataStoreService = {
	Name = "PlayerDataStoreService",

	ProfileStore = ProfileService.GetProfileStore("RoRoomsPlayerData", DataTemplate),
	ProfileLoaded = Signal.new(),
	DataUpdated = Signal.new(),
	Profiles = {},
}

function PlayerDataStoreService:UpdateData(Player: Player, Callback: (ProfileData) -> ProfileData)
	local Profile = self:GetProfile(Player.UserId)
	if Profile then
		local OldData = Profile.Data
		local NewData = Callback(DeepCopy(OldData))

		if (NewData ~= nil) and (not DeepEquals(OldData, NewData)) then
			Profile.Data = NewData

			self.DataUpdated:Fire(Player, OldData, NewData)
			return true
		end
	end

	return false
end

function PlayerDataStoreService:GetProfiles(): { [number]: Profile }
	return self.Profiles
end

function PlayerDataStoreService:GetProfile(UserId: number): Profile?
	return self.Profiles[UserId]
end

function PlayerDataStoreService:_AddPlayer(Player: Player)
	if Player.UserId < 1 then
		return
	end

	local Profile = self.ProfileStore:LoadProfileAsync(tostring(Player.UserId))
	if Profile ~= nil then
		Profile:AddUserId(Player.UserId)
		Profile:Reconcile()
		Profile:ListenToRelease(function()
			self.Profiles[Player.UserId] = nil
			Player:Kick(KICK_MESSAGE)
		end)
		Profile.Player = Player

		if Player:IsDescendantOf(Players) then
			self.Profiles[Player.UserId] = Profile
			self.ProfileLoaded:Fire(Profile)
		else
			Profile:Release()
			return
		end
	else
		Player:Kick(KICK_MESSAGE)
	end
end

function PlayerDataStoreService:_RemovePlayer(Player: Player)
	local Profile = self:GetProfile(Player.UserId)
	if Profile ~= nil then
		Profile:Release()
	end
end

function PlayerDataStoreService:KnitStart()
	for _, Player in ipairs(Players:GetPlayers()) do
		task.spawn(function()
			self:_AddPlayer(Player)
		end)
	end
	Players.PlayerAdded:Connect(function(Player)
		self:_AddPlayer(Player)
	end)

	Players.PlayerRemoving:Connect(function(Player)
		self:_RemovePlayer(Player)
	end)
end

return PlayerDataStoreService


local PlayerStatsService = {
	Name = script.Name,
}

return PlayerStatsService


local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local UserService = game:GetService("UserService")

local Rorooms = script.Parent.Parent.Parent.Parent
local Knit = require(Rorooms.Parent.Knit)
local FilterString = require(Rorooms.SourceCode.Shared.ExtPackages.FilterString)
local PlayerDataStoreService = require(Rorooms.SourceCode.Server.PlayerDataStore.PlayerDataStoreService)
local t = require(Rorooms.Parent.t)
local Config = require(Rorooms.Config).Config
local Future = require(Rorooms.Parent.Future)
local Types = require(Rorooms.SourceCode.Shared.Types)
local GamepassesHandler = require(Rorooms.SourceCode.Shared.ExtPackages.GamepassesHandler)

local ProfilesService = {
	Name = "ProfilesService",
	Client = {
		ProfileUpdated = Knit.CreateSignal(),
	},
}

function ProfilesService.Client:GetProfile(Player: Player, UserId: number)
	assert(t.tuple(t.instanceOf("Player")(Player), t.number(UserId)))

	return ProfilesService:GetProfile(UserId)
end

function ProfilesService.Client:SetRole(Player: Player, RoleId: string): boolean
	assert(t.tuple(t.instanceOf("Player")(Player), t.string(RoleId)))

	return ProfilesService:SetRole(Player, RoleId)
end

function ProfilesService.Client:SetNickname(Player: Player, Nickname: string)
	assert(t.tuple(t.instanceOf("Player")(Player), t.string(Nickname)))
	assert(utf8.len(Nickname) <= Config.Systems.Profiles.NicknameCharacterLimit, "Nickname exceeds character limit")

	ProfilesService:SetNickname(Player, FilterString(Nickname, Player))
end

function ProfilesService.Client:SetBio(Player: Player, Bio: string)
	assert(t.tuple(t.instanceOf("Player")(Player), t.string(Bio)))
	assert(utf8.len(Bio) <= Config.Systems.Profiles.BioCharacterLimit, "Bio exceeds character limit")

	ProfilesService:SetBio(Player, FilterString(Bio, Player))
end

function ProfilesService:GetProfile(UserId: number): Types.Profile
	local Profile = {}
	local DataProfile = PlayerDataStoreService:GetProfile(UserId)
	local Player = Players:GetPlayerByUserId(UserId)

	if DataProfile then
		Profile.Nickname = DataProfile.Data.Profile.Nickname
		Profile.Bio = DataProfile.Data.Profile.Bio
		Profile.Role = DataProfile.Data.Profile.Role
		Profile.Level = DataProfile.Data.Level
	end

	if Player then
		Profile.DisplayName = Player.DisplayName
		Profile.Username = Player.Name
	else
		local Success, Result = Future.Try(function()
			return UserService:GetUserInfosByUserIdsAsync({ UserId })[1]
		end):Await()
		if Success and Result then
			Profile.DisplayName = Result.DisplayName
			Profile.Username = Result.Username
		end
	end

	return Profile
end

function ProfilesService:SetRole(Player: Player, RoleId: string): (boolean, string?)
	if (RoleId == nil) and (Config.Systems.Profiles.DefaultRoleId ~= nil) then
		RoleId = Config.Systems.Profiles.DefaultRoleId
	end

	local RoleToSet = nil
	local Role = Config.Systems.Profiles.Roles[RoleId]

	if Role ~= nil then
		RoleToSet = RoleId
	end

	local AbleToUse, FailureReason = self:_CanPlayerAccessRole(Player, RoleId)
	if AbleToUse then
		Player:SetAttribute("RR_Role", RoleToSet)

		return PlayerDataStoreService:UpdateData(Player, function(Data)
			Data.Profile.Role = RoleToSet
			return Data
		end)
	else
		return false, FailureReason
	end
end

function ProfilesService:SetNickname(Player: Player, Nickname: string)
	Player:SetAttribute("RR_Nickname", Nickname)

	return PlayerDataStoreService:UpdateData(Player, function(Data)
		Data.Profile.Nickname = Nickname
		return Data
	end)
end

function ProfilesService:SetBio(Player: Player, Bio: string)
	Player:SetAttribute("RR_Bio", Bio)

	return PlayerDataStoreService:UpdateData(Player, function(Data)
		Data.Profile.Bio = Bio
		return Data
	end)
end

function ProfilesService:_CanPlayerAccessRole(Player: Player, RoleId: string, Bypass: boolean?)
	local Role: Config.Role? = Config.Systems.Profiles.Roles[RoleId]

	if Bypass == true then
		return true
	end

	if Role ~= nil then
		if Role.CallbackRequirement then
			local AbleToUse, FailureReason = Role.CallbackRequirement(Player, RoleId, Role)

			if (not AbleToUse) and (FailureReason == nil) then
				FailureReason = "Insuffient requirements to set " .. Role.Name .. "."
			end

			return AbleToUse, FailureReason
		end

		if Role.GamepassRequirement ~= nil then
			local OwnsGamepass = GamepassesHandler:PlayerOwnsGamepass(Player, Role.GamepassRequirement)

			if OwnsGamepass then
				return true
			else
				Future.Try(function()
					MarketplaceService:PromptGamePassPurchase(Player, Role.GamepassRequirement)
				end)

				return false
			end
		end

		if Role.LevelRequirement ~= nil then
			local Profile = PlayerDataStoreService:GetProfile(Player.UserId)
			if Profile then
				if Profile.Data.Level < Role.LevelRequirement then
					return false, Role.Name .. " role requires level " .. Role.LevelRequirement .. "."
				end
			end
		end
	end

	return true
end

function ProfilesService:_UpdateFromDataStoreProfile(Player: Player)
	if not Player then
		return
	end

	local Profile = PlayerDataStoreService:GetProfile(Player.UserId)
	if Profile then
		self:SetNickname(Player, Profile.Data.Profile.Nickname)
		self:SetBio(Player, Profile.Data.Profile.Bio)
		self:SetRole(Player, Profile.Data.Profile.Role)
	end
end

function ProfilesService:_CheckDefaultRole()
	local DefaultRoleId = Config.Systems.Profiles.DefaultRoleId
	local DefaultRole = Config.Systems.Profiles.Roles[DefaultRoleId]

	if DefaultRoleId ~= nil then
		if DefaultRole == nil then
			assert(false, "DefaultRoleId is set to a nonexistent role.")
		else
			assert(DefaultRole.CallbackRequirement == nil, "The default role cannot have a CallbackRequirement.")
		end
	end
end

function ProfilesService:KnitStart()
	PlayerDataStoreService.ProfileLoaded:Connect(function(Profile: PlayerDataStoreService.Profile)
		self:_UpdateFromDataStoreProfile(Profile.Player)
	end)
	for _, Profile in pairs(PlayerDataStoreService:GetProfiles()) do
		self:_UpdateFromDataStoreProfile(Profile.Player)
	end

	self:_CheckDefaultRole()

	PlayerDataStoreService.DataUpdated:Connect(
		function(
			Player: Player,
			OldData: PlayerDataStoreService.ProfileData,
			NewData: PlayerDataStoreService.ProfileData
		)
			if OldData.Profile ~= NewData.Profile then
				self.Client.ProfileUpdated:FireAll(Player.UserId)
			end
		end
	)

	PlayerDataStoreService.ProfileLoaded:Connect(function(Profile: PlayerDataStoreService.Profile)
		self.Client.ProfileUpdated:FireAll(Profile.Player.UserId)
	end)
end

return ProfilesService


local Rorooms = script.Parent.Parent.Parent.Parent
local WorldRegistryService = require(script.Parent.WorldRegistryService)
local ShuffleArray = require(Rorooms.SourceCode.Shared.ExtPackages.ShuffleArray)
local Knit = require(Rorooms.Parent.Knit)
local t = require(Rorooms.Parent.t)
local GetEntriesFromArray = require(Rorooms.SourceCode.Shared.ExtPackages.GetEntriesFromArray)

type World = {
	PlaceId: number,
}

local ROW_SIZE = 3

local RandomWorldsService = {
	Name = script.Name,
	Client = {
		RandomWorldsInitialized = Knit.CreateSignal(),
	},

	RandomWorlds = {},
}

function RandomWorldsService.Client:GetRandomWorlds(Player: Player, StartIndex: number, EndIndex: number)
	assert(t.tuple(t.instanceOf("Player")(Player), t.number(StartIndex), t.number(EndIndex)))

	return RandomWorldsService:GetRandomWorlds(StartIndex, EndIndex)
end

function RandomWorldsService:GetRandomWorlds(StartIndex: number, EndIndex: number)
	return GetEntriesFromArray(self.RandomWorlds, StartIndex, EndIndex)
end

function RandomWorldsService:_UpdateRandomWorlds(WorldRegistry: { [number]: World })
	local RandomWorlds = {}

	for PlaceId, _ in pairs(WorldRegistry) do
		table.insert(RandomWorlds, {
			PlaceId = tonumber(PlaceId),
		})
	end

	self.RandomWorlds = ShuffleArray(RandomWorlds)

	self.Client.RandomWorldsInitialized:FireAll(self:GetRandomWorlds(0, 10 * ROW_SIZE))
end

function RandomWorldsService:KnitStart() end

WorldRegistryService.RegistryUpdated:Connect(function(WorldRegistry: { [number]: World })
	RandomWorldsService:_UpdateRandomWorlds(WorldRegistry)
end)

return RandomWorldsService


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

local Rorooms = script.Parent.Parent.Parent.Parent
local Future = require(Rorooms.Parent.Future)
local WorldRegistryService = require(Rorooms.SourceCode.Server.Worlds.WorldRegistryService)
local Knit = require(Rorooms.Parent.Knit)
local t = require(Rorooms.Parent.t)
local GetEntriesFromArray = require(Rorooms.SourceCode.Shared.ExtPackages.GetEntriesFromArray)

type TopWorld = {
	PlaceId: number,
	Teleports: number,
}
type Page = {
	[number]: TopWorld,
}

local DATASTORE_NAME = "RR_WorldTeleports"
local PAGE_ADVANCE_DELAY = 60
local REINITIALIZATION_DELAY = 30 * 60
local ROW_SIZE = 3
local WORLD_PERSISTENCE_TIME = (((60 * 60) * 24) * 7)

local TopWorldsService = {
	Name = script.Name,
	Client = {
		TopWorldsInitialized = Knit.CreateSignal(),
	},

	TopWorlds = {},
	TeleportsStore = DataStoreService:GetOrderedDataStore(
		DATASTORE_NAME,
		tostring(math.floor(os.time() / WORLD_PERSISTENCE_TIME))
	),
	LastWeekTeleportsStore = DataStoreService:GetOrderedDataStore(
		DATASTORE_NAME,
		tostring(math.floor(os.time() / WORLD_PERSISTENCE_TIME) - 1)
	),
}

function TopWorldsService.Client:GetTopWorlds(Player: Player, StartIndex: number, EndIndex: number)
	assert(t.tuple(t.instanceOf("Player")(Player), t.number(StartIndex), t.number(EndIndex)))

	return TopWorldsService:GetTopWorlds(StartIndex, EndIndex)
end

function TopWorldsService:GetTopWorlds(StartIndex: number, EndIndex: number)
	return GetEntriesFromArray(TopWorldsService.TopWorlds, StartIndex, EndIndex)
end

function TopWorldsService:_SpawnInitializationLoop()
	return task.spawn(function()
		while true do
			local Success, Result = Future.Try(function()
				self:_ClearTopWorlds()
				self:_SpawnUpdateLoop()
				self:_SpawnLastWeekUpdateLoop()
			end)
			if not Success then
				warn(Result)
			end

			task.wait(REINITIALIZATION_DELAY)
		end
	end)
end

function TopWorldsService:_SpawnUpdateLoop()
	if self.UpdateLoop then
		task.cancel(self.UpdateLoop)
		self.UpdateLoop = nil
	end

	self.UpdateLoop = task.spawn(function()
		local Pages = self.TeleportsStore:GetSortedAsync(false, 100)
		local AdvancedLastCheck = true

		self:_LoadPage(Pages:GetCurrentPage())
		AdvancedLastCheck = self:_AdvanceToNextPage(Pages)

		self.Client.TopWorldsInitialized:FireAll(self:GetTopWorlds(0, 10 * ROW_SIZE))

		while task.wait(PAGE_ADVANCE_DELAY) do
			if AdvancedLastCheck then
				self:_LoadPage(Pages:GetCurrentPage())
			end
			AdvancedLastCheck = self:_AdvanceToNextPage(Pages)
		end
	end)
end

function TopWorldsService:_SpawnLastWeekUpdateLoop()
	if self.LastWeekUpdateLoop then
		task.cancel(self.LastWeekUpdateLoop)
		self.LastWeekUpdateLoop = nil
	end

	self.LastWeekUpdateLoop = task.spawn(function()
		local LastWeekPages = self.LastWeekTeleportsStore:GetSortedAsync(false, 100)
		local AdvancedLastCheck = true

		self:_LoadPage(LastWeekPages:GetCurrentPage())
		AdvancedLastCheck = self:_AdvanceToNextPage(LastWeekPages)

		while task.wait(PAGE_ADVANCE_DELAY) do
			if AdvancedLastCheck then
				self:_LoadPage(LastWeekPages:GetCurrentPage())
			end
			AdvancedLastCheck = self:_AdvanceToNextPage(LastWeekPages)
		end
	end)
end

function TopWorldsService:_FindEntryFromPlaceId(PlaceId: number)
	for _, TopWorld: TopWorld in ipairs(self.TopWorlds) do
		if TopWorld.PlaceId == PlaceId then
			return TopWorld
		end
	end

	return nil
end

function TopWorldsService:_LoadPage(Page: Page)
	for _, Entry in ipairs(Page) do
		local PlaceId = tonumber(Entry.key)
		local Teleports = tonumber(Entry.value)

		local ExistingEntry = self:_FindEntryFromPlaceId(PlaceId)
		if ExistingEntry then
			ExistingEntry.Teleports += Teleports
		else
			local World = WorldRegistryService:GetWorldDetails(PlaceId)
			if (World ~= nil) and not (World.delisted or World.forceDelisted) then
				table.insert(self.TopWorlds, {
					PlaceId = PlaceId,
					Teleports = Teleports,
				})
			end
		end
	end
end

function TopWorldsService:_AdvanceToNextPage(Pages: DataStorePages)
	if Pages.IsFinished then
		return false
	else
		local Success, Result = Future.Try(function()
			return Pages:AdvanceToNextPageAsync()
		end):Await()
		if not Success then
			warn(Result)
		end

		return Success, Result
	end
end

function TopWorldsService:_ClearTopWorlds()
	self.TopWorlds = {}
end

function TopWorldsService:_LogIncomingTeleport(PlaceId: number)
	if WorldRegistryService:IsWorldRegistered(PlaceId) then
		local Success, Result = Future.Try(function()
			return self.TeleportsStore:IncrementAsync(tostring(PlaceId), 1)
		end):Await()
		if not Success then
			warn(Result)
		end

		return Success, Result
	else
		return false
	end
end

function TopWorldsService:KnitStart()
	Players.PlayerAdded:Connect(function(Player)
		local JoinData = Player:GetJoinData()
		if JoinData and JoinData.SourcePlaceId then
			self:_LogIncomingTeleport(JoinData.SourcePlaceId)
		end
	end)
end

WorldRegistryService.RegistryUpdated:Once(function()
	TopWorldsService._InitializationLoop = TopWorldsService:_SpawnInitializationLoop()
end)

return TopWorldsService


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

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

local WorldsService = {
	Name = script.Name,
	Client = {},
}

function WorldsService.Client:TeleportToWorld(Player: Player, PlaceId: number)
	assert(t.tuple(t.instanceOf("Player")(Player), t.number(PlaceId)))

	return WorldsService:TeleportPlayerToWorld(Player, PlaceId)
end

function WorldsService:TeleportPlayerToWorld(Player: Player, PlaceId: number)
	if RunService:IsStudio() == false then
		TeleportService:TeleportAsync(PlaceId, { Player })

		return true
	else
		return false, "Cannot teleport in Roblox Studio."
	end
end

return WorldsService


return {
	RoroomsLogo = "rbxassetid://124390954251956",
	RoroomsGlyph = "rbxassetid://97237065883195",
}


local FEATURE_MODULES_MAP = {
	Profiles = { "ProfilesService", "ProfilesController" },
	Items = { "ItemsService", "ItemsController" },
	Emotes = { "EmotesService", "EmotesController" },
	Worlds = {
		"WorldsService",
		"WorldRegistryService",
		"TopWorldsService",
		"RandomWorldsService",
		"WorldsController",
	},
	Music = { "MusicService", "MusicController" },
	Settings = { "SettingsController" },
	Friends = { "FriendsController" },
	Leveling = { "LevelingService" },
}

return function(Module)
	for Feature, ModuleNames in pairs(FEATURE_MODULES_MAP) do
		if table.find(ModuleNames, Module.Name) then
			return Feature
		end
	end

	return nil
end


export type Profile = {
	Nickname: string?,
	DisplayName: string,
	Username: string,
	Bio: string?,
	Role: string?,
	Level: number,
}

return nil


local XPToLevelUp = require(script.Parent.XPToLevelUp)

return function(GoalLevel: number)
  local Level = 0
  local TotalXP = 0
  for _ = 0, GoalLevel do
    TotalXP += XPToLevelUp(Level)
    Level += 1
  end
  return TotalXP
end


local Rorooms = script.Parent.Parent.Parent
local Config = require(Rorooms.Config).Config

local CURVE_MULTIPLIER = 0.85

return function(Level: number)
	Level = math.floor(Level)
	local Result = Config.Systems.Leveling.BaseLevelUpXP * (CURVE_MULTIPLIER * ((Level % 5) + (Level / 5)))
	if Result > 0 then
		return Result
	else
		return Config.Systems.Leveling.BaseLevelUpXP
	end
end


_G.NOCOLOR = _G.NOCOLOR

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packages = ReplicatedStorage.Packages

local Jest = require("@DevPackages/Jest")

local runCLIOptions = {
	verbose = true,
	ci = false,
}

local projects = {
	Packages.Project,
}

Jest.runCLI(script, runCLIOptions, projects):await()

return nil


local Poll = require("./Poll")
export type Poll<E, U...> = Poll.Poll<E, U...>

local Result = require("./Result")
export type Result<E, U...> = Result.Result<E, U...>

--- @class Future
---
--- A Future represents a read-only asynchronous value, one that may not
--- have finished computation.
---
--- Futures are lazy in their computation, meaning execution will not begin
--- until [Future:poll] or [Future:await] is used.
---
--- ## Types
---
--- You should refer to the [Typechecking Guide](/docs/typechecking) for more information.
---
--- ### Future
---
--- This type should only be used when returning a Future from a function,
--- using Futures.FutureLike will not provide intellisense.
---
--- ### FutureLike
---
--- This type should be used to typecheck function parameters, using
--- Futures.Future will not work as expected.
---
local Future = {}
Future.__index = Future

--- @method after
--- @within Future
--- @param fn (Result<E, U...>) -> Future<E, T...>
--- @return Future<E, T...>
---
--- After completion, passes the Result of the current future
--- to the closure, returning a new Future.
function Future:after(fn)
	-- DEVIATION: `then` is a reserved keyword in Luau,
	-- this method is equivalent to FutureExt::then
	return self.new(function()
		local future = fn(self:await())
		local result = future:await()

		if result:isOk() then
			return result:unwrapOk()
		elseif result:isErr() then
			error(result:unwrapErr(), 0)
		end
	end)
end

--- @method andThen
--- @within Future
--- @param fn <T>(U...) -> Future<E, T...>
--- @return Future<E, T...>
---
--- After successfully resolving, create and execute another Future
--- created within the closure, with the Ok result passed in the
--- closure arguments, otherwise it is never executed.
function Future:andThen(fn)
	return self.new(function()
		local result = self:await()

		if result:isOk() then
			local newFuture = fn(result:unwrapOk())
			local newResult = newFuture:await()

			if newResult:isOk() then
				return newResult:unwrapOk()
			elseif newResult:isErr() then
				error(newResult:unwrapErr(), 0)
			end
		elseif result:isErr() then
			error(result:unwrapErr(), 0)
		end
	end)
end

--- @method await
--- @within Future
--- @yields
--- @return Result<E, U...>
---
--- Yields until the Future finishes execution, then returns the result.
---
--- :::warning
--- Because this is a yielding method, it is suggested that it is only
--- used within Futures, as opposed to the main thread. \
--- \
--- See [Future:poll] for the recommended way of executing Futures.
--- :::
function Future:await()
	local poll = self:poll()

	if not poll:isReady() then
		table.insert(self._yieldedThreads, coroutine.running())
		coroutine.yield()
	end

	poll = self:poll()
	return poll:unwrap()
end

--- @method inspectErr
--- @within Future
--- @param fn (E) -> ()
--- @return Future<E, U...>
---
--- Allows you to read the error value of a Future before passing it on.
function Future:inspectErr(fn)
	table.insert(self._onCompletion, {
		on = "Err",
		type = "inspect",
		fn = fn,
	})

	return self
end

--- @method inspectOk
--- @within Future
--- @param fn (U...) -> ()
--- @return Future<E, U...>
---
--- Allows you to read the success value of a Future before passing it on.
function Future:inspectOk(fn)
	table.insert(self._onCompletion, {
		on = "Ok",
		type = "inspect",
		fn = fn,
	})

	return self
end

--- @method join
--- @within Future
--- @param otherFuture Future<E, T...>
--- @return Future<never, { Result<E, never> | (U..., T...) }>
---
--- Joins the results of two futures into a table.
---
--- Futures of different types, Err and Ok will still
--- have their results joined into a table.
---
--- Results of type Ok will be unwrapped in the table,
--- whereas Results of type Err will not be unwrapped
--- and will be added as `Result<E, never>` in the table.
function Future:join(otherFuture)
	return self.new(function()
		local result = self:await()
		local otherResult = otherFuture:await()

		-- unwrapOk and unwrapErr are functionally similar
		local results = result:isOk() and { result:unwrapOk() } or { result }

		if otherResult:isOk() then
			for _, v in { otherResult:unwrapOk() } do
				table.insert(results, v)
			end
		else
			table.insert(results, otherResult)
		end

		return results
	end)
end

--- @method joinAll
--- @within Future
--- @param ... Future<E, ...any>
--- @return Future<never, { Result<E, never> | ...any }>
---
--- Joins the results of two or more futures into a table.
function Future:joinAll(...)
	local previousFuture = self

	for i = 1, select("#", ...) do
		previousFuture = self.join(previousFuture, select(i, ...)):mapOk(function(t)
			return table.unpack(t)
		end)
	end

	return previousFuture
end

--- @method mapErr
--- @within Future
--- @param fn (E) -> T
--- @return Future<T, U...>
---
--- Maps the type of the Err result of a Future.
function Future:mapErr(fn)
	table.insert(self._onCompletion, {
		on = "Err",
		type = "map",
		fn = fn,
	})

	return self
end

--- @method mapOk
--- @within Future
--- @param fn (U...) -> T...
--- @return Future<E, T...>
---
--- Maps the type of the Ok result of a Future.
function Future:mapOk<E, U..., T...>(fn)
	table.insert(self._onCompletion, {
		on = "Ok",
		type = "map",
		fn = fn,
	})

	return self
end

--- @method orElse
--- @within Future
--- @param fn (E) -> Future<E, U...>
--- @return Future<E, U...>
---
--- On Err, executes another Future of the same type.
function Future:orElse(fn)
	return self.new(function()
		local result = self:await()

		if result:isOk() then
			return result:unwrapOk()
		elseif result:isErr() then
			local newFuture = fn(result:unwrapErr())
			local newResult = newFuture:await()

			if newResult:isOk() then
				return newResult:unwrapOk()
			elseif newResult:isErr() then
				error(newResult:unwrapErr(), 0)
			end
		end
	end)
end

--- @method unwrapOrElse
--- @within Future
--- @param fn (E) -> U...
--- @return Future<never, U...>
---
--- On Err, the result is passed to the closure to create a Ok result,
--- then returns a Future with that Ok result.
function Future:unwrapOrElse(fn)
	return self.new(function()
		local result = self:await()

		if result:isOk() then
			return result:unwrapOk()
		elseif result:isErr() then
			return fn(result:unwrapErr())
		end
	end)
end

--- @method poll
--- @within Future
--- @return Poll<E, U...>
---
--- Executes the Future on it's next resumption point,
--- returning `Result::Pending` if it is not ready yet or
--- `Result::Ready` if it is ready.
---
--- :::danger
--- Polling a Future that is ready will return the same result.
--- This will however not be guaranteed behavior, and you should
--- avoid polling a Future that is already ready.
--- :::
function Future:poll()
	local threadExists = self.thread ~= nil

	if not threadExists then
		local thread = coroutine.create(function(...)
			local success, result = xpcall(function(...)
				local results = { self._callback(...) }

				for _, command in self._onCompletion do
					if command.on == "Err" then
						continue
					end

					if command.type == "map" then
						results = { command.fn(table.unpack(results)) }
					else
						command.fn(table.unpack(results))
					end
				end

				return results
			end, function(err)
				for _, command in self._onCompletion do
					if command.on == "Ok" then
						continue
					end

					if command.type == "map" then
						_, err = pcall(command.fn, err)
					else
						command.fn(err)
					end
				end

				return err
			end, ...)

			if not success then
				result = { result }
			end

			self._threadStatus = {
				success = success,
				result = result,
			}

			for _, yieldedThread in self._yieldedThreads do
				if coroutine.status(yieldedThread) ~= "suspended" then
					continue
				end

				coroutine.resume(yieldedThread)
			end
		end)

		self.thread = thread
		coroutine.resume(thread, table.unpack(self._arguments))
	end

	local ready = if self._threadStatus then true else false
	local success = if ready then self._threadStatus.success else false
	local result = if ready then self._threadStatus.result else {}

	local resultType: ("Ok" | "Err")? = if success then "Ok" elseif ready then "Err" else nil

	if resultType == "Ok" then
		return Poll.ok(table.unpack(result))
	elseif resultType == "Err" then
		return Poll.err(table.unpack(result))
	else
		return Poll.notReady()
	end
end

--- @function new
--- @within Future
--- @param callback (T...) -> U...
--- @param ... T...
--- @return Future<E, U...>
---
--- Creates a new Future, taking an asynchronous callback and
--- parameters to pass into that callback.
function Future.new(callback, ...)
	return setmetatable({
		_onCompletion = {},
		_arguments = table.pack(...),
		_callback = callback,
		_yieldedThreads = {},
	}, Future)
end

return Future


local Result = require("./Result")
type Result<E, U...> = Result.Result<E, U...>

type PollImpl<E, U...> = {
	isReady: (self: Poll<E, U...>) -> boolean,
	isPending: (self: Poll<E, U...>) -> boolean,
	unwrap: (self: Poll<E, U...>) -> Result<E, U...>,
}

--- @class Poll
---
--- A Poll represents the status of a [Result],
--- whether that result is Ready or Pending.
---
--- ```lua
--- local poll = Poll.ok(...)
---
--- if poll:isReady() then
--- 	local result = poll:unwrap()
--- 	-- ...
--- elseif poll:isPending() then
--- 	-- ...
--- end
--- ```
local Poll = {}
Poll.__index = Poll

function Poll:__tostring()
	if self:isReady() then
		local result = self:unwrap()
		return `Poll::Ready({result})`
	elseif self:isPending() then
		return "Poll::Pending"
	end

	return "Poll::Unknown"
end

export type Poll<E, U...> = {
	_ready: boolean,
	_results: { any },
	_resultType: ("Ok" | "Err")?,
} & PollImpl<E, U...>

--- @within Poll
---
--- Checks whether the Result of a Poll is ready.
--- ```lua
--- local poll = future:poll()
--- if poll:isReady() then
--- 	local result = poll:unwrap()
--- 	-- ...
--- end
--- ```
function Poll:isReady(): boolean
	return self._ready
end

--- @within Poll
---
--- Checks whether the Result of a Poll is pending.
--- ```lua
--- local poll = future:poll()
--- if poll:isPending() then
--- 	continue
--- end
--- ```
function Poll:isPending(): boolean
	return not self._ready
end

--- @within Poll
--- @error Attempt to unwrap a pending future -- To prevent this, use Poll:isReady() or Future:await() to ensure the Result is ready.
---
--- Checks whether the Result of a Poll is ready.
--- ```lua
--- local poll = future:poll()
--- if poll:isReady() then
--- 	local result = poll:unwrap()
--- 	-- ...
--- else
--- 	local result = poll:unwrap() --! Errors
--- 	-- Error: Attempt to unwrap a pending future
--- end
--- ```
function Poll:unwrap<E, U...>(): Result<E, U...>
	if not self._ready then
		error("Attempt to unwrap a pending future, use Poll:isReady() or Future:await() to avoid this error.")
	else
		if self._resultType == "Ok" then
			return Result.ok(table.unpack(self._results)) :: any
		else
			return Result.err(table.unpack(self._results)) :: any
		end
	end
end

--- @within Poll
---
--- Creates a Poll with an `Ok` result.
function Poll.ok<U...>(...: U...): Poll<never, U...>
	local poll = setmetatable({
		_ready = true,
		_resultType = "Ok",
		_results = table.pack(...),
	}, Poll) :: any

	return poll
end

--- @within Poll
---
--- Creates a Poll with an `Err` result.
function Poll.err<E>(errValue: E): Poll<E, never>
	local poll = setmetatable({
		_ready = true,
		_resultType = "Err",
		_results = { errValue },
	}, Poll) :: any

	return poll
end

--- @within Poll
---
--- Creates a Poll which status is not ready.
function Poll.notReady(): Poll<never, never>
	local poll = setmetatable({
		_ready = false,
		_resultType = nil,
		_results = {},
	}, Poll) :: any

	return poll
end

type Library = {
	ok: <U...>(U...) -> Poll<never, U...>,
	err: <E>(errValue: E) -> Poll<E, never>,
	notReady: () -> Poll<never, never>,
}

return (Poll :: any) :: Library


local utils = require("./utils")
local prettyPrint = utils.prettyPrint

type ResultType = "Ok" | "Err"

type ResultImpl<E, U...> = {
	isOk: (self: Result<E, U...>) -> boolean,
	isErr: (self: Result<E, U...>) -> boolean,
	unwrapOk: (self: Result<E, U...>) -> U...,
	unwrapErr: (self: Result<E, U...>) -> E,
}

--- @class Result
---
--- A Result type for use with Futures, with API designed
--- to support strict typing.
local Result = {}
Result.__index = Result

function Result:__tostring()
	local results = prettyPrint(table.unpack(self._results))

	if self:isOk() then
		return `Result::Ok({results})`
	elseif self:isErr() then
		return `Result::Err({results})`
	end

	return "Result::Unknown"
end

export type Result<E, U...> = {
	_type: ResultType,
	_results: { any },
} & ResultImpl<E, U...>

--- @within Result
---
--- Checks whether the Result is of the `Ok` type.
function Result:isOk(): boolean
	return self._type == "Ok"
end

--- @within Result
---
--- Checks whether the Result is of the `Err` type.
function Result:isErr(): boolean
	return self._type == "Err"
end

--- @within Result
---
--- Unwraps the results of an `Ok` Result.
---
--- :::danger
--- Make sure you use [Result:isOk] before using this method.
---
--- ```lua
--- if result:isOk() then
--- 	local result = result:unwrapOk()
--- end
--- ```
--- :::
function Result:unwrapOk<E, U...>(): U...
	return table.unpack(self._results :: any)
end

--- @within Result
---
--- Unwraps the results of an `Err` Result.
---
--- :::danger
--- Make sure you use [Result:isErr] before using this method.
---
--- ```lua
--- if result:isErr() then
--- 	local err = result:unwrapErr()
--- end
--- ```
--- :::
function Result:unwrapErr<E, U...>(): E
	return table.unpack(self._results)
end

--- @within Result
---
--- Creates a new Result of the `Ok` type.
function Result.ok<U...>(...: U...): Result<never, U...>
	local result = setmetatable({
		_type = "Ok",
		_results = table.pack(...),
	}, Result) :: any

	return result
end

--- @within Result
---
--- Creates a new Result of the `Err` type.
function Result.err<E>(errValue: E): Result<E, never>
	local result = setmetatable({
		_type = "Err",
		_results = { errValue },
	}, Result) :: any

	return result
end

type Library = {
	ok: <U...>(U...) -> Result<never, U...>,
	err: <E>(E) -> Result<E, never>,
}

return (Result :: any) :: Library


local JestGlobals = require("@DevPackages/JestGlobals")

local describe = JestGlobals.describe
local test = JestGlobals.test
local expect = JestGlobals.expect
local jest = JestGlobals.jest

local Futures = require("../init")
local Future = Futures.Future

describe("after", function()
	test("success", function()
		local myFuture = Future.new(function()
			return "a", "b", "c"
		end)

		local _mockSuccess, onSuccess = nil, function()
			return 1, 2, 3
		end

		local mockFailure, onFailure = jest.fn(function()
			error("This should never be called")
		end)

		local nextFuture = myFuture:after(function(result)
			if result:isOk() then
				return Future.new(onSuccess)
			else
				return Future.new(onFailure :: any)
			end
		end)

		local result = nextFuture:await()
		expect(result:isOk()).toBe(true)

		-- FUTURE: use jest.spyOn
		-- expect(mockSuccess).toHaveBeenCalled()
		expect(mockFailure).never.toHaveBeenCalled()

		local values = { result:unwrapOk() }
		expect(values).toEqual({ 1, 2, 3 })
	end)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("andThen", function()
	local myFuture = Future.new(function()
		return "a", "b", "c"
	end)

	local nextFuture = myFuture:andThen(function()
		return Future.new(function()
			return 1, 2, 3
		end)
	end)

	local result = nextFuture:await()
	expect(result:isOk()).toBe(true)

	local values = { result:unwrapOk() }

	expect(values).toEqual({ 1, 2, 3 })
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local describe = JestGlobals.describe
local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

describe("await", function()
	test("immediate", function()
		local myFuture = Future.new(function()
			return "a", "b", "c"
		end)

		local result = myFuture:await()
		expect(result:isOk()).toBe(true)

		local values = { result:unwrapOk() }

		expect(values).toEqual({ "a", "b", "c" })
	end)

	test("yielding", function()
		local myFuture = Future.new(function()
			wait()
			return "a", "b", "c"
		end)

		local result = myFuture:await()
		expect(result:isOk()).toBe(true)

		local values = { result:unwrapOk() }

		expect(values).toEqual({ "a", "b", "c" })
	end)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("chaining", function()
	expect.assertions(6)

	local myFuture = Future.new(function()
		return 1, 2, 3
	end)
		:andThen(function(a, b, c)
			return Future.new(function()
				expect({ a, b, c }).toEqual({ 1, 2, 3 })
				return tostring(a), tostring(b), tostring(c)
			end)
		end)
		:inspectOk(function(a, b, c)
			expect({ a, b, c }).toEqual({ "1", "2", "3" })
		end)
		:mapErr(function(_err)
			return "I don't expect an error here"
		end)
		:mapOk(function(a, b, c)
			expect({ a, b, c }).toEqual({ "1", "2", "3" })
			return false, true, 22
		end)
		:inspectOk(function(a, b, c)
			expect({ a, b, c }).toEqual({ false, true, 22 } :: { any })
		end)
		:orElse(function(_err)
			return Future.new(function()
				return false, true, 21
			end) :: any
		end)

	local result = myFuture:await()
	expect(result:isOk()).toBe(true)

	local values = { result:unwrapOk() }
	expect(values).toEqual({ false, true, 22 } :: { any })
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("inspectErr", function()
	local myFuture = Future.new(function()
		error("A", 0)
	end)

	myFuture:inspectErr(function(...)
		expect(...).toEqual("A")
	end)

	local result = myFuture:await()
	expect(result:isErr()).toBe(true)

	expect(result:unwrapErr()).toEqual("A")
	expect.assertions(3)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("inspectOk", function()
	expect.assertions(3)

	local myFuture = Future.new(function()
		return "a", "b", "c"
	end)

	myFuture:inspectOk(function(...)
		expect({ ... }).toEqual({ "a", "b", "c" })
	end)

	local result = myFuture:await()
	expect(result:isOk()).toBe(true)

	local values = { result:unwrapOk() }

	expect(values).toEqual({ "a", "b", "c" })
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local describe = JestGlobals.describe
local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

describe("join", function()
	test("ok", function()
		local myFuture = Future.new(function()
			return "a", "b", "c"
		end)

		local nextFuture = myFuture:join(Future.new(function()
			return "d", "e", "f"
		end))

		local result = nextFuture:await()
		expect(result:isOk()).toBe(true)

		local values = result:unwrapOk()

		expect(values).toEqual({ "a", "b", "c", "d", "e", "f" })
	end)

	test("err", function()
		local myFuture = Future.new(function()
			error("A", 0)
		end)

		local nextFuture = myFuture:join(Future.new(function()
			error("B", 0)
		end))

		local result = nextFuture:await()
		expect(result:isOk()).toBe(true)

		local values = result:unwrapErr()

		expect(values[1]:unwrapErr()).toEqual("A")
		expect(values[2]:unwrapErr()).toEqual("B")
	end)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("joinAll", function()
	local myFuture = Future.new(function()
		return "a", "b", "c"
	end)

	local nextFuture = myFuture:joinAll(
		Future.new(function()
			return "d", "e", "f"
		end),
		Future.new(function()
			return "g", "h", "i"
		end),
		Future.new(function()
			return "j", "k", "l"
		end)
	)

	local result = nextFuture:await()
	expect(result:isOk()).toBe(true)

	local values = { result:unwrapOk() }

    -- stylua: ignore
	expect(values).toEqual({
        "a", "b", "c",
        "d", "e", "f",
        "g", "h", "i",
        "j", "k", "l",
    })
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local describe = JestGlobals.describe
local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

describe("mapErr", function()
	test("to error", function()
		local myFuture = Future.new(function()
			error("A", 0)
		end)

		myFuture:mapErr(function(err)
			expect(err).toEqual("A")
			error("B", 0)
		end)

		local result = myFuture:await()
		expect(result:isErr()).toBe(true)

		local err = result:unwrapErr()
		expect(err).toEqual("B")
		expect.assertions(3)
	end)

	test("to string", function()
		local myFuture = Future.new(function()
			error("A", 0)
			return "A"
		end)

		myFuture:mapErr(function(err)
			expect(err).toEqual("A")
			return "B"
		end)

		local result = myFuture:await()
		expect(result:isErr()).toBe(true)

		local err = result:unwrapErr()
		expect(err).toEqual("B")
		expect.assertions(3)
	end)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("mapOk", function()
	local myFuture = Future.new(function()
		return "a", "b", "c"
	end)

	myFuture:mapOk(function(a, b, c)
		return a == "a" and 1, b == "b" and 2, c == "c" and 3
	end)

	local result = myFuture:await()
	expect(result:isOk()).toBe(true)

	local values = { result:unwrapOk() }

	expect(values).toEqual({ 1, 2, 3 })
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("orElse", function()
	local myFuture = Future.new(function()
		error("A", 0)
	end)

	myFuture = myFuture:orElse(function(err)
		expect(err).toEqual("A")
		return Future.new(function()
			return 1, 2, 3
		end)
	end)

	local result = myFuture:await()
	expect(result:isOk()).toBe(true)

	local results = { result:unwrapOk() }
	expect(results).toEqual({ 1, 2, 3 })

	expect.assertions(3)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect
local describe = JestGlobals.describe

local Futures = require("../init")
local Future = Futures.Future

describe("ordering", function()
	test("chaining", function()
		expect.assertions(7)

		local callOrder = {}

		local myFuture = Future.new(function()
			table.insert(callOrder, "A")
			return 1, 2, 3
		end)
			:andThen(function(a, b, c)
				return Future.new(function()
					table.insert(callOrder, "B")
					expect({ a, b, c }).toEqual({ 1, 2, 3 })
					return tostring(a), tostring(b), tostring(c)
				end)
			end)
			:inspectOk(function(a, b, c)
				table.insert(callOrder, "C")
				expect({ a, b, c }).toEqual({ "1", "2", "3" })
			end)
			:mapErr(function(_err)
				table.insert(callOrder, "Should not be called")
				return "I don't expect an error here"
			end)
			:mapOk(function(a, b, c)
				table.insert(callOrder, "D")
				expect({ a, b, c }).toEqual({ "1", "2", "3" })
				return false, true, 22
			end)
			:inspectOk(function(a, b, c)
				table.insert(callOrder, "E")
				expect({ a, b, c }).toEqual({ false, true, 22 } :: { any })
			end)
			:orElse(function(_err)
				table.insert(callOrder, "Should not be called")
				return Future.new(function()
					return false, true, 21
				end) :: any
			end)

		local result = myFuture:await()
		expect(result:isOk()).toBe(true)

		local values = { result:unwrapOk() }
		expect(values).toEqual({ false, true, 22 } :: { any })

		expect(callOrder).toEqual({ "A", "B", "C", "D", "E" })
	end)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local describe = JestGlobals.describe
local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

describe("poll", function()
	test("immediate", function()
		local myFuture = Future.new(function()
			return "a", "b", "c"
		end)

		local poll = myFuture:poll()
		expect(poll:isReady()).toBe(true)

		local result = poll:unwrap()
		expect(result:isOk()).toBe(true)

		local values = { result:unwrapOk() }

		expect(values).toEqual({ "a", "b", "c" })
	end)

	test("yielding", function()
		local wait = wait
		local myFuture = Future.new(function()
			wait(0.1)
			return "a", "b", "c"
		end)

		local poll = myFuture:poll()
		expect(poll:isPending()).toBe(true)

		wait(0.1)

		poll = myFuture:poll()
		expect(poll:isReady()).toBe(true)

		local result = poll:unwrap()
		expect(result:isOk()).toBe(true)

		local values = { result:unwrapOk() }

		expect(values).toEqual({ "a", "b", "c" })
	end)
end)


local JestGlobals = require("@DevPackages/JestGlobals")

local test = JestGlobals.test
local expect = JestGlobals.expect

local Futures = require("../init")
local Future = Futures.Future

test("unwrapOrElse", function()
	local myFuture = Future.new(function()
		error("A", 0)
	end)

	myFuture = myFuture:unwrapOrElse(function(err)
		expect(err).toEqual("A")
		return 1, 2, 3
	end)

	local result = myFuture:await()
	expect(result:isOk()).toBe(true)

	local results = { result:unwrapOk() }
	expect(results).toEqual({ 1, 2, 3 })

	expect.assertions(3)
end)


local Poll = require("@self/Poll")
export type Poll<E, U...> = Poll.Poll<E, U...>

local Result = require("@self/Result")
export type Result<E, U...> = Result.Result<E, U...>

local Future = require("@self/Future") :: any

-- FUTURE: FutureLike<E, U...> = Future<E, U...> for backwards compatibility
-- This type should be used to typecheck function parameters,\
-- using Futures.Future will not work as expected.
export type FutureLike<E, U...> = {
	await: (self: FutureLike<E, U...>) -> Result<E, U...>,
	poll: (self: FutureLike<E, U...>) -> Poll<E, U...>,
	[any]: any,
}

-- FUTURE: Replace when recursive type restrictions are lifted
-- This type should only be used when returning a Future from a function,\
-- Using Futures.FutureLike will not provide intellisense.
export type Future<E, U...> = FutureFirst<E, U...>

type FutureExhausted = {
	_onResolve: { (...any) -> () },
	_onErr: { (any) -> () },
	_mapOn: {
		ok: { <T...>(...any) -> T... },
		err: { <T>(any) -> T },
	},
	_arguments: { any },
	_callback: (...any) -> ...any,
	_threadStatus: { success: boolean, results: { any } },
	_thread: thread?,

	after: <T...>(self: FutureLike<any, ...any>, fn: (Result<any, ...any>) -> FutureLike<any, T...>) -> FutureExhausted,
	andThen: <T...>(self: FutureLike<any, ...any>, fn: (...any) -> FutureLike<any, T...>) -> FutureExhausted,
	await: (self: FutureLike<any, ...any>) -> Result<any, ...any>,
	inspectErr: (self: FutureLike<any, ...any>, fn: (any) -> ()) -> FutureExhausted,
	inspectOk: (self: FutureLike<any, ...any>, fn: (...any) -> ()) -> FutureExhausted,
	join: <T...>(self: FutureLike<any, ...any>, otherFuture: FutureLike<any, T...>) -> FutureExhausted,
	joinAll: (...FutureLike<any, ...any>) -> FutureExhausted,
	mapErr: <T>(self: FutureLike<any, ...any>, fn: (any) -> T) -> FutureExhausted,
	mapOk: <T...>(self: FutureLike<any, ...any>, fn: (...any) -> T...) -> FutureExhausted,
	orElse: (self: FutureLike<any, ...any>, fn: (any) -> FutureExhausted) -> FutureExhausted,
	unwrapOrElse: (self: FutureLike<any, ...any>, fn: (err: any) -> ...any) -> FutureExhausted,
	poll: (self: FutureLike<any, ...any>) -> Poll<any, ...any>,
}

type FutureLast<E, U...> = {
	_onResolve: { (U...) -> () },
	_onErr: { (E) -> () },
	_mapOn: {
		ok: { <T...>(U...) -> T... },
		err: { <T>(E) -> T },
	},
	_arguments: { any },
	_callback: (...any) -> U...,
	_threadStatus: { success: boolean, results: { any } },
	_thread: thread?,

	after: <T...>(self: FutureLike<E, U...>, fn: (Result<E, U...>) -> FutureLike<E, T...>) -> FutureExhausted,
	andThen: <T...>(self: FutureLike<E, U...>, fn: (U...) -> FutureLike<E, T...>) -> FutureExhausted,
	await: (self: FutureLike<E, U...>) -> Result<E, U...>,
	inspectErr: (self: FutureLike<E, U...>, fn: (E) -> ()) -> FutureLast<E, U...>,
	inspectOk: (self: FutureLike<E, U...>, fn: (U...) -> ()) -> FutureLast<E, U...>,
	join: <T...>(self: FutureLike<E, U...>, otherFuture: FutureLike<E, T...>) -> FutureExhausted,
	joinAll: (...FutureLike<any, ...any>) -> FutureExhausted,
	mapErr: <T>(self: FutureLike<E, U...>, fn: (E) -> T) -> FutureExhausted,
	mapOk: <T...>(self: FutureLike<E, U...>, fn: (U...) -> T...) -> FutureExhausted,
	orElse: (self: FutureLike<E, U...>, fn: (E) -> FutureLast<E, U...>) -> FutureLast<E, U...>,
	unwrapOrElse: (self: FutureLike<E, U...>, fn: (err: E) -> U...) -> FutureLast<E, U...>,
	poll: (self: FutureLike<E, U...>) -> Poll<E, U...>,
}

type FutureNext<E, U...> = {
	_onResolve: { (U...) -> () },
	_onErr: { (E) -> () },
	_mapOn: {
		ok: { <T...>(U...) -> T... },
		err: { <T>(E) -> T },
	},
	_arguments: { any },
	_callback: (...any) -> U...,
	_threadStatus: { success: boolean, results: { any } },
	_thread: thread?,

	after: <T...>(self: FutureLike<E, U...>, fn: (Result<E, U...>) -> FutureLike<E, T...>) -> FutureLast<E, T...>,
	andThen: <T...>(self: FutureLike<E, U...>, fn: (U...) -> FutureLike<E, T...>) -> FutureLast<E, T...>,
	await: (self: FutureLike<E, U...>) -> Result<E, U...>,
	inspectErr: (self: FutureLike<E, U...>, fn: (E) -> ()) -> FutureNext<E, U...>,
	inspectOk: (self: FutureLike<E, U...>, fn: (U...) -> ()) -> FutureNext<E, U...>,
	join: <T...>(self: FutureLike<E, U...>, otherFuture: FutureLike<E, T...>) -> FutureExhausted,
	joinAll: (...FutureLike<any, ...any>) -> FutureExhausted,
	mapErr: <T>(self: FutureLike<E, U...>, fn: (E) -> T) -> FutureLast<T, U...>,
	mapOk: <T...>(self: FutureLike<E, U...>, fn: (U...) -> T...) -> FutureLast<E, T...>,
	orElse: (self: FutureLike<E, U...>, fn: (E) -> FutureNext<E, U...>) -> FutureNext<E, U...>,
	unwrapOrElse: (self: FutureLike<E, U...>, fn: (err: E) -> U...) -> FutureNext<E, U...>,
	poll: (self: FutureLike<E, U...>) -> Poll<E, U...>,
}

type FutureFirst<E, U...> = {
	_onResolve: { (U...) -> () },
	_onErr: { (E) -> () },
	_mapOn: {
		ok: { <T...>(U...) -> T... },
		err: { <T>(E) -> T },
	},
	_arguments: { any },
	_callback: (...any) -> U...,
	_threadStatus: { success: boolean, results: { any } },
	_thread: thread?,

	after: <T...>(self: FutureLike<E, U...>, fn: (Result<E, U...>) -> FutureLike<E, T...>) -> FutureNext<E, T...>,
	andThen: <T...>(self: FutureLike<E, U...>, fn: (U...) -> FutureLike<E, T...>) -> FutureNext<E, T...>,
	await: (self: FutureLike<E, U...>) -> Result<E, U...>,
	inspectErr: (self: FutureLike<E, U...>, fn: (E) -> ()) -> FutureFirst<E, U...>,
	inspectOk: (self: FutureLike<E, U...>, fn: (U...) -> ()) -> FutureFirst<E, U...>,
	-- FUTURE: Return Future<E, (U..., T...)>
	join: <T...>(self: FutureLike<E, U...>, otherFuture: FutureLike<E, T...>) -> FutureExhausted,
	-- NOTE: This might be a little tricky to type, we can supply join3, join4, join5... methods
	joinAll: (...FutureLike<any, ...any>) -> FutureExhausted,
	mapErr: <T>(self: FutureLike<E, U...>, fn: (E) -> T) -> FutureNext<T, U...>,
	mapOk: <T...>(self: FutureLike<E, U...>, fn: (U...) -> T...) -> FutureNext<E, T...>,
	orElse: (self: FutureLike<E, U...>, fn: (E) -> FutureFirst<E, U...>) -> FutureFirst<E, U...>,
	-- FUTURE: Return Future<never, U...>
	unwrapOrElse: (self: FutureLike<E, U...>, fn: (err: E) -> U...) -> FutureFirst<E, U...>,
	poll: (self: FutureLike<E, U...>) -> Poll<E, U...>,
}

type Library = {
	Poll: typeof(Poll),
	Result: typeof(Result),

	-- FUTURE: Replace when recursive type restrictions are lifted
	Future: {
		new: <E, U..., T...>(callback: (T...) -> U..., T...) -> FutureFirst<E, U...>,
		[any]: any,
	},
}

return {
	Poll = Poll,
	Result = Result,

	Future = Future,
} :: Library


return {
	testMatch = {
		"**/__tests__/*.(spec|test)",
	},
	testPathIgnorePatterns = {
		"Packages",
		"DevPackages",
	},
}


-- https://stackoverflow.com/questions/7526223/how-do-i-know-if-a-table-is-an-array/52697380#52697380
local function isArray(t)
	if type(t) ~= "table" then
		return false
	end

	if #t > 0 then
		return true
	end

	for _, _ in pairs(t) do
		return false
	end

	return true
end

local function printArray(arr, indentation: number): string
	local output = ""

	local indent = ""
	for _ = 1, indentation do
		indent ..= "\t"
	end

	local outerIndent = " "
	if #arr ~= 0 and string.len(indent) >= 1 then
		outerIndent = string.sub(indent, 1, string.len(indent) - 1)
	end

	for i, v in arr do
		local vOutput = if type(v) == "table" then prettyPrintTable(v, indentation + 1) else prettyPrint(v)

		output ..= `\n{indent}{vOutput},`

		if i == #arr then
			output ..= `\n`
		end
	end

	return `\{ {output}{outerIndent}\}`
end

local function printDictionary(dict, indentation: number): string
	local output = ""

	local indent = ""
	for _ = 1, indentation do
		indent ..= "\t"
	end

	local outerIndent = ""
	if string.len(indent) >= 1 then
		outerIndent = string.sub(indent, 1, string.len(indent) - 1)
	end

	for k, v in dict do
		local kOutput = if type(k) == "table" then prettyPrintTable(k, indentation + 1) else prettyPrint(k)
		local vOutput = if type(v) == "table" then prettyPrintTable(v, indentation + 1) else prettyPrint(v)

		output ..= `\n{indent}[{kOutput}] = {vOutput},`
	end

	return `\{ {output} \n{outerIndent}\}`
end

function prettyPrintTable(t: any, indentation: number): string
	if isArray(t) then
		return printArray(t, indentation)
	else
		return printDictionary(t, indentation)
	end
end

local function prettyPrintString(str)
	local singleFound = string.find(str, "'")
	local doubleFound = string.find(str, '"')

	if not doubleFound then
		return `"{str}"`
	elseif not singleFound then
		return `'{str}'`
	else
		str = string.gsub(str, '"', '\\"')
		str = string.gsub(str, "'", "\\'")
		return `"{str}"`
	end
end

function prettyPrint(...): string
	local output = {}

	for i = 1, select("#", ...) do
		local v = select(i, ...)

		if type(v) == "table" then
			output[i] = prettyPrintTable(v, 1)
			continue
		elseif type(v) == "string" then
			output[i] = prettyPrintString(v)
			continue
		end

		output[i] = tostring(v)
	end

	return table.concat(output, ", ")
end

return {
	prettyPrint = prettyPrint,
}


return {
	TaggedProperties = {
		--[["RunContext",]] "Enabled", "Name", "ClassName"
	},
	EditorsEnabled = true,
	ExplorerFrameNameFormat = "f_%s",
	ExplorerFrameScriptClassNameFormat = "s_%s",
	UnsavedTokenTag = "DontSaveToken",
	DefaultTokenTag = "Default",
	InsertScopeFormat = "Insert:%s"
}


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local main = script.Parent.Parent

local shared = main.Shared

local PluginUI = require(shared.PluginUI)
local RawInst = require(shared.Small.RawInst)
local UITemplates = require(shared.UITemplates)
local ItemGroup = require(shared.Vendor.ItemGroup)
local cleanup = ItemGroup.cleanup
local Constants = require(main.Constants)
local gt = require(shared.Vendor.greentea)

local Types = require(main.Types)

local rawindex = RawInst.rawindex

local init = false

local ContextMenuEditor = {} :: Types.ContextMenuEditor

local function createCtxMenuInterface(actions: any)
	local self: Types.ContextMenuInterface; self = {
		getMenuContents = function() return actions end,
		getSubMenu = function(name)
			local subMenus = {}
			for _, v in actions do
				if type(v) ~= "table" then continue end
				if v.Id ~= name then continue end
				return createCtxMenuInterface(v[1])
			end
			error(`SubMenu '{name}' not found`)
		end,
		relocate = function(name, newIndex)
			local idx = table.find(actions, name)
			if idx then
				table.remove(actions, idx)
			end
			table.insert(actions, newIndex, name)
		end,
		flipOrder = function()
			local reversed = {}
			for i = #actions, 1, -1 do
				table.insert(reversed, actions[i])
			end
			actions = reversed
		end,

		remove = function(idx)
			table.remove(actions, idx)
		end,
		insert = function(idx, v)
			table.insert(actions, idx, v)
		end,

		findAction = function(name)
			return assert(table.find(actions, name), `Action {name} not found`)
		end,
		findSubMenu = function(name)
			for i, v in actions do
				if type(v) ~= "table" then continue end
				if v.Id ~= name then continue end
				return i
			end
			error(`SubMenu '{name}' not found`)
		end,

		inline = function(idx, arr)
			for _, v in arr do
				table.insert(actions, idx, v)
				idx += 1
			end
		end,

		addSection = function(idx, sectionName, content)
			self.insert(idx, {SectionName = sectionName})
			self.inline(idx + 1, content)
		end,
		getSection = function(sectionName)
			local x: number?, y: number?
			local sectionFound = false

			for i, v in actions do
				if type(v) ~= "table" then continue end
				if not v.SectionName then continue end
				if sectionFound then
					y = i - 1
					break
				end
				local name = v.SectionName
				if name == sectionName then
					x = i
					sectionFound = true
					continue
				end
			end

			assert(x, `Section '{sectionName}' doesn't exist.`)
			if not y then
				y = #actions
			end

			return x::any, y::any
		end,
	}
	return self
end

local contextMenuConfigType: Types.ContextMenuConfigs = gt.struct {
	Menus = gt.array(gt.struct {
		Id = gt.string(),
		Priority = gt.number(),
		Condition = gt.anyfn(),
		Contents = gt.union(gt.anyfn(), gt.array(gt.any())),
	}),
	Actions = gt.dictionary(gt.string(), gt.struct {
		Id = gt.string(),
		Callback = gt.anyfn(),
		Text = gt.string(),
		Desc = gt.opt(gt.string()),
		Icon = gt.opt(gt.union(gt.anyfn(),gt.string()))
	})
}

local returnValTy: Types.ContextMenuConfigModuleReturnValue = gt.union(
	contextMenuConfigType,
	gt.anyfn()
)

local currentlyUsedModule: Instance?
function ContextMenuEditor.init(Manager)
	init = true
	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local connections = state.Connections
	local instances = connections:extend(cleanup.destroy)

	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "ContextMenuEditor", "Context Menu Editor")
	ContextMenuEditor.Widget = widget
	Manager.Style:Link(widget)

	local list = PluginUI.createItemListFromExisting(mainframe, mainframe.Items.Content, mainframe.Items)

	local moduleName = plugin:GetSetting("ContextMenuConfigsModuleName") or ""
	local nameBox = PluginUI.createAttributeBox(list, "Module Name", moduleName, function(v)
		plugin:SetSetting("ContextMenuConfigsModuleName", v)
	end)
	local delayBox = PluginUI.createAttributeBox(list, "Source Changed Refresh Delay", plugin:GetSetting("ContextMenuConfigsRefreshDelay") or "0.5", function(v)
		assert(tonumber(v), `'{v}' could not be converted into number`)
		plugin:SetSetting("ContextMenuConfigsRefreshDelay", v)
	end)
	PluginUI.createButton(list, "Insert Config Template", function()
		local name = nameBox:Get()
		local clone = Instance.new("ModuleScript")
		clone.Source = [[
--!strict
local Types = require("@game/ServerStorage/tag.jexplorer_instance_storage/Types")

return function(plugin: Plugin, manager: Types.Manager, utils: Types.ContextMenuUtils): Types.ContextMenuConfigs
	-- Modify this to your desire!
	return {
		Actions = {},
		Menus = {}
	}
end]]
		clone.Name = name
		clone.Parent = workspace

		task.delay(0.1, function()
			game.Selection:Set({clone})
		end)
	end)

	local function deepCopy(tbl)
		local r = {}

		for i, v in tbl do
			if type(v) == "table" then
				r[i] = deepCopy(v)
				continue
			end
			r[i] = v
		end

		return r
	end

	local defaultInstanceCtxMenus = deepCopy(state.InstanceContextMenus)
	local defaultActionInfos = deepCopy(Manager.ActionInfos)
	local defaultActions =	deepCopy(Manager.BindableActions)

	local refreshDebounceThread: thread? = nil
	local sourceChangedConn: RBXScriptConnection?
	local stored: {Instance} = {}

	local function getCtxMenuInterface(defaultActionsName: string?)
		local tbl
		if defaultActionsName and state.InstanceContextMenus[defaultActionsName] then
			tbl = table.clone(state.InstanceContextMenus[defaultActionsName].Menu.Contents)
		else
			tbl = {}
		end
		return createCtxMenuInterface(tbl)
	end

	local utils: Types.ContextMenuUtils = table.freeze {
		createInterface = function(defaultActionsName: string?)
			local tbl
			if defaultActionsName and state.InstanceContextMenus[defaultActionsName] then
				tbl = table.clone(state.InstanceContextMenus[defaultActionsName].Menu.Contents)
			else
				tbl = {}
			end
			return createCtxMenuInterface(tbl)
		end,
		getMenuIds = function()
			local ids = {}
			for name in state.InstanceContextMenus do
				table.insert(ids, name)
			end
			return ids
		end,
		createModifiedMenu = function(menuId, newContents)
			local menuInfo = assert(state.InstanceContextMenus[menuId], `{menuId} is not an existing menu, cant create modified one.`)
			local contents = newContents(createCtxMenuInterface(menuInfo.Menu.Contents))

			return {
				Id = menuId,
				Priority = menuInfo.Priority,
				Condition = function(manager, target)
					return menuInfo.Condition(manager, target, assert(Manager.instanceOf(target)))
				end,
				Contents = contents
			}
		end,
		createSubMenu = function(id, title, icon, content)
			return {
				Id = id,
				Title = title,
				Icon = icon,
				content
			}
		end,
		createSection = function(name)
			return {SectionName = name}
		end,
	}

	local refreshThread
	local currentRefreshThread: thread?
	local function refresh(module: Instance)
		if not module:IsA("ModuleScript") then return end
		state.InstanceContextMenus = deepCopy(defaultInstanceCtxMenus)
		Manager.ActionInfos = deepCopy(defaultActionInfos)
		Manager.BindableActions = deepCopy(defaultActions)

		for _, v in stored do
			v:Destroy()
		end
		table.clear(stored)

		if currentlyUsedModule then
			currentlyUsedModule:Destroy()
		end
		if sourceChangedConn then
			sourceChangedConn:Disconnect()
		end
		local clone = module:Clone()
		currentlyUsedModule = clone
		clone.Parent = plugin
		sourceChangedConn = module:GetPropertyChangedSignal("Source"):Connect(function()
			if refreshDebounceThread then
				task.cancel(refreshDebounceThread)
			end
			refreshDebounceThread = task.delay(tonumber(delayBox:Get()), function()
				refreshThread(module)
				refreshDebounceThread = nil
			end)
		end)

		local returnval: Types.ContextMenuConfigModuleReturnValue = (require::any)(clone)
		gt.build(returnValTy)
			:assert(returnval)

		local configs: Types.ContextMenuConfigs
		if type(returnval) == "function" then
			configs = returnval(plugin, Manager, utils, table.clone(state.DefaultInstanceActionsArray))
		elseif type(returnval) == "table" then
			configs = returnval
		else
			error("ContextMenu module must return (Plugin, Manager) -> ContextMenuConfigs or ContextMenuConfigs.")
		end
		gt.build(contextMenuConfigType)
			:assert(configs)

		for name, info in configs.Actions do
			local id = info.Id or `JexplorerCustomAction({tostring(os.clock())})`

			local action = Manager.createPluginAction(
				id, info.Text, info.Desc or "", if info.Icon then info.Icon() else nil
			)
			table.insert(stored, action)

			local function triggered()
				local target = Manager.takeContextTarget()
				info.Callback(Manager, target)
			end

			action.Triggered:Connect(triggered)

			Manager.BindableActions[name] = action
			Manager.ActionInfos[name] = {
				Id = id,
				Text = info.Text,
				Desc = info.Desc or "",
				Icon = info.Icon,
				Triggered = triggered
			}
		end
		for name, info in configs.Menus do
			local contents
			if typeof(info.Contents) == "function" then
				contents = info.Contents(utils)
			else
				contents = info.Contents
			end
			local newMenu = Manager.createContextMenu(PluginUI.getId(info.Id, true), contents)

			table.insert(stored, newMenu.Menu)
			for _, v in newMenu.SubMenus do
				table.insert(stored, v.Menu)
			end
			state.InstanceContextMenus[info.Id] = {
				Menu = newMenu,
				Condition = info.Condition,
				Priority = info.Priority
			}
		end
	end
	local function killRefreshThread()
		if currentRefreshThread then
			if coroutine.status(currentRefreshThread) == "dead" then
				currentRefreshThread = nil
			else
				repeat
					task.cancel(currentRefreshThread)
				until coroutine.status(currentRefreshThread) == "dead"
				currentRefreshThread = nil
			end
		end
	end
	function refreshThread(inst: Instance)
		killRefreshThread()
		currentRefreshThread = task.spawn(refresh, inst)
	end

	local function findConfigs(moduleName: string)
		local result = game:QueryDescendants(`[Name = "{moduleName}"]`)
		if #result > 1 then warn(`More than 1 Instance named '{moduleName}'`) return end
		local inst = result[1]
		if not inst then warn(`ContextMenuConfigs module with name '{moduleName}' not found`) return end
		-- If it errors wont affect this thread
		refreshThread(inst)

	end
	if moduleName ~= "" then
		findConfigs(moduleName)
	end
	nameBox:OnChange(findConfigs)
	game.DescendantAdded:Connect(function(inst)
		if inst ~= currentlyUsedModule and inst.Name == nameBox:Get() then
			refreshThread(inst)
		end
	end)
end

return ContextMenuEditor


--!strict
--!optimize 2
local plugin = script:FindFirstAncestorOfClass("Plugin")
local ReflectionService = game:GetService("ReflectionService")
local HttpService = game:GetService("HttpService")

local main = script.Parent.Parent
local shared = main.Shared

local UITemplates = require(shared.UITemplates)
local PluginUI = require(shared.PluginUI)
local ClassIcon = require(shared.Small.ClassIcon)
local ItemGroup = require(shared.Vendor.ItemGroup)
local cleanup = ItemGroup.cleanup
local Types = require(main.Types)

local items = {}
local init = false

local InstanceVisibilityEditor = {
	InvisibleClasses = {},
	InvisibleInstances = {}
} :: Types.InstanceVisibilityEditor

function InstanceVisibilityEditor.setInvisible(className, invisible)
	if not init then return false end
	local item = items[className]
	if not item then
		warn(`class '{className}' has no designated item.`)
		return false
	end
	item.toggle(invisible)
	return true
end

function InstanceVisibilityEditor.init(Manager)
	init = true
	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local connections = state.Connections
	local instances = connections:extend(cleanup.destroy)

	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "InstanceVisibilityEditor", "Instance Visibility Editor")
	InstanceVisibilityEditor.Widget = widget
	Manager.Style:Link(widget)

	local list = PluginUI.createItemListFromExisting(mainframe, mainframe.Items.Content, mainframe.Items)

	local classVisibilityDropdown = PluginUI.createDropDownItemList({Name = "InvisibleClassesDropdown", Visible = true}, "")
	classVisibilityDropdown:Parent(list)

	local instIcon = ClassIcon.getIcon("Instance")
	assert(instIcon)

	local jsonBox

	classVisibilityDropdown:SetText("Loading, please wait...")
	local thread = task.spawn(function()
		local i = 1
		local classes = ReflectionService:GetClasses({
			Security = SecurityCapabilities.new(unpack(Enum.SecurityCapability:GetEnumItems()))
		})

		local customclasses: {ReflectedClass} = {}
		local function insertclass(f: ReflectedClass)
			table.insert(customclasses, f)
		end

		insertclass({
			Name = "InstanceFileSyncService",
			Permits = {},
			Subclasses = {}
		})

		local buttons: {GuiObject} = {}
		local function onClass(info: ReflectedClass)
			local className = info.Name
			local icon = ClassIcon.getIcon(className) or instIcon
			--if i % 2 == 0 then task.wait(0.1) end

			-- Now invisible is the primary state
			local invisible = InstanceVisibilityEditor.InvisibleClasses[className]
			if invisible == nil then
				invisible = false
			end

			local button

			local function toggle(_invisible: boolean?)
				invisible = if _invisible ~= nil then _invisible else not invisible
				if invisible then
					-- Invisible: dark background
					button.Instance:RemoveTag("JEXP_Background")
					button.Instance.Label:AddTag("JEXP_SelectedColor")
					button.Instance.Label:RemoveTag("JEXP_Text")
					button.Instance:AddTag("JEXP_SelectedColor")
				else
					-- Visible: normal background
					button.Instance:RemoveTag("JEXP_SelectedColor")
					button.Instance.Label:RemoveTag("JEXP_SelectedColor")
					button.Instance.Label:AddTag("JEXP_Text")
					button.Instance:AddTag("JEXP_Background")
				end
				InstanceVisibilityEditor.InvisibleClasses[className] = (invisible or nil) :: any
				if _invisible == nil then
					-- Update JSON box with invisible classes
					jsonBox:Set(HttpService:JSONEncode(InstanceVisibilityEditor.InvisibleClasses))
					Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
				end
			end

			button = PluginUI.createButtonWithIcon(nil, className, icon.Image, icon.ImageRectOffset, icon.ImageRectSize, toggle)
			table.insert(buttons, button.Instance)
			if invisible then
				button.Instance:RemoveTag("JEXP_Background")
				button.Instance.Label:RemoveTag("JEXP_Text")
				button.Instance:AddTag("JEXP_SelectedColor")
				button.Instance.Label:AddTag("JEXP_SelectedColor")
			end

			items[className] = {
				className = className,
				button = button.Instance,
				toggle = toggle
			}
			i += 1
		end

		for _, info in customclasses do
			onClass(info)
		end
		for _, info in classes do
			onClass(info)
			task.wait()
		end
		classVisibilityDropdown:AddBulkItem(buttons)
		classVisibilityDropdown:SetText("Invisible Classes")
	end)
	connections:extend(task.cancel, {thread})

	-- JSON box now stores invisible classes directly
	jsonBox = PluginUI.createAttributeBox(list, "Invisible Classes JSON", HttpService:JSONEncode(InstanceVisibilityEditor.InvisibleClasses), function(newValue)
		if newValue == "" then
			InstanceVisibilityEditor.InvisibleClasses = {}
			Manager.VirtualScroller.update(Manager, false, true)
			return
		end

		local tbl = HttpService:JSONDecode(newValue)
		for className, item in items do
			local invisible = tbl[className]
			if invisible == nil then
				invisible = false
			end

			item.toggle(invisible)
		end
		Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
	end)
end

return InstanceVisibilityEditor


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local main = script.Parent.Parent

local shared = main.Shared

local PluginUI = require(shared.PluginUI)
local RawInst = require(shared.Small.RawInst)
local UITemplates = require(shared.UITemplates)
local ItemGroup = require(shared.Vendor.ItemGroup)
local cleanup = ItemGroup.cleanup

local Types = require(main.Types)

local rawindex = RawInst.rawindex

local init = false

local JextensionEditor = {} :: Types.JextensionEditor

local currentlyUsedModule: Instance?
function JextensionEditor.init(Manager)
	init = true
	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local connections = state.Connections
	local instances = connections:extend(cleanup.destroy)

	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "JextensionEditor", "Jextension Editor")
	JextensionEditor.Widget = widget
	Manager.Style:Link(widget)

	local list = PluginUI.createItemListFromExisting(mainframe, mainframe.Items.Content, mainframe.Items)

	local moduleName = plugin:GetSetting("JextensionModuleName") or ""
	local nameBox = PluginUI.createAttributeBox(list, "Module Name", moduleName, function(v)
		plugin:SetSetting("JextensionModuleName", v)
	end)
	local delayBox = PluginUI.createAttributeBox(list, "Source Changed Refresh Delay", plugin:GetSetting("JextensionRefreshDelay") or "0.5", function(v)
		assert(tonumber(v), `'{v}' could not be converted into number`)
		plugin:SetSetting("JextensionRefreshDelay", v)
	end)

	local refreshDebounceThread: thread? = nil
	local sourceChangedConn: RBXScriptConnection?
	local function refresh(module: Instance)
		if not module:IsA("ModuleScript") then return end

		if currentlyUsedModule then
			currentlyUsedModule:Destroy()
		end
		if sourceChangedConn then
			sourceChangedConn:Disconnect()
		end
		local clone = module:Clone()
		currentlyUsedModule = clone
		clone.Parent = plugin
		sourceChangedConn = module:GetPropertyChangedSignal("Source"):Connect(function()
			if refreshDebounceThread then
				task.cancel(refreshDebounceThread)
			end
			refreshDebounceThread = task.delay(tonumber(delayBox:Get()), function()
				refresh(module)
				refreshDebounceThread = nil
			end)
		end)

		local fn = (require::any)(clone)
		assert(type(fn) == "function", "Jextension must return a function")

		fn(plugin, Manager)
	end
	local function findConfigs(moduleName: string)
		local result = game:QueryDescendants(`[Name = "{moduleName}"]`)
		if #result > 1 then warn(`More than 1 Instance named '{moduleName}'`) return end
		local inst = result[1]
		if not inst then warn(`Jextension module with name '{moduleName}' not found`) return end
		-- If it errors wont affect this thread
		task.spawn(refresh, inst)
	end
	if moduleName ~= "" then
		findConfigs(moduleName)
	end
	nameBox:OnChange(findConfigs)
	game.DescendantAdded:Connect(function(inst)
		local expectedName = nameBox:Get()
		if expectedName == "" then return end
		if inst.Name == nameBox:Get() then
			refresh(inst)
		end
	end)
end

return JextensionEditor


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local RunService = game:GetService("RunService")

local main = script.Parent.Parent

local shared = main.Shared

local UITemplates = require(shared.UITemplates)
local Types = require(main.Types)

local JDF = require(shared.UDF)
local PluginUI = require(shared.PluginUI)
local ClassIcon = require(shared.Small.ClassIcon)
local Constants = require(main.Constants)
local ItemGroup = require(shared.Vendor.ItemGroup)
local cleanup = ItemGroup.cleanup

local INSTANCE_ICON = assert(ClassIcon.getIcon("Instance"))

local getId = PluginUI.getId

local init = false

local OrderEditor = {
	DefaultPriority = 999,
	PriorityVersion = 0,
	ClassPriorities = {}, -- Initialized by JState.load()
} :: Types.OrderEditor

function OrderEditor.init(Manager)
	init = true
	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local connections = state.Connections
	local instances = connections:extend(cleanup.destroy)

	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "OrderEditor", "Order Editor")
	OrderEditor.Widget = widget
	Manager.Style:Link(widget)

	local list = PluginUI.createItemListFromExisting(mainframe, mainframe.Items.Content, mainframe.Items)

	local box; box = PluginUI.createAttributeBox(list, "Default Priority", 999, function(newValue)
		local num = JDF.from(newValue)
		if typeof(num) ~= "number" then
			box:Set(999)
			return
		end

		OrderEditor.DefaultPriority = num
	end)

	local dropdown = PluginUI.createDropDownItemList({
		Name = "InstSortingPriorities",
		Visible = true
	}, "")
	dropdown:Parent(list)

	local currentDrag: {
		frame: Frame & any,
		moveConn: RBXScriptConnection,
		class: string,
		button: Frame
	}?
	local buttons = {}
	local buttonToClass = {}

	local function createClassPriorityBox(className: string, priority: number, dropdown: PluginUI.ItemList?)
		local icon
		local success, _icon = pcall(ClassIcon.getIcon, className)
		if success and _icon then
			icon = _icon
		elseif not success then
			icon = INSTANCE_ICON
		elseif not icon then
			icon = INSTANCE_ICON
		end

		local buttonItem = PluginUI.createButtonWithIcon(
			dropdown, className,
			icon.Image,
			icon.ImageRectOffset,
			icon.ImageRectSize
		)

		local button = buttonItem.Instance

		buttonToClass[button] = className
		button.LayoutOrder = priority
		table.insert(buttons, priority, button)

		local click: TextButton = button.Click
		click.InputBegan:Connect(function(input)
			if currentDrag then return end
			if input.UserInputState ~= Enum.UserInputState.Begin then return end
			if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end

			local clone = button:Clone()
			clone.ZIndex = 1000
			clone.Parent = mainframe

			local con = RunService.RenderStepped:Connect(function()
				local pos = widget:GetRelativeMousePosition()
				clone.Position = UDim2.fromOffset(pos.X, pos.Y)
			end)
			currentDrag = {
				frame = clone,
				moveConn = con,
				class = className,
				button = button
			}
		end)
		click.InputEnded:Connect(function(input)
			if not currentDrag then return end
			if input.UserInputState ~= Enum.UserInputState.End then return end
			if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end

			local drag = currentDrag
			currentDrag.frame:Destroy()
			currentDrag.moveConn:Disconnect()
			currentDrag = nil

			local pos = widget:GetRelativeMousePosition()
			local objects = PluginUI.getGuiObjectsAtPosition(mainframe, pos.X, pos.Y)
			local targetButton
			for _, v in objects do
				if v.Name == "ButtonWithIconItem" and v ~= drag.button then
					targetButton = v :: Frame
					break
				end
			end
			if not targetButton then return end
			local targetClass = buttonToClass[targetButton]
			local targetPriority = table.find(OrderEditor.ClassPriorities, targetClass)
			if not targetPriority then return end

			local dragPriority = table.find(OrderEditor.ClassPriorities, drag.class)
			if not dragPriority then return end

			table.remove(OrderEditor.ClassPriorities, dragPriority)

			local newTargetPriority = table.find(OrderEditor.ClassPriorities, targetClass)
			if not newTargetPriority then return end
			local insertPos
			if dragPriority < targetPriority then
				insertPos = newTargetPriority + 1
			else
				insertPos = newTargetPriority
			end

			table.insert(OrderEditor.ClassPriorities, insertPos, drag.class)

			for i, class in OrderEditor.ClassPriorities do
				for button, buttonClass in buttonToClass do
					if buttonClass == class then
						button.LayoutOrder = i
						break
					end
				end
			end

			OrderEditor.PriorityVersion += 1
			Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
		end)

		PluginUI.createDeleteButton(button, function()
			local idx = table.find(buttons, button)
			if idx then
				table.remove(buttons, idx)
			end

			local currentPriority = table.find(OrderEditor.ClassPriorities, className)
			if currentPriority then
				table.remove(OrderEditor.ClassPriorities, currentPriority)
				for i = currentPriority, #buttons do
					buttons[i].LayoutOrder = i
				end
			end

			button:Destroy()
		end)
		return buttonItem
	end

	local header = dropdown.Backdrop:WaitForChild("DropDownHeader")
	PluginUI.createInsertButton(header, function()
		local nameBox = PluginUI.createAttributeBox(nil, "ClassName", "")
		local widget, cancel, confirm, close = PluginUI.createCreatorWidget("Insert Priority Class", {nameBox})

		cancel:OnTrigger(function()
			close()
		end)

		confirm:OnTrigger(function()
			local name = nameBox:Get()

			table.insert(OrderEditor.ClassPriorities, 1, name)
			for i, class in OrderEditor.ClassPriorities do
				for button, buttonClass in buttonToClass do
					if buttonClass == class then
						button.LayoutOrder = i
						break
					end
				end
			end
			createClassPriorityBox(name, 1, dropdown)
			close()
		end)
	end)

	dropdown:SetText("Loading, please wait...")
	connections:extend(task.cancel, {
		task.spawn(function()
			local items = {}
			for i, v in OrderEditor.ClassPriorities do
				table.insert(items, createClassPriorityBox(v, i, nil).Instance)
				task.wait()
			end
			dropdown:AddBulkItem(items)
			dropdown:SetText("Instance Sorting Priorities (descending)")
		end)
	})
end

return OrderEditor


--!strict
--!optimize 2

local InstanceRecord = require("../../Shared/Small/InstanceRecord")

local main = script.Parent.Parent.Parent
local shared = main.Shared

local PluginUI = require(shared.PluginUI)
local RawInst = require(shared.Small.RawInst)

local rawindex = RawInst.rawindex

local Types = require(main.Types)

local PropertyFrame = {
	PluginUI = PluginUI,
	RawInst = RawInst,
	tonumber = function(a: any)
		return assert(tonumber(a))
	end,
	beginRecording = InstanceRecord.beginRecording,
	finishRecording = InstanceRecord.finishRecording
}

type CallbackInfo = {
	PropertyEditor: Types.PropertyEditor,
	Manager: Types.Manager,
	InstanceCleanup: {Instance},
	VersionControl: Types.VersionControl,
	Plugin: Plugin,
	Instances: {Instance}
}

export type PropertyFrame = {
	createUI: (info: CallbackInfo, list: PluginUI.ItemList, item: Types.PropertyItem, initValue: any) -> (PluginUI.ValueUIItem<any> & any, {[string]: any}),
	applyBehavior: (info: CallbackInfo, item: Types.PropertyItem, initValue: any, data: {[string]: any}) -> (() -> (), (changedValue: any) -> ()),
	typeId: string
}

local typeIdToFrame: {[string]: PropertyFrame} = {}

function PropertyFrame.create(type_id: string, ui_callback: (info: CallbackInfo, list: PluginUI.ItemList, item: Types.PropertyItem, initValue: any) -> (PluginUI.ValueUIItem & any, {[string]: any}), behavior_callback: (info: CallbackInfo, item: Types.PropertyItem, initValue: any, uiData: {[string]: any}) -> (() -> (), (changedValue: any) -> ())): PropertyFrame
	local frame = {
		createUI = ui_callback,
		applyBehavior = behavior_callback,
		typeId = type_id
	}:: PropertyFrame
	typeIdToFrame[type_id] = frame
	return frame
end

function PropertyFrame.fromTypeId(type_id: string): PropertyFrame
	return assert(typeIdToFrame[type_id])
end

function PropertyFrame.isAllValuesSame(instances: {Instance}, propName: string)
	local firstValue = rawindex(instances[1], propName)
	local allSame = true

	for j = 2, #instances do
		if rawindex(instances[j], propName) ~= firstValue then
			allSame = false
			break
		end
	end

	return allSame, firstValue
end

return PropertyFrame


--!strict
--!optimize 2

local StudioService = game:GetService("StudioService")

local PropertyFrame = require("../PropertyFrame")
local xyz = require("../../../Shared/Small/xyz")
local ColorPicker = require("../../../Shared/Vendor/PluginColorPicker")

local PluginUI = PropertyFrame.PluginUI
local RawInst = PropertyFrame.RawInst
local getId = PluginUI.getId

return PropertyFrame.create(
	"Color3",
	function(info, list, item, initValue)
		local ui = PluginUI.createAttributeBox(list, item.name, "")
		local chooseColorButton = PluginUI.createSideButtonImage("", Vector2.zero, Vector2.zero, ui.Instance.ValueBox)
		chooseColorButton:AddTag("JEXP_Border")
		chooseColorButton.BackgroundColor3 = initValue

		return ui, {
			ui = ui,
			chooseColorButton = chooseColorButton
		}
	end,
	function(info, item, initValue, data)
		local ui = data.ui
		local chooseColorButton: ImageButton = data.chooseColorButton

		ui:Set(`{math.round(initValue.R * 255)}, {math.round(initValue.G * 255)}, {math.round(initValue.B * 255)}`)
		ui:SetName(item.name)

		chooseColorButton.BackgroundColor3 = initValue

		local function colorChanged(newColor: Color3)
			ui:Set(`{math.round(newColor.R * 255)}, {math.round(newColor.G * 255)}, {math.round(newColor.B * 255)}`)
			chooseColorButton.BackgroundColor3 = newColor
		end
		local function colorChangedInner(newColor: Color3)
			colorChanged(newColor)
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newColor)
				info.VersionControl.addPendingChange(inst, item.name, newColor)
			end
		end
		local picker
		local activatedConn = chooseColorButton.Activated:Connect(function()
			if picker then
				picker:Close()
				picker:Destroy()
			end

			picker = ColorPicker.new(info.Plugin, getId("PropertyColorPicker", true))
			picker:Show()

			local originalColor
			local allSame, firstValue = PropertyFrame.isAllValuesSame(info.Instances, item.name)
			if allSame then
				originalColor = firstValue
			else
				originalColor = initValue
			end

			picker:Set(originalColor)

			local operation = Enum.FinishRecordingOperation.Commit
			picker:BindToChange(function(newColor)
				local id = PropertyFrame.beginRecording("Try Color3")
				colorChangedInner(newColor)
				PropertyFrame.finishRecording(id, operation)
				operation = Enum.FinishRecordingOperation.Append
			end)
			picker:BindToCancel(function()
				local id = PropertyFrame.beginRecording("Cancel")
				colorChangedInner(originalColor)
				PropertyFrame.finishRecording(id)

				picker:Close()
				picker:Destroy()
			end)
			picker:BindToConfirm(function()
				picker:Close()
				picker:Destroy()
			end)
		end)

		local changedConn = ui:OnChange(function(newValue)
			local r, g, b = xyz(newValue)
			local color = Color3.fromRGB(
				PropertyFrame.tonumber(r),
				PropertyFrame.tonumber(g),
				PropertyFrame.tonumber(b)
			)
			chooseColorButton.BackgroundColor3 = color

			local id = PropertyFrame.beginRecording("Change Color3")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, color)
				info.VersionControl.addPendingChange(inst, item.name, color)
			end
			PropertyFrame.finishRecording(id)
		end)

		return function()
			changedConn:Disconnect()
			activatedConn:Disconnect()
		end, colorChanged
	end
)


--!strict
--!optimize 2

local StudioService = game:GetService("StudioService")

local PropertyFrame = require("../PropertyFrame")
local PluginUI = PropertyFrame.PluginUI
local RawInst = require("../../../Shared/Small/RawInst")

return PropertyFrame.create(
	"ContentId",
	function(info, list, item, initValue)
		local ui = PluginUI.createAttributeBox(list, item.name, initValue)
		local importFromFileButton = PluginUI.createSideButtonImage("rbxassetid://13544220123", Vector2.zero, Vector2.zero, ui.Instance.ValueBox)
		importFromFileButton:AddTag("JEXP_MonochromeImage")
		importFromFileButton.BackgroundTransparency = 1

		return ui, {
			ui = ui,
			importFromFileButton = importFromFileButton
		}
	end,
	function(info, item, initValue, data)
		local ui = data.ui
		local importFromFileButton: ImageButton = data.importFromFileButton

		local function onChanged(newValue: string)
			ui:Set(newValue)
		end
		local function onChangedInner(newValue: string)
			onChanged(newValue)
			local id = PropertyFrame.beginRecording("ContentId")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		local activatedConn = importFromFileButton.Activated:Connect(function()
			local file = StudioService:PromptImportFile({"png", "jpg", "jpeg"}) :: File?
			if not file then return end

			local id = file:GetTemporaryId()
			onChangedInner(id)
		end)
		local onChangedConn = ui:OnChange(function(txt)
			if txt:match("(%d+)") == txt then
				txt = `rbxassetid://{txt}`
				ui:Set(txt)
			end
			onChangedInner(txt)
		end)

		return function()
			activatedConn:Disconnect()
			onChangedConn:Disconnect()
		end, onChanged
	end
)