--Implementation of a command.
--!strict

local PhysicsService = game:GetService("PhysicsService")

local IncludedCommandUtil = require(script.Parent.Parent:WaitForChild("IncludedCommandUtil"))
local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "uncollide",
    Category = "UsefulFunCommands",
    Description = "Makes a set of players unable to collide with each other.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to make uncollidable with each other.",
        },
    },
    ServerLoad = function(Api: Types.NexusAdminApiServer)
        Api.CommandData.PlayerCollisionEvents = {}

        --Determine if the collision group exists.
        local PlayerCollisionGroupExists = false
        for _, GroupData in PhysicsService:GetRegisteredCollisionGroups() do
            if GroupData.name == "NexusAdmin_PlayerCollisionGroup" then
                PlayerCollisionGroupExists = true
                break
            end
        end

        --Create the collision group.
        if not PlayerCollisionGroupExists then
            PhysicsService:RegisterCollisionGroup("NexusAdmin_PlayerCollisionGroup")
            PhysicsService:CollisionGroupSetCollidable("NexusAdmin_PlayerCollisionGroup", "NexusAdmin_PlayerCollisionGroup", false)
        end
    end,
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        local Util = IncludedCommandUtil.ForContext(CommandContext)
        local Api = Util:GetApi()

        --Make the players collidable.
        for _, Player in Players do
            --Disconnect the events.
            if Api.CommandData.PlayerCollisionEvents[Player] then
                for _, Event in Api.CommandData.PlayerCollisionEvents[Player] do
                    Event:Disconnect()
                end
                Api.CommandData.PlayerCollisionEvents[Player] = {}
            end

            --[[
            Makes the current character uncollidable.
            --]]
            local function MakeCharacterUncollidable()
                if Player.Character then
                    --Set the collision group of the existing parts.
                    for _, Part in Player.Character:GetDescendants() do
                        if Part:IsA("BasePart") then
                            Part.CollisionGroup = "NexusAdmin_PlayerCollisionGroup"
                        end
                    end

                    --Connect the new parts being added.
                    table.insert(Api.CommandData.PlayerCollisionEvents[Player], Player.Character.DescendantAdded:Connect(function(Part)
                        if Part:IsA("BasePart") then
                            Part.CollisionGroup = "NexusAdmin_PlayerCollisionGroup"
                        end
                    end))
                end
            end

            --Make the character uncollidable.
            Api.CommandData.PlayerCollisionEvents[Player] = {
                Player.CharacterAdded:Connect(MakeCharacterUncollidable),
            }
            MakeCharacterUncollidable()
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "unff",
    Category = "UsefulFunCommands",
    Description = "Removes all force fields from the given players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to remove force fields from.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        for _, Player in Players do
            local Character = Player.Character
            if Character then
                for _, Ins in Character:GetChildren() do
                    if Ins:IsA("ForceField") then
                        Ins:Destroy()
                    end
                end
            end
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

local FlyState = require(script.Parent.Parent:WaitForChild("Resources"):WaitForChild("FlyState"))

return {
    Keyword = "unfly",
    Category = "UsefulFunCommands",
    Description = "Removes flight from a set of players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to unfly.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        for _, Player in Players do
            FlyState.RemoveFromPlayer(Player)
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "ungod",
    Category = "UsefulFunCommands",
    Description = "Sets the max health of the given players 100.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to ungod.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        for _, Player in Players do
            local Character = Player.Character
            if Character then
                local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
                if Humanoid then
                    Humanoid.MaxHealth = 100
                    Humanoid.Health = 100
                end
            end
        end
    end,
}


--Implementation of a command.
--!strict

local IncludedCommandUtil = require(script.Parent.Parent:WaitForChild("IncludedCommandUtil"))
local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "unjail",
    Category = "UsefulFunCommands",
    Description = "Unjails a set of players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to unjail.",
        },
    },
    ServerLoad = function(Api: Types.NexusAdminApiServer)
        Api.CommandData.PlayerJails = {}
    end,
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        local Util = IncludedCommandUtil.ForContext(CommandContext)
        local Api = Util:GetApi()

        --Destroy the jails.
        local PlayerJails = Api.CommandData.PlayerJails
        for _, Player in Players do
            if PlayerJails[Player] then
                PlayerJails[Player]:Destroy()
            end
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "unlock",
    Category = "UsefulFunCommands",
    Description = "Unlocks the character of the given players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to unlock.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        for _, Player in Players do
            if Player.Character then
                for _,Ins in Player.Character:GetDescendants() do
                    if Ins:IsA("BasePart") then
                        Ins.Locked = false
                    end
                end
            end
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "unname",
    Category = "UsefulFunCommands",
    Description = "Removes the name of a player's character from :name.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to set the name.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        for _, Player in Players do
            local Character = Player.Character
            if Character then
                local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
                if Humanoid then
                    Humanoid.DisplayName = ""
                end
            end
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "unstun",
    Category = "UsefulFunCommands",
    Description = "Unstuns a set of players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to unstun.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        for _, Player in Players do
            local Character = Player.Character
            if Character then
                local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
                if Humanoid then
                    Humanoid.PlatformStand = false
                end
            end
        end
    end,
}


--Implementation of a command.
--!strict

local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = {"walkspeed", "speed"},
    Category = "UsefulFunCommands",
    Description = "Sets the walkspeeds of the given players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to set the walkspeed.",
        },
        {
            Type = "number",
            Name = "Walkspeed",
            Description = "Walkspeed to set.",
        },
    },
    ServerRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player}, WalkSpeed: number)
        for _, Player in Players do
            local Character = Player.Character
            if Character then
                local Humanoid = Character:FindFirstChildOfClass("Humanoid")
                if Humanoid then
                    Humanoid.WalkSpeed = WalkSpeed
                end
            end
        end
    end,
}


--Implementation of a command.
--!strict

local IncludedCommandUtil = require(script.Parent.Parent:WaitForChild("IncludedCommandUtil"))
local Types = require(script.Parent.Parent.Parent:WaitForChild("Types"))

return {
    Keyword = "where",
    Category = "UsefulFunCommands",
    Description = "Displays the positions of the given players.",
    Arguments = {
        {
            Type = "nexusAdminPlayers",
            Name = "Players",
            Description = "Players to get the locations of.",
        },
    },
    ClientRun = function(CommandContext: Types.CmdrCommandContext, Players: {Player})
        local Util = IncludedCommandUtil.ForContext(CommandContext)
        local Api = Util:GetClientApi()

        --Show the window.
        local Window = Api.Window:CreateWindow("Positions")
        local TextList = Window:AddTextList()
        TextList:EnableSearching()

        local function UpdateText()
            local Positions = {}
            for _, Player in Players do
                local Character = Player.Character
                if Character then
                    local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart") :: BasePart
                    if HumanoidRootPart then
                        local Position = HumanoidRootPart.Position
                        table.insert(Positions, string.format(`{Player.Name}: %.3f,%.3f,%.3f`, Position.X, Position.Y, Position.Z))
                    else
                        table.insert(Positions, `{Player.Name} has no HumanoidRootPart.`)
                    end
                else
                    table.insert(Positions, `{Player.Name} has no character.`)
                end
            end
            table.sort(Positions,function(a, b) return string.lower(a) < string.lower(b) end)
            TextList:SetLines(Positions)
        end

        task.spawn(UpdateText)
        Window:EnableRefreshing(UpdateText)
        Window:EnableClosing()
        Window:EnableResizing()
        Window:Show()
    end,
}


--Loads Nexus Admin on the client.
--!strict

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local GuiService = game:GetService("GuiService")
local ContextActionService = game:GetService("ContextActionService")

local NexusAdminClient = require(ReplicatedStorage:WaitForChild("NexusAdminClient")) :: {
    Cmdr: {
        SetActivationKeys: (self: any, {Enum.KeyCode}) -> (),
    },
    Authorization: {
        IsPlayerAuthorized: (self: any, Player: Player, AdminLevel: number) -> (boolean),
    },
    Configuration: {
        ActivationKeys: {Enum.KeyCode},
        DefaultAdminLevel: number,
        GetCommandAdminLevel: (self: any, CommandGroup: string, Command: string) -> (number),
    },
    Registry: {
        LoadServerCommands: (self: any) -> (),
    },
    LoadIncludedCommands: (self: any) -> (),
}
NexusAdminClient.Cmdr:SetActivationKeys(NexusAdminClient.Configuration.ActivationKeys)
NexusAdminClient.Registry:LoadServerCommands()
NexusAdminClient:LoadIncludedCommands()

--Roblox's Keyboard Navigation collides with the original default keybind of \.
--To get around this, navigation is disabled when \ is pressed, the user is an admin, and it is an activation key.
local CmdrWindow = require(ReplicatedStorage:WaitForChild("CmdrClient"):WaitForChild("CmdrInterface"):WaitForChild("Window")) :: {AddLine: (self: any, Message: string, {Color: Color3}) -> ()}
for _, ActivationKey in NexusAdminClient.Configuration.ActivationKeys do
    if ActivationKey ~= Enum.KeyCode.BackSlash then return end
    ContextActionService:BindAction("NexusAdminLegacyKeybindWarning", function()
        --Return if the keyboard navigation is already disabled.
        if not GuiService.AutoSelectGuiEnabled then
            return Enum.ContextActionResult.Pass
        end

        --Return if the player is not authorized to use the window.
        local AdminLevel = NexusAdminClient.Configuration:GetCommandAdminLevel("Administrative", "cmdbar")
        if not NexusAdminClient.Authorization:IsPlayerAuthorized(Players.LocalPlayer, AdminLevel) then
            return Enum.ContextActionResult.Pass
        end

        --Disable keyboard navigation and show the deprecation message.
        GuiService.AutoSelectGuiEnabled = false
        local Message = "Using \\ for opening the Cmdr window is deprecated due to Roblox's Keyboard Navigation.\n"
        .."https://devforum.roblox.com/t/new-keybinds-for-keyboard-navigation/2069353\n\n"
        .."F2 is the new default for toggling the window.\n"
        .."Keyboard navigation is disabled for those who can toggle the command bar (this window).\n"
        .."Please specify ActivationKeys in the loader of Nexus Admin to not contain Enum.KeyCode.BackSlash."
        if AdminLevel <= NexusAdminClient.Configuration.DefaultAdminLevel then
            Message ..= "\nIf you are keeping \\ as an activation key, please set cmdbar command to an admin level that is above non-admins so that they can use keyboard navigation."
        end
        CmdrWindow:AddLine(Message, {
            Color = Color3.fromRGB(255, 200, 0),
        })
        return Enum.ContextActionResult.Pass
    end, false, Enum.KeyCode.BackSlash)
    break
end


--Replicates instances to players.
--!strict

local Players = game:GetService("Players")
local StarterPlayerScripts = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts")

local Replicator = {}
Replicator.__index = Replicator

export type Replicator = typeof(setmetatable({}, Replicator))

--[[
Creates a replicator instance.
--]]
function Replicator.new(): Replicator
    return setmetatable({}, Replicator) :: Replicator
end

--[[
Gives a script to a player.
--]]
function Replicator.GiveScriptToPlayer(self: Replicator, Player: Player, Script: BaseScript): ()
    local PlayerGui = Player:FindFirstChild("PlayerGui")
    if PlayerGui then
        local ScriptParent = Instance.new("ScreenGui")
        ScriptParent.Name = `NexusAdminScript_{Script.Name}`
        ScriptParent.ResetOnSpawn = false
        Script:Clone().Parent = ScriptParent
        ScriptParent.Parent = PlayerGui
    else
        warn(`{Player.Name} didn't have a PlayerGui. The script was not given.`)
    end
end

--[[
Gives a script to all players.
--]]
function Replicator.GiveStarterScript(self: Replicator, Script: BaseScript): ()
    warn(`Replicator::GiveStarterScript is deprecated as of V.2.8.0 in favor of Script.RunContext.`)
    --Add the script to the starter player script.
    Script:Clone().Parent = StarterPlayerScripts

    --Give the player the scripts.
    for _, Player in Players:GetPlayers() do
        self:GiveScriptToPlayer(Player, Script)
    end
end

return Replicator


--Handles users being authorized.
--!strict

local GROUP_LEVEL_CACHE_EXPIRE_SECONDS = 30 * 60

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

local Authorization = require(script.Parent.Parent:WaitForChild("Common"):WaitForChild("Authorization"))
local Configuration = require(script.Parent.Parent:WaitForChild("Common"):WaitForChild("Configuration"))

local ServerAuthorization = {}
ServerAuthorization.__index = ServerAuthorization
setmetatable(ServerAuthorization, Authorization)

export type ServerAuthorization = {
    GroupService: GroupService,
    GameId: number,
    CreatorId: number,
    GroupGameCreator: number?,
    GetAdminLevels: RemoteFunction,
    AdminLevelChangedRemote: RemoteEvent,
    GroupAdminLevelCache: {[number]: {AdminLevel: number, ExpireTime: number}}
} & typeof(setmetatable({}, ServerAuthorization)) & Authorization.Authorization

--[[
Creates a server authorization instance.
--]]
function ServerAuthorization.new(Configuration: Configuration.Configuration, NexusAdminRemotes: Folder): ServerAuthorization
    local self = setmetatable(Authorization.new(Configuration), ServerAuthorization) :: ServerAuthorization
    self.GroupService = GroupService
    self.GameId = game.GameId
    self.CreatorId = game.CreatorId

    --Store the highest AdminLevel of a UserId based on Groups alone.
    self.GroupAdminLevelCache = {}

    --Determine the group owner.
    if game.CreatorType == Enum.CreatorType.Group then
        xpcall(function()
            self.GroupGameCreator = self.GroupService:GetGroupInfoAsync(self.CreatorId).Owner.Id
        end, function(ErrorMessage)
            warn(`Fetching group owner failed because {ErrorMessage}`)
        end)
    end

    --Create the remote objects.
    local AuthorizationEvents = Instance.new("Folder")
    AuthorizationEvents.Name = "AuthorizationEvents"
    AuthorizationEvents.Parent = NexusAdminRemotes

    local GetAdminLevels = Instance.new("RemoteFunction")
    GetAdminLevels.Name = "GetAdminLevels"
    GetAdminLevels.Parent = AuthorizationEvents
    self.GetAdminLevels = GetAdminLevels

    local AdminLevelChanged = Instance.new("RemoteEvent")
    AdminLevelChanged.Name = "AdminLevelChanged"
    AdminLevelChanged.Parent = AuthorizationEvents
    self.AdminLevelChangedRemote = AdminLevelChanged

    --Connect the remote objects.
    function GetAdminLevels.OnServerInvoke()
        return self.AdminLevels
    end

    --Return the object.
    return self
end

--[[
Initializes the existing players and connects new players.
--]]
function ServerAuthorization.InitializePlayers(self: ServerAuthorization): ()
    --Connect new players.
    Players.PlayerAdded:Connect(function(Player: Player): ()
        self:InitializePlayer(Player)
    end)

    --Initialize the existing players.
    for _, Player in Players:GetPlayers() do
        task.spawn(function()
            self:InitializePlayer(Player)
        end)
    end
end

--[[
Initializes a player.
--]]
function ServerAuthorization.InitializePlayer(self: ServerAuthorization, Player: Player): ()
    --Clear the cached entry to re-check group levels.
    self.GroupAdminLevelCache[Player.UserId] = nil

    --Set the admin level based on the default or given admin level.
    local UserId = tostring(Player.UserId)
    local AdminLevel = self:EvaluateAdminLevelAsync(Player.UserId)

    --Send the admin level change.
    if self.AdminLevels[UserId] then
        self.AdminLevels[UserId] = math.max(AdminLevel, self.AdminLevels[UserId])
    else
        self.AdminLevels[UserId] = AdminLevel
    end
    self.AdminLevelChangedRemote:FireAllClients(Player, AdminLevel)
    self.AdminLevelChanged:Fire(Player)
end

--[[
Evaluates the AdminLevel of a UserId.
--]]
function ServerAuthorization.EvaluateAdminLevelAsync(self: ServerAuthorization, UserId: number): number
    --Set the admin level based on the default and user id admin levels.
    local UserIdString = tostring(UserId)
    local AdminLevel = self.AdminLevels[UserIdString] or self.Configuration.DefaultAdminLevel
    if self.Configuration.Admins[UserId] then
        AdminLevel = math.max(AdminLevel, self.Configuration.Admins[UserId])
    end

    --Set the admin level based on group id.
    AdminLevel = math.max(AdminLevel, self:EvaluateGroupAdminLevelAsync(UserId))

    --Set the admin level to highest one if the user is the owner or in Studio.
    if self.GameId == 0 or self.CreatorId == 0 or UserId == self.CreatorId or UserId == self.GroupGameCreator then
        if self.Configuration then
            for Level, _ in self.Configuration.AdminNames do
                AdminLevel = math.max(tonumber(Level) :: number, AdminLevel)
            end
        end
    end
    return AdminLevel
end

--[[
Evaluates the AdminLevel of a UserId based on groups alone and store said value.
--]]
function ServerAuthorization.EvaluateGroupAdminLevelAsync(self: ServerAuthorization, UserId: number): number
    --Return the cached value if one exists and is recent enough.
    local CacheEntry = self.GroupAdminLevelCache[UserId]
    if CacheEntry and os.time() < CacheEntry.ExpireTime then
        return CacheEntry.AdminLevel
    end

    --Make the number unreasonably low just so the first math.max doesn't get fed a nil value.
    local AdminLevel = -math.huge
    xpcall(function()
        for _, UserGroupInfo in self.GroupService:GetGroupsAsync(UserId) do
            local GroupInfo = self.Configuration.GroupAdminLevels[UserGroupInfo.Id]
            if GroupInfo then
                for Rank, NewAdminLevel in GroupInfo do
                    if UserGroupInfo.Rank >= Rank then
                        AdminLevel = math.max(AdminLevel, NewAdminLevel)
                    end
                end
            end
        end
    end, function(ErrorMessage)
        warn(`Getting group info failed because {ErrorMessage}`)
    end)

    --Store and return the admin level.
    self.GroupAdminLevelCache[UserId] = {
        AdminLevel = AdminLevel,
        ExpireTime = os.time() + GROUP_LEVEL_CACHE_EXPIRE_SECONDS,
    }
    return AdminLevel
end

--[[
Sets the admin level for a player.
--]]
function ServerAuthorization.SetAdminLevel(self: ServerAuthorization, Player: Player, AdminLevel: number): ()
    Authorization.SetAdminLevel(self, Player, AdminLevel)
    self.AdminLevelChangedRemote:FireAllClients(Player, self.AdminLevels[tostring(Player.UserId)])
end

return ServerAuthorization


--Filters strings to comply with Roblox's filtering rules.
--!strict

local Filter = require(script.Parent.Parent:WaitForChild("Common"):WaitForChild("Filter"))

local ServerFilter = {}
ServerFilter.__index = ServerFilter
setmetatable(ServerFilter, Filter)

export type ServerFilter = typeof(setmetatable({}, ServerFilter)) & Filter.Filter

--[[
Creates a filter instance.
--]]
function ServerFilter.new(): ServerFilter
    return setmetatable(Filter.new(), ServerFilter) :: ServerFilter
end

--[[
Filters a string for a user.
--]]
function ServerFilter.FilterString(self: ServerFilter, String: string, PlayerFrom: Player, PlayerTo: Player?): string
    if PlayerTo then
        --TODO: Deprecate when an easy solution exists.
        --warn("ServerFilter::FilterString with PlayerTo is deprecated in Nexus Admin 2.9.0 due to TextChatService changes (https://devforum.roblox.com/t/textservice-filtering-returns-empty-message-for-valid-text-input/3650504).")
    end

    --Return the string if it is an empty string.
    if not string.match(String, "([%w%p])") then return String end

    --Filter the string.
    local Worked, Return = pcall(function()
        if PlayerTo and self.Chat:CanUsersChatAsync(PlayerFrom.UserId, PlayerTo.UserId) then
            return self.TextService:FilterStringAsync(String, PlayerFrom.UserId, Enum.TextFilterContext.PrivateChat):GetNonChatStringForUserAsync(PlayerTo.UserId)
        end
        return self.TextService:FilterStringAsync(String, PlayerFrom.UserId, Enum.TextFilterContext.PublicChat):GetNonChatStringForBroadcastAsync()
    end)

    --Return the message.
    if Worked and Return then
        return Return
    else
        warn(`Filter string failed for "{String}" because {Return}`)
        return string.gsub(String, "[^%s]", "#")
    end
end

--[[
Filters a string for a set of users.
Returns a map of the players to their filtered string.
--]]
function ServerFilter.FilterStringForPlayers(self: ServerFilter, String: string, PlayerFrom: Player, PlayersTo: {Player}): {[Player]: string}
    --TODO: Deprecate when an easy solution exists.
    --warn("ServerFilter::FilterStringForPlayers is deprecated in Nexus Admin 2.9.0 due to TextChatService changes (https://devforum.roblox.com/t/textservice-filtering-returns-empty-message-for-valid-text-input/3650504).")

    --Return the string if it is an empty string.
    local FilteredResults = {}
    if not string.match(String,"([%w%p])") then
        for _,Player in PlayersTo do
            FilteredResults[Player] = String
        end
        return FilteredResults
    end

    --Add the filtered strings for the players.
    local Worked, Return = pcall(function()
        local FilterObject = self.TextService:FilterStringAsync(String,PlayerFrom.UserId, Enum.TextFilterContext.PrivateChat)
        local CachedBroadcastMessage
        for _, Player in PlayersTo do
            local Worked, Return = pcall(function()
                if self.Chat:CanUsersChatAsync(PlayerFrom.UserId, Player.UserId) then
                    FilteredResults[Player] = FilterObject:GetNonChatStringForUserAsync(Player.UserId)
                else
                    if not CachedBroadcastMessage then
                        CachedBroadcastMessage = FilterObject:GetNonChatStringForBroadcastAsync()
                    end
                    FilteredResults[Player] = CachedBroadcastMessage
                end
            end)
            if not Worked then
                warn(`Filter string for player {Player} failed for "{String}"  because {Return}`)
            end
        end
    end)
    if not Worked then
        warn(`Filter string failed for "{String}"  because {Return}`)
    end

    --Add the missing filtered strings.
    local BaseFilteredString
    for _, Player in PlayersTo do
        if not FilteredResults[Player] then
            if not BaseFilteredString then
                BaseFilteredString = self:FilterString(String,PlayerFrom)
            end
            FilteredResults[Player] = BaseFilteredString
        end
    end

    --Return the filtered results.
    return FilteredResults
end

return ServerFilter


--Manages logs that are streamed to clients on the server.
--!strict

local Players = game:GetService("Players")

local Types = require(script.Parent.Parent:WaitForChild("Types"))

local ServerLogsRegistry = {}
ServerLogsRegistry.__index = ServerLogsRegistry

type LogDataEntry = {
    MinimumAdminLevel: number,
    ListeningPlayers: {Player},
    Logs: Types.Logs,
}
export type ServerLogsRegistry = {
    GetLogEntriesFunction: RemoteFunction,
    LogAddedEvent: RemoteEvent,
    Logs: {[string]: LogDataEntry},
} & typeof(setmetatable({}, ServerLogsRegistry))

--[[
Creates a server logs registry instance.
--]]
function ServerLogsRegistry.new(Authorization: Types.Authorization, NexusAdminRemotes: Folder): ServerLogsRegistry
    local self = setmetatable({}, ServerLogsRegistry) :: ServerLogsRegistry

    --Create the remote objects.
    local LogsRegistryEvents = Instance.new("Folder")
    LogsRegistryEvents.Name = "LogsRegistry"
    LogsRegistryEvents.Parent = NexusAdminRemotes

    local GetLogEntriesFunction = Instance.new("RemoteFunction")
    GetLogEntriesFunction.Name = "GetLogEntries"
    GetLogEntriesFunction.Parent = LogsRegistryEvents
    self.GetLogEntriesFunction = GetLogEntriesFunction

    local LogAddedEvent = Instance.new("RemoteEvent")
    LogAddedEvent.Name = "LogAdded"
    LogAddedEvent.Parent = LogsRegistryEvents
    self.LogAddedEvent = LogAddedEvent

    --Connect getting logs.
    self.Logs = {}
    GetLogEntriesFunction.OnServerInvoke = function(Player: Player, LogName: string): ({any} | string, number?)
        --Get the log.
        local LogData = self.Logs[LogName]
        if not LogData then
            return "Log not found"
        end

        --Check if the player is authorized.
        if LogData.MinimumAdminLevel and not Authorization:IsPlayerAuthorized(Player, LogData.MinimumAdminLevel) then
            return "Unauthorized"
        end

        --Add the players to the listeners.
        local PlayerListening = false
        for _, OtherPlayer in LogData.ListeningPlayers do
            if Player ~= OtherPlayer then continue end
            PlayerListening = true
            break
        end
        if not PlayerListening then
            table.insert(LogData.ListeningPlayers, Player)
        end

        --Return the log entires.
        return LogData.Logs:GetLogs(), LogData.Logs.MaxLogs
    end

    --Connect players leaving.
    Players.PlayerRemoving:Connect(function(Player: Player)
        for _, LogData in self.Logs do
            for i = 1, #LogData.ListeningPlayers do
                if LogData.ListeningPlayers[i] == Player then
                    table.remove(LogData.ListeningPlayers, i)
                    break
                end
            end
        end
    end)

    --Return the object.
    return self
end

--[[
Registers a log that can be streamed.
--]]
function ServerLogsRegistry.RegisterLogs(self: ServerLogsRegistry, LogName: string, Logs: Types.Logs, MinimumAdminLevel: number): ()
    --Store the log.
    if self.Logs[LogName] then
        error(`Log already registered: {LogName}`)
    end
    self.Logs[LogName] = {
        Logs = Logs,
        MinimumAdminLevel = MinimumAdminLevel,
        ListeningPlayers = {},
    }

    --Connect entries being stored.
    Logs.LogAdded:Connect(function(LogEntry)
        for _, Player in (self.Logs :: {[string]: LogDataEntry})[LogName].ListeningPlayers do
            self.LogAddedEvent:FireClient(Player, LogName, LogEntry)
        end
    end)
end

--[[
Gets a registered log.
--]]
function ServerLogsRegistry.GetLogs(self: ServerLogsRegistry, LogName: string): Types.Logs
    if not self.Logs[LogName] then
        error(`Unable to get registered log "{LogName}": Log not found.`)
    end
    return self.Logs[LogName].Logs
end

return ServerLogsRegistry


--Sends messages to players.
--!strict

local ServerMessages = {}
ServerMessages.__index = ServerMessages

export type ServerMessages = {
    DisplayHintEvent: RemoteEvent,
    DisplayMessageEvent: RemoteEvent,
    DisplayNotificationEvent: RemoteEvent,
} & typeof(setmetatable({}, ServerMessages))

--[[
Creates a server messages instance.
--]]
function ServerMessages.new(NexusAdminRemotes: Folder): ServerMessages
    --Create the remote objects.
    local MessageEvents = Instance.new("Folder")
    MessageEvents.Name = "MessageEvents"
    MessageEvents.Parent = NexusAdminRemotes

    local DisplayHintEvent = Instance.new("RemoteEvent")
    DisplayHintEvent.Name = "DisplayHint"
    DisplayHintEvent.Parent = MessageEvents

    local DisplayMessageEvent = Instance.new("RemoteEvent")
    DisplayMessageEvent.Name = "DisplayMessage"
    DisplayMessageEvent.Parent = MessageEvents

    local DisplayNotificationEvent = Instance.new("RemoteEvent")
    DisplayNotificationEvent.Name = "DisplayNotification"
    DisplayNotificationEvent.Parent = MessageEvents

    --Create and return the object.
    return setmetatable({
        DisplayHintEvent = DisplayHintEvent,
        DisplayMessageEvent = DisplayMessageEvent,
        DisplayNotificationEvent = DisplayNotificationEvent,
    }, ServerMessages) :: ServerMessages
end

--[[
Sends a message to a player.
--]]
function ServerMessages.DisplayMessage(self: ServerMessages, Player: Player, TopText: string, Message: string, DisplayTime: number?): ()
    self.DisplayMessageEvent:FireClient(Player, TopText, Message, DisplayTime)
end

--[[
Sends a hint to a player.
--]]
function ServerMessages.DisplayHint(self: ServerMessages, Player: Player, Message: string, DisplayTime: number): ()
    self.DisplayHintEvent:FireClient(Player, Message, DisplayTime)
end

--[[
Sends a notification to a player.
--]]
function ServerMessages.DisplayNotification(self: ServerMessages, Player: Player, TopText: string, Message: string, DisplayTime: number?): ()
    self.DisplayNotificationEvent:FireClient(Player, TopText, Message, DisplayTime)
end

return ServerMessages


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

-- stylua: ignore
process.exec(
	"rojo",
	{ "build", "--output", "fluid.rbxm" },
	{ stdio = "forward" }
)


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

process.exec(
	"rojo",
	{ "build", "roblox-tests.project.json", "--output", "test-file.rbxl" },
	{ stdio = "forward" }
)


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

task.spawn(function()
	process.exec("rojo", {
		"sourcemap",
		"roblox-tests.project.json",
		"--output",
		"sourcemap.json",
		"--watch",
		"--include-non-scripts",
	}, {
		stdio = "forward",
	})
end)

task.spawn(function()
	-- stylua: ignore
	process.exec(
		"rojo",
		{ "serve", "roblox-tests.project.json" },
		{ stdio = "forward" }
	)
end)


local graph = require("../reactive/graph")
local oklab = require("../utils/oklab")
local read = require("../utils/read")
local types = require("../reactive/types")

local active_animations = {}

local function vector_lerp(a: vector, b: vector, t: number): vector
	return a + (b - a) * t
end

local function general_lerp(a: any, b: any, t: number): any
	return a + (b - a) * t
end

local lerp_functions: { [string]: (a: any, b: any, t: number) -> any } = {
	number = math.lerp,
	Vector3 = vector_lerp,
	Vector2 = general_lerp,
	Color3 = function(a: Color3, b: Color3, t)
		local initial_vector = oklab.from_srgb(vector.create(a.R, a.G, a.B))
		local goal_vector = oklab.from_srgb(vector.create(b.R, b.G, b.B))
		local interpolated_vector = vector_lerp(initial_vector, goal_vector, t)
		local result_vector =
			vector.clamp(oklab.to_srgb(interpolated_vector), vector.zero, vector.one)

		return Color3.new(result_vector.x, result_vector.y, result_vector.z)
	end,
	CFrame = CFrame.identity.Lerp,
	Rect = function(a: Rect, b: Rect, t: number)
		return Rect.new(
			general_lerp(a.Min, b.Min, t) :: Vector2,
			general_lerp(a.Max, b.Max, t) :: Vector2
		)
	end,
	UDim = function(a: UDim, b: UDim, t: number)
		return UDim.new(math.lerp(a.Scale, b.Scale, t), math.lerp(a.Offset, b.Offset, t))
	end,
	UDim2 = UDim2.new().Lerp,
}

local lerp = {}

function lerp.create<T>(goal: types.UsedAs<T>, takes: types.UsedAs<number>)
	graph.set_group(graph.new_group("lerp"))
	local output = graph.create_source_node(read(goal))

	local function effect()
		active_animations[output] = {
			start = output.cached_value,
			goal = read(goal),

			takes = takes,
			time_passed = 0,
		}

		return nil
	end

	graph.create_reactive_node(graph.assert_stable_parent(), effect, "deferred")
	graph.clear_group()

	return function()
		graph.push_dependency(output)
		return output.cached_value
	end
end

function lerp.step(delta_time: number)
	for output, data in active_animations do
		data.time_passed += delta_time
		local kind = typeof(output.cached_value)
		local fn = lerp_functions[kind]
		local t = math.clamp(data.time_passed / read(data.takes), 0, 1)

		if t == 1 then
			active_animations[output] = nil
		end

		local interpolated = fn(data.start, data.goal, t)
		graph.update_source_node(output, interpolated)
	end
end

return lerp


local function under_dampened(
	t: number,
	speed: number,
	damping: number
): (number, number, number, number)
	local alpha = speed * math.sqrt(1 - damping ^ 2)
	local over_alpha = 1 / alpha
	local exp = math.exp(-1 * t * speed * damping)
	local exp_sin = exp * math.sin(alpha * t)
	local exp_cos = exp * math.cos(alpha * t)
	local exp_sin_speed_damping_over_alpha = exp_sin * speed * damping * over_alpha

	return exp_sin_speed_damping_over_alpha + exp_cos,
		exp_sin * over_alpha,
		(exp_sin * alpha + speed * damping * exp_sin_speed_damping_over_alpha) * -1,
		exp_cos - exp_sin_speed_damping_over_alpha
end

local function critically_dampened(t: number, speed: number): (number, number, number, number)
	local time_speed = t * speed
	local time_speed_neg1 = time_speed * -1
	local exp = math.exp(time_speed_neg1)

	return exp * (time_speed + 1),
		exp * t,
		exp * (time_speed_neg1 * speed),
		exp * (time_speed_neg1 + 1)
end

local function over_dampened(
	t: number,
	damping: number,
	speed: number
): (number, number, number, number)
	local alpha = math.sqrt(damping ^ 2 - 1)
	local neg_half_over_alpha_speed = -0.5 / (alpha * speed)
	local z1 = speed * (alpha + damping) * -1
	local z2 = speed * (alpha - damping)
	local exp1 = math.exp(t * z1)
	local exp2 = math.exp(t * z2)

	return (exp2 * z1 - exp1 * z2) * neg_half_over_alpha_speed,
		(exp1 - exp2) * neg_half_over_alpha_speed / speed,
		(exp2 - exp1) * neg_half_over_alpha_speed * speed,
		(exp1 * z1 - exp2 * z2) * neg_half_over_alpha_speed
end

local function coefficients(
	t: number,
	damping: number,
	speed: number
): (number, number, number, number)
	if t == 0 or speed == 0 then
		return 1, 0, 0, 1
	end

	if damping > 1 then
		return over_dampened(t, speed, damping)
	elseif damping == 1 then
		return critically_dampened(t, speed)
	else
		return under_dampened(t, speed, damping)
	end
end

return coefficients


--[[
	Temporarily named this until luau language server fixes issue
]]
local coefficients = require("./coefficients")
local graph = require("../../reactive/graph")
local oklab = require("../../utils/oklab")
local read = require("../../utils/read")
local types = require("../../reactive/types")

local SLEEP_VELOCITY = 1e-3
local SLEEP_DISTANCE = 1e-5

type AnimatableDataTypeEnum = "color" | "vector" | "udim" | "udim2" | "number"
type AnimatableDataType = Color3 | vector | UDim | UDim2 | number

-- In order to support springing multiple data types, we need to abstract values into an array.
-- It sucks, but it is necessary to support things like UDim2 and Color3.
type SpringValue = { number }

type SpringState = {
	position: SpringValue,
	velocity: SpringValue,
	target: SpringValue,

	datatype: AnimatableDataTypeEnum,
	elements: number, -- 1-6

	damping: number,
	speed: number,

	unpacked_value: AnimatableDataType,
}

type SpringControls<T> = {
	position: T?,
	impulse: T?,
	velocity: T?,
}

local active_springs: { [SpringState]: graph.SourceNode<AnimatableDataType> } = {}

local function pack_type(value: AnimatableDataType): (AnimatableDataTypeEnum, { number })
	if typeof(value) == "Color3" then
		-- oklab conversion
		local intermediate_vector = oklab.from_srgb(vector.create(value.R, value.G, value.B))

		return "color",
			{
				intermediate_vector.x,
				intermediate_vector.y,
				intermediate_vector.z,
			}
	elseif typeof(value) == "UDim2" then
		return "udim2", { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
	elseif typeof(value) == "UDim" then
		return "udim", { value.Scale, value.Offset }
	elseif type(value) == "vector" then
		return "vector", { value.x, value.y, value.z }
	elseif type(value) == "number" then
		return "number", { value }
	end

	error(`tried tweening {typeof(value)}`)
end

local function unpack_type(
	type_string: AnimatableDataTypeEnum,
	array: { number }
): AnimatableDataType
	if type_string == "color" then
		-- oklab conversion
		local intermediate_vector = vector.clamp(
			oklab.to_srgb(vector.create(array[1], array[2], array[3])),
			vector.zero,
			vector.one
		)

		return Color3.new(intermediate_vector.x, intermediate_vector.y, intermediate_vector.z)
	elseif type_string == "vector" then
		return vector.create(array[1], array[2], array[3])
	elseif type_string == "udim" then
		return UDim.new(array[1], array[2])
	elseif type_string == "udim2" then
		return UDim2.new(array[1], array[2], array[3], array[4])
	elseif type_string == "number" then
		return array[1]
	end

	return error("unreachable")
end

local function subtract(a: { number }, b: { number })
	local c = table.clone(a)
	for idx, value in c do
		c[idx] = value - b[idx]
	end
	return c
end

local function magnitude(a: { number })
	local d = 0
	for idx, value in a do
		d += value ^ 2
	end
	return d
end

local function step_spring<T>(state: SpringState, delta_time: number)
	local pos_pos, pos_vel, vel_pos, vel_vel = coefficients(delta_time, state.damping, state.speed)
	local low_velocity = true

	-- iterate and solve the spring for each value in our SpringValue
	for i = 1, state.elements do
		local start_pos = state.position[i]
		local target_pos = state.target[i]
		local start_vel = state.velocity[i]
		local start_delta = start_pos - target_pos

		-- todo: vectorize?
		local next_delta = (start_delta * pos_pos) + (start_vel * pos_vel)
		local next_velocity = (start_delta * vel_pos) + (start_vel * vel_vel)

		if next_delta ~= next_delta or next_velocity ~= next_velocity then
			warn("nan")
			next_delta, next_velocity = 0, 0
		end

		local next_position = next_delta + target_pos

		state.position[i] = next_position
		state.velocity[i] = next_velocity

		if next_velocity > SLEEP_VELOCITY then
			low_velocity = false
		end
	end

	if low_velocity and magnitude(subtract(state.position, state.target)) < SLEEP_DISTANCE then
		for i = 1, state.elements do
			state.position[i] = state.target[i]
			state.velocity[i] = 0
		end
		active_springs[state] = nil
	end

	state.unpacked_value = unpack_type(state.datatype, state.position)
end

local spring_file = {}

function spring_file.create<T>(
	input: types.UsedAs<T>,
	speed: number,
	damping: number
): (() -> read.CastIntoValue<T>, (SpringControls<T>) -> ())
	-- initialize spring state early so we can reference it in the effect for the output node
	graph.set_group(graph.new_group("spring"))

	local output = graph.create_source_node(read(input))

	local call = graph.evaluate_node(output)
	assert(call.success)

	local starting_value_string, starting_value = pack_type(call.value :: any)

	local spring_state: SpringState = {
		position = starting_value,
		velocity = table.create(#starting_value, 0),
		target = starting_value,

		elements = #starting_value,
		datatype = starting_value_string,

		speed = speed,
		damping = damping,

		unpacked_value = call.value :: any,
	}

	local function on_update()
		local input_as_value = read(input)

		local datatype, array = pack_type(input_as_value)
		assert(datatype == spring_state.datatype, "Can't switch the data types of a spring")

		spring_state.target = array

		-- need luau feature: bounded generics
		active_springs[spring_state] = output :: any

		return nil
	end

	-- set up the dependency
	local input_node = graph.create_reactive_node(
		graph.assert_stable_parent(),
		on_update,
		"deferred",
		read(input)
	)

	graph.evaluate_node(input_node)
	graph.clear_group()

	return function()
		graph.push_dependency(output)
		return output.cached_value
	end, function(controls: SpringControls<T>)
		on_update()

		if controls.position then
			local datatype, array = pack_type(controls.position)
			assert(datatype == spring_state.datatype, "Can't switch the data types of a spring")
			spring_state.position = array
		end

		if controls.velocity then
			local datatype, array = pack_type(controls.velocity)
			assert(datatype == spring_state.datatype, "Can't switch the data types of a spring")
			spring_state.velocity = array
		end

		if controls.impulse then
			local datatype, array = pack_type(controls.impulse)
			assert(datatype == spring_state.datatype, "Can't switch the data types of a spring")
			for i = 1, #array do
				spring_state.velocity[i] += array[i]
			end
		end
	end
end

function spring_file.step(delta_time: number)
	for spring_state, node in active_springs do
		step_spring(spring_state, delta_time)
		graph.update_source_node(node, spring_state.unpacked_value)
	end
end

return spring_file


if _G.__DEV__ == nil then
	-- thanks type state
	_G.__DEV__ = (game and not game:GetService("RunService"):IsStudio()) :: any
end

local action = require("@self/instances/action")
local async = require("@self/reactive/async")
local bind = require("@self/instances/bind")
local changed = require("@self/instances/changed")
local cleanup = require("@self/reactive/cleanup")
local create = require("@self/instances/create")
local deferred = require("@self/reactive/deferred")
local derive = require("@self/reactive/derive")
local effect = require("@self/reactive/effect")
local for_keys = require("@self/instances/for_keys")
local for_values = require("@self/instances/for_values")
local graph = require("@self/reactive/graph")
local interval = require("@self/utils/interval")
local lerp = require("@self/anim/lerp")
local mount = require("@self/instances/mount")
local read = require("@self/utils/read")
local root = require("@self/reactive/root")
local scheduler = require("@self/scheduler")
local show = require("@self/utils/show")
local show_delay = require("@self/utils/show_delay")
local source = require("@self/reactive/source")
local spring = require("@self/anim/spring/spring_file")
local switch = require("@self/utils/switch")
local switch_delay = require("@self/utils/switch_delay")
local tags = require("@self/utils/tags")
local types = require("@self/reactive/types")
local untrack = require("@self/reactive/untrack")

export type UsedAs<T> = types.UsedAs<T>
export type Source<T> = types.Source<T>
export type Action = action.Identity
export type PropertiesFor<T> = create.PropertiesFor<T>

local scheduler_thread: thread? = nil

local function step(delta_time: number)
	-- This will call intervals
	scheduler.step(delta_time)

	-- Runs deferred nodes
	graph.flush_deferred_nodes()

	-- Finally, after all state is reconciled, we animate
	lerp.step(delta_time)
	spring.step(delta_time)
end

if game then
	local RunService = game:GetService("RunService")

	scheduler_thread = task.spawn(function()
		while true do
			local delta_time = RunService.PreRender:Wait()

			step(delta_time)
		end
	end)
end

local fluid = {}

fluid.create = create
fluid.mount = mount
fluid.tags = tags
fluid.bind = bind
fluid.changed = changed

-- actions
fluid.action = action.create
fluid.is_action = action.is_action

-- Reactive graph
fluid.cleanup = cleanup
fluid.derive = derive
fluid.effect = effect
fluid.deferred = deferred
fluid.graph = graph
fluid.root = root
fluid.source = source
fluid.untrack = untrack
fluid.interval = interval
fluid.for_keys = for_keys
fluid.async = async
fluid.for_values = for_values
fluid.read = read
fluid.show = show
fluid.switch = switch
fluid.show_delay = show_delay
fluid.switch_delay = switch_delay

-- Animation
fluid.lerp = lerp.create
fluid.spring = spring.create

-- Scheduling
fluid.provide_scheduler = function(): (delta_time: number) -> ()
	if scheduler_thread then
		task.cancel(scheduler_thread)
		scheduler_thread = nil
	end

	return step
end

fluid.__SCHEDULER_INTERFACE = scheduler

return fluid


local action_metatable_symbol = table.freeze({})

export type Identity = setmetatable<{
	callback: (inst: Instance) -> (),
	priority: number,
}, typeof(action_metatable_symbol)>

local action = {
	metatable = action_metatable_symbol,
}

function action.is_action(action: unknown): boolean
	return type(action) == "table" and (getmetatable(action :: any) == action_metatable_symbol)
end

function action.create(callback: (inst: Instance) -> (), priority: number?): Identity
	return setmetatable({
		callback = callback,
		priority = priority or 1,
	}, action_metatable_symbol)
end

return action


local action = require("./action")
local graph = require("../reactive/graph")
local scheduler = require("../scheduler")
local types = require("../reactive/types")
local untrack = require("../reactive/untrack")

type PropertyPair<T> = {
	tag: "property",
	name: string,
	value: T,
}

type ReactivePropertyPair<T> = {
	tag: "reactive_property",
	name: string,
	value: () -> T,
}

type EventPair<T...> = {
	tag: "event",
	name: string,
	event: (T...) -> (),
}

type ActionPair = {
	tag: "action",
	action: action.Identity,
}

type ChildPair = {
	tag: "child",
	value: Instance,
}

type ReactiveChildPair = {
	tag: "reactive_child",
	value: () -> Instance | { Instance },
}

type Pair =
	| ReactivePropertyPair<unknown>
	| PropertyPair<unknown>
	| EventPair<unknown>
	| ReactiveChildPair
	| ActionPair
	| ChildPair

local function set_property(inst: Instance, property: string, value: any)
	(inst :: any)[property] = value
end

local function get_property(inst: Instance, property: string): any
	return (inst :: any)[property]
end

local function bind_property<T>(inst: Instance, property: string, value: types.Readable<T>)
	graph.set_group(graph.new_group(`bind "{property}"`))
	local property_node = graph.create_reactive_node(graph.assert_stable_parent(), function()
		set_property(inst, property, value())
		return nil
	end, "eager", nil)

	graph.evaluate_node(property_node)
	graph.clear_group()
end

local function bind_event(inst: Instance, event_name: string, func: <T...>(T...) -> ())
	local event = (inst :: any)[event_name] :: RBXScriptSignal
	local connection = event:Connect(func :: any)

	graph.push_cleanup(graph.assert_scope(), function()
		connection:Disconnect()
	end)
end

type Result = Instance | { Result } | () -> Result

local function reactive_child_effect(
	inst: Instance,
	func: () -> Result,
	sets: {
		new: { [Instance]: true },
		current: { [Instance]: true },
	}
)
	local new_child_set = sets.new
	local current_child_set = sets.current
	local source_result = func()

	if not source_result then
		for old_inst in current_child_set do
			old_inst.Parent = nil
		end

		return sets
	end

	local function process_child(child: Result)
		if typeof(child) == "Instance" then
			new_child_set[child] = true

			if current_child_set[child] then
				current_child_set[child] = nil
				return
			end

			child.Parent = inst
			return
		end

		if typeof(child) == "function" then
			local children: { new: { [Instance]: true }, current: { [Instance]: true } } =
				{ new = {}, current = {} }

			graph.create_reactive_node(graph.get_scope() :: any, function(value)
				return reactive_child_effect(inst, child, value)
			end, "deferred", children)

			graph.push_cleanup(graph.get_scope(), function()
				for child in children.current do
					child.Parent = nil
				end
			end)

			return
		end

		for _, child in child do
			process_child(child)
		end

		return
	end

	process_child(source_result)

	for child in current_child_set do
		child.Parent = nil
	end

	table.clear(current_child_set)
	sets.current, sets.new = new_child_set, current_child_set

	return sets
end

local function bind_reactive_child(inst: Instance, func: () -> Result)
	graph.set_group(graph.new_group("children"))
	local stable_node = graph.get_scope() :: graph.StableNode<unknown>

	local children: { new: { [Instance]: true }, current: { [Instance]: true } } =
		{ new = {}, current = {} }

	local property_node = graph.create_reactive_node(stable_node :: any, function(value)
		return reactive_child_effect(inst, func, value)
	end, "deferred", children)

	graph.evaluate_node(property_node)
	graph.clear_group()
end

local function evaluate(
	inst: Instance,
	props: { [unknown & number]: unknown },
	existing_array: { Pair }?
): { Pair }
	local resulting_array: { Pair } = existing_array or {}

	for key, value in props do
		local value_is_action = action.is_action(value :: any)

		if type(key) == "number" and type(value) == "table" and value_is_action then
			table.insert(resulting_array, {
				tag = "action",
				action = value,
			})
		elseif type(key) == "number" and type(value) == "table" and not value_is_action then
			evaluate(inst, value :: any, resulting_array)
		elseif type(key) == "number" and type(value) == "function" then
			table.insert(resulting_array, {
				tag = "reactive_child",
				value = value,
			})
		elseif type(key) == "string" and typeof(get_property(inst, key)) == "RBXScriptSignal" then
			table.insert(resulting_array, {
				tag = "event",
				name = key,
				event = value,
			})
		elseif
			type(key) == "string"
			and typeof(get_property(inst, key)) ~= "RBXScriptSignal"
			and type(value) == "function"
		then
			table.insert(resulting_array, {
				tag = "reactive_property",
				name = key,
				value = value,
			})
		elseif
			type(key) == "string"
			and typeof(get_property(inst, key)) ~= "RBXScriptSignal"
			and type(value) ~= "function"
		then
			table.insert(resulting_array, {
				tag = "property",
				name = key,
				value = value,
			})
		elseif typeof(value) == "Instance" then
			table.insert(resulting_array, {
				tag = "child",
				value = value,
			})
		end
	end

	return resulting_array
end

local function bind(inst: Instance, props: { [unknown & number]: unknown })
	local pairings = evaluate(inst, props)
	local parent: (ReactivePropertyPair<Instance> | PropertyPair<Instance>)? = nil

	for _, pairing in pairings do
		if
			(pairing.tag == "property" or pairing.tag == "reactive_property")
			and pairing.name == "Parent"
		then
			parent = pairing :: any
			continue
		end

		if pairing.tag == "property" then
			scheduler.defer(set_property, inst, pairing.name, pairing.value)
		elseif pairing.tag == "reactive_property" then
			bind_property(inst, pairing.name, pairing.value)
		elseif pairing.tag == "event" then
			bind_event(inst, pairing.name, pairing.event)
		elseif pairing.tag == "child" then
			scheduler.defer(function()
				pairing.value.Parent = inst
			end)
		elseif pairing.tag == "reactive_child" then
			bind_reactive_child(inst, pairing.value)
		elseif pairing.tag == "action" then
			pairing.action.callback(inst)
		else
			error("unreachable")
		end
	end

	if parent and parent.tag == "reactive_property" then
		bind_property(inst, "Parent", parent.value)
	elseif parent and parent.tag == "property" then
		set_property(inst, "Parent", parent.value)
	end

	return inst
end

return bind


local action = require("./action")
local graph = require("../reactive/graph")

local function changed(property: string, fn: (any) -> ())
	return action.create(function(instance: Instance)
		fn((instance :: any)[property])

		local con = instance:GetPropertyChangedSignal(property):Connect(function()
			fn((instance :: any)[property])
		end)

		graph.push_cleanup(graph.assert_scope(), function()
			con:Disconnect()
		end)
	end)
end

return changed


local action = require("./action")
local bind = require("./bind")

type Instances = {
	AudioChannelSplitter: AudioChannelSplitter,
	AudioPitchShifter: AudioPitchShifter,
	AudioDeviceOutput: AudioDeviceOutput,
	AudioChannelMixer: AudioChannelMixer,
	AudioTextToSpeech: AudioTextToSpeech,
	AudioDeviceInput: AudioDeviceInput,
	AudioDistortion: AudioDistortion,
	AudioCompressor: AudioCompressor,
	AudioEqualizer: AudioEqualizer,
	AudioAnalyzer: AudioAnalyzer,
	AudioListener: AudioListener,
	AudioRecorder: AudioRecorder,
	AudioLimiter: AudioLimiter,
	AudioFlanger: AudioFlanger,
	AudioEmitter: AudioEmitter,
	AudioPlayer: AudioPlayer,
	AudioReverb: AudioReverb,
	AudioFilter: AudioFilter,
	ArcHandles: ArcHandles,
	AudioFader: AudioFader,
	AudioEcho: AudioEcho,
	AdGui: AdGui,
	BoxHandleAdornment: BoxHandleAdornment,
	BillboardGui: BillboardGui,
	BlurEffect: BlurEffect,
	CylinderHandleAdornment: CylinderHandleAdornment,
	ConeHandleAdornment: ConeHandleAdornment,
	CanvasGroup: CanvasGroup,
	Camera: Camera,
	DialogChoice: DialogChoice,
	DragDetector: DragDetector,
	Dialog: Dialog,
	Folder: Folder,
	Frame: Frame,
	Highlight: Highlight,
	Handles: Handles,
	ImageHandLeAdornment: ImageHandleAdornment,
	ImageButton: ImageButton,
	ImageLabel: ImageLabel,
	LineHandleAdornment: LineHandleAdornment,
	ProximityPrompt: ProximityPrompt,
	Part: Part,
	SphereHandLeAdornment: SphereHandleAdornment,
	SelectionSphere: SelectionSphere,
	ScrollingFrame: ScrollingFrame,
	SelectionBox: SelectionBox,
	StyleDerive: StyleDerive,
	SurfaceGui: SurfaceGui,
	StyleSheet: StyleSheet,
	SoundGroup: SoundGroup,
	StyleLink: StyleLink,
	ScreenGui: ScreenGui,
	StyleBase: StyleBase,
	StyleRule: StyleRule,
	Sound: Sound,
	TextButton: TextButton,
	TextLabel: TextLabel,
	TextBox: TextBox,
	UIAspectRatioConstraint: UIAspectRatioConstraint,
	UITextSizeConstraint: UITextSizeConstraint,
	UISizeConstraint: UISizeConstraint,
	UIDragDetector: UIDragDetector,
	UITableLayout: UITableLayout,
	UIPageLayout: UIPageLayout,
	UIGridLayout: UIGridLayout,
	UIListLayout: UIListLayout,
	UIGradient: UIGradient,
	UIFlexItem: UIFlexItem,
	UIPadding: UIPadding,
	UIStroke: UIStroke,
	UICorner: UICorner,
	UIScale: UIScale,
	ViewportFrame: ViewportFrame,
	VideoFrame: VideoFrame,
	WireframeHandleAdornment: WireframeHandleAdornment,
	WorldModel: WorldModel,
	Wire: Wire,
}

type UnknownConnectCallback = (...any) -> ()

type Signal = RBXScriptSignal

type function get_connect(v: type): type?
	local connect = v:readproperty(types.singleton("Connect"))

	if connect and connect:is("function") then
		local head = connect:parameters().head

		if not head then
			return nil
		end
		local callback = head[2]

		if callback and callback:is("function") then
			return callback
		end
	end

	return nil
end

type function is_signal(v: type): type?
	if v == Signal and v.tag == "class" then
		return UnknownConnectCallback
	-- luau lsp types RBXScriptSignals as tables
	elseif v.tag == "table" then
		local connect = get_connect(v)

		if connect then
			return connect
		end

		-- i cba to test if this is needed or not
		local metatable = v:metatable()

		if metatable then
			connect = get_connect(metatable)

			if connect then
				return connect
			end
		end
	end

	return nil
end

type instance = Instance

type Action = action.Identity

type function GetInstance(class_name: type, instances: type): type
	if not class_name:is("singleton") then
		return instance
	end

	return instances:readproperty(class_name) or instance
end

-- temp arg for action, because for some reason its not added as a global automatically
export type function PropertiesFor(class_name: type): type
	local object = GetInstance(class_name, Instances)
	local head_table = {}
	local args_table = { head = head_table }
	local properties = types.newtable()
	local current = object
	local empty_table = {}

	local read_only_properties = table.freeze({
		types.singleton("RobloxLocked"),
		types.singleton("ClassName"),
		types.singleton("UniqueId"),
	})

	while current do
		for property, value in current:properties() do
			if table.find(read_only_properties, property) then
				continue
			end

			local value_read = value.read :: type
			local callback = is_signal(value_read)

			if callback then
				properties:setproperty(property :: any, types.optional(callback))
			elseif value_read.tag == "function" or value_read.tag == "intersection" then
				continue
			elseif value.write then
				head_table[1] = value_read

				properties:setproperty(
					property :: any,
					types.optional(
						types.unionof(
							types.newfunction(empty_table :: any, args_table :: any),
							value.write :: any
						)
					)
				)
			end
		end

		current = current:readparent()
	end

	local nested_table_or_function_or_child = types.newtable()
	local function_returns_child = types.newfunction()
	local union =
		types.unionof(nested_table_or_function_or_child, function_returns_child, instance, Action)

	nested_table_or_function_or_child:setindexer(types.number, union)
	function_returns_child:setreturns({ union })

	properties:setindexer(types.number, types.optional(union))

	return properties
end

local function try_create_instance(class_name: string): Instance
	local success, call_result = pcall(Instance.new, class_name)
	assert(success, call_result)

	return call_result
end

local function create<ClassName>(
	class_name: ClassName | keyof<Instances> | ""
): (
	props: PropertiesFor<ClassName>
) -> GetInstance<ClassName, Instances>
	return function(properties)
		return bind(try_create_instance(class_name), properties) :: any
	end
end

return create


local graph = require("../reactive/graph")
local read = require("../utils/read")
local scheduler = require("../scheduler")
local types = require("../reactive/types")

local function indexes<K, VI, VO>(
	input: types.UsedAs<{ [K]: VI }>,
	transform: (
		() -> VI,
		K,
		active: types.Readable<boolean>
	) -> (VO, number?)
): () -> { VO }
	graph.set_group(graph.new_group("for_keys"))
	local parent = graph.assert_stable_parent()
	local child_parent = graph.create_stable_node(parent)

	local input_nodes: { [K]: graph.SourceNode<VI> } = {}
	local scopes: { [K]: graph.StableNode<any> } = {}
	local output_cache: { [K]: VO } = {}
	-- given a key, maintains how long it should be delayed by (if necessary)
	-- and a list of source nodes given to these nodes for if a given node is
	-- active or not.
	local output_delays: { [K]: number? } = {}
	local output_active: { [K]: graph.SourceNode<boolean> } = {}
	local destruction_threads: { [K]: thread } = {}
	local reactive_node = graph.create_reactive_node(child_parent, false, "lazy", {})
	-- due to how the hybrid push pull graph works, we can't invalidate the reactive
	-- node. doing so will cause the graph to look at it's dependencies and
	-- make the correct assumption that the reactive node hasn't updated.
	-- by adding a useless dependency, we can force it to work.
	local useless_dependency = graph.create_source_node(0)

	local function destroy(key: K)
		graph.destroy(scopes[key])
		input_nodes[key] = nil
		scopes[key] = nil
		output_cache[key] = nil
		output_active[key] = nil
		output_delays[key] = nil
		destruction_threads[key] = nil
	end

	local function destroy_delayed(key: K)
		destroy(key)
		graph.update_source_node(useless_dependency, useless_dependency.cached_value + 1)
	end

	local function update(): { VO }
		local input: { [K]: VI } = read(input)
		graph.push_dependency(useless_dependency)

		-- remove unused values
		for key in input_nodes do
			if input[key] or destruction_threads[key] then
				continue
			end
			local node = output_active[key]
			local delay_by = output_delays[key]
			graph.update_source_node(node, false)

			if delay_by then
				destruction_threads[key] = scheduler.delay(delay_by, destroy_delayed, key)
			else
				destroy(key)
			end
		end

		-- process new / changed values
		for key, value in input do
			local source_node: graph.SourceNode<VI> = input_nodes[key]

			if destruction_threads[key] then
				scheduler.cancel(destruction_threads[key])
				destruction_threads[key] = nil
				graph.update_source_node(output_active[key], true)
			end

			if source_node then -- changed
				graph.update_source_node(source_node, value)
			else -- new
				local value_node = graph.create_source_node(value)
				local output_node = graph.create_stable_node(child_parent)
				local active_node = graph.create_source_node(true)

				local function read_value(): VI
					graph.push_dependency(value_node)
					return value_node.cached_value
				end

				local function read_active(): boolean
					graph.push_dependency(active_node)
					return active_node.cached_value
				end

				local result: VO, delay: number? =
					graph.run_as_unsafe(output_node, transform, read_value, key, read_active)

				if delay then
					output_delays[key] = delay
				end

				output_active[key] = active_node
				input_nodes[key] = value_node
				scopes[key] = output_node
				output_cache[key] = result
			end
		end

		local output_array = {}
		local idx = 0
		for _, value in output_cache do
			output_array[idx + 1] = value
			idx += 1
		end

		return output_array
	end

	reactive_node.effect = update
	graph.clear_group()

	return function()
		local result = graph.evaluate_node(reactive_node)
		return if result.success then result.value else error(result.err)
	end
end

return indexes :: (<K, VI, VO>(
	input: types.UsedAs<{ [K]: VI }>,
	transform: (() -> VI, K, active: types.Readable<boolean>) -> VO
) -> () -> { VO }) & <K, VI, VO>(
	input: types.UsedAs<{ [K]: VI }>,
	transform: (() -> VI, K, active: types.Readable<boolean>) -> (VO, number?)
) -> () -> { VO }


local graph = require("../reactive/graph")
local read = require("../utils/read")
local scheduler = require("../scheduler")
local types = require("../reactive/types")

local function values<V, KI, KO>(
	input: types.UsedAs<{ [KI]: V }>,
	transform: (
		value: V,
		key: () -> KI,
		active: types.Readable<boolean>
	) -> (KO, number?)
): () -> { KO }
	graph.set_group(graph.new_group("for_values"))
	local parent = graph.assert_stable_parent()
	local child_parent = graph.create_stable_node(parent)

	local input_nodes: { [V]: graph.SourceNode<KI> } = {}
	local scopes: { [V]: graph.StableNode<any> } = {}
	local output_cache: { [V]: KO } = {}
	-- given a key, maintains how long it should be delayed by (if necessary)
	-- and a list of source nodes given to these nodes for if a given node is
	-- active or not.
	local output_delays: { [V]: number? } = {}
	local output_active: { [V]: graph.SourceNode<boolean> } = {}
	local destruction_threads: { [V]: thread } = {}
	local reactive_node = graph.create_reactive_node(child_parent, false, "lazy", {})
	-- due to how the hybrid push pull graph works, we can't invalidate the reactive
	-- node. doing so will cause the graph to look at it's dependencies and
	-- make the correct assumption that the reactive node hasn't updated.
	-- by adding a useless dependency, we can force it to work.
	local useless_dependency = graph.create_source_node(0)

	local function destroy(value: V)
		graph.destroy(scopes[value])
		input_nodes[value] = nil
		scopes[value] = nil
		output_cache[value] = nil
		output_active[value] = nil
		output_delays[value] = nil
		destruction_threads[value] = nil
		graph.invalidate(reactive_node)
	end

	local function destroy_delayed(value: V)
		destroy(value)
		graph.update_source_node(useless_dependency, useless_dependency.cached_value + 1)
	end

	local function update(): { KO }
		local input: { [KI]: V } = read(input)
		graph.push_dependency(useless_dependency)

		local reversed: { [V]: KI } = {}
		for key, value in input do
			reversed[value] = key
		end

		-- remove unused values
		for value in input_nodes do
			if reversed[value] or destruction_threads[value] then
				continue
			end

			local node = output_active[value]
			local delay_by = output_delays[value]
			graph.update_source_node(node, false)

			if delay_by then
				destruction_threads[value] = scheduler.delay(delay_by, destroy_delayed, value)
			else
				destroy(value)
			end
		end

		-- process new / changed values
		for key, value in input do
			local source_node: graph.SourceNode<KI> = input_nodes[value]

			if destruction_threads[value] then
				scheduler.cancel(destruction_threads[value])
				destruction_threads[value] = nil
				graph.update_source_node(output_active[value], true)
			end

			if source_node then -- changed
				graph.update_source_node(source_node, key)
			else -- new
				local key_node = graph.create_source_node(key)
				local output_node = graph.create_stable_node(child_parent)
				local active_node = graph.create_source_node(true)

				local function read_key(): KI
					graph.push_dependency(key_node)
					return key_node.cached_value
				end

				local function read_active(): KI
					graph.push_dependency(active_node)
					return active_node.cached_value
				end

				local result: KO, delay: number? =
					graph.run_as_unsafe(output_node, transform, value, read_key, read_active)

				if delay then
					output_delays[value] = delay
				end

				output_active[value] = active_node
				input_nodes[value] = key_node
				scopes[value] = output_node
				output_cache[value] = result
			end
		end

		local output_array = {}
		local idx = 0
		for _, value in output_cache do
			output_array[idx + 1] = value
			idx += 1
		end

		return output_array
	end

	reactive_node.effect = update
	graph.clear_group()

	return function()
		local result = graph.evaluate_node(reactive_node)
		return if result.success then result.value else error(result.err)
	end
end

-- need to do this because nil != ()
return values :: (<V, KI, KO>(
	input: types.UsedAs<{ [KI]: V }>,
	transform: (value: V, key: () -> KI, active: types.Readable<boolean>) -> KO
) -> () -> { KO }) & <V, KI, KO>(
	input: types.UsedAs<{ [KI]: V }>,
	transform: (value: V, key: () -> KI, active: types.Readable<boolean>) -> (KO, number?)
) -> () -> { KO }


local bind = require("./bind")
local root = require("../reactive/root")

local function mount(fn: () -> { [any]: any }, inst: Instance): () -> ()
	return root(function()
		bind(inst, {
			fn(),
		})
	end)
end

return mount


--!optimize 1
local colorful = require("./vendor/colorful")
local scheduler = require("./scheduler")

local dev_mode = _G.__DEV__ == true

local warn_storage: { [string]: true } = {}

local function format(text: string, is_supressed: boolean): string
	if is_supressed then
		return (string.format("[fluid][SUPRESSED]: %*", text))
	end
	return (string.format("[fluid]: %*", text))
end

local function use_traceback(text: string, is_supressed: boolean): string
	return (debug.traceback(format(text, is_supressed), 3))
end

local function warn_facade(text: string)
	if warn then
		warn(text)
		return
	end
	print(colorful.color.yellowBright(text))
end

local logging = {
	__warn_facade = warn_facade,
}

function logging.trace(text: string, traceback: boolean?)
	if traceback == nil then
		traceback = dev_mode
	end

	if dev_mode then
		print(if traceback then use_traceback(text, false) else format(text, false))
	end
end

function logging.warn(text: string, traceback: boolean?)
	if traceback == nil then
		traceback = dev_mode
	end

	logging.__warn_facade(if traceback then use_traceback(text, false) else format(text, false))
end

-- blocks warns from overlapping in most notably that happen everyframe
function logging.warn_suppressed(text: string, traceback: boolean?)
	if warn_storage[text] then
		return
	end

	warn_storage[text] = true

	if traceback == nil then
		traceback = dev_mode
	end

	logging.__warn_facade(if traceback then use_traceback(text, true) else format(text, true))

	scheduler.delay(10, function()
		warn_storage[text] = nil
	end)
end

return logging


local graph = require("./graph")
local logging = require("../logging")
local task = task or require("@lune/task")

type State = "busy" | "ok"

--[[
async nodes creates a source node for the latest valid value and for the state.
it also creates a reactive node which spawns a new thread that it runs under it's
own scope.

when created, the async node gets immediately evaluated similar to an effect.
unlike an effect though, all evaluations are deferred since we make the assumption
that async yields every single time.
]]

local function async<T>(callback: (set: (T) -> ()) -> T): (() -> T, () -> State)
	graph.set_group(graph.new_group("async"))
	local reactive_node: graph.ReactiveNode<any>
	local value: graph.ReactiveNode<T> = graph.create_reactive_node(
		graph.assert_stable_parent(),
		function()
			graph.defer_node(reactive_node :: any)
		end :: any,
		"async"
	)
	local state: graph.SourceNode<State> = graph.create_source_node("ok") :: any

	local function set(new: T)
		graph.update_source_node(value :: any, new)
	end

	local function update()
		assert(reactive_node, "reactive node does not exist")
		graph.update_source_node(state, "busy")

		-- manually set the node to be busy
		local thread = task.spawn(function()
			graph.flush_cleanups(value)
			graph.destroy_owned(value)

			value.state = coroutine.running()
			local call = graph.run_as(value, callback, set)

			if call.success then
				set(call.value)
			else
				logging.warn(`async effect error: {call.err} {call.trace}`)
			end

			graph.update_source_node(state, "ok")
			value.state = "clean"
		end)

		graph.push_cleanup(reactive_node, function()
			task.cancel(thread)
		end)
	end

	reactive_node =
		graph.create_reactive_node(graph.assert_stable_parent(), update :: any, "deferred")

	graph.evaluate_node(reactive_node)
	graph.clear_group()

	return function()
		graph.evaluate_node(reactive_node)
		graph.push_dependency(value)
		return value.cached_value
	end, function()
		graph.evaluate_node(reactive_node)
		graph.push_dependency(state)
		return state.cached_value
	end
end

return async


local graph = require("./graph")
local get_scope = graph.get_scope
local push_cleanup = graph.push_cleanup

local function helper(obj: any)
	return if typeof(obj) == "RBXScriptConnection"
		then function()
			obj:Disconnect()
		end
		elseif type(obj) == "thread" then function()
			task.cancel(obj)
		end
		elseif typeof(obj) == "Instance" then function()
			obj:Destroy()
		end
		elseif obj.destroy then function()
			obj:destroy()
		end
		elseif obj.disconnect then function()
			obj:disconnect()
		end
		elseif obj.Destroy then function()
			obj:Destroy()
		end
		elseif obj.Disconnect then function()
			obj:Disconnect()
		end
		else error("cannot cleanup given object")
end

local function cleanup(value: unknown)
	local scope = get_scope()

	if not scope then
		error("cannot cleanup outside a stable or reactive scope")
	end
	assert(scope)

	if type(value) == "function" then
		push_cleanup(scope, value :: () -> ())
	else
		push_cleanup(scope, helper(value))
	end
end

type Destroyable = { destroy: (any) -> () } | { Destroy: (any) -> () }
type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }

return cleanup :: ((callback: () -> ()) -> ()) & ((thread: thread) -> ()) & ((instance: Destroyable) -> ()) & ((connection: Disconnectable) -> ()) & ((instance: Instance) -> ()) & ((connection: RBXScriptConnection) -> ())


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

local function deferred<T>(callback: types.Effect<T>, initial_value: T)
	graph.set_group(graph.new_group("deferred"))
	local node = graph.create_reactive_node(
		graph.assert_stable_parent(),
		callback,
		"deferred",
		initial_value
	)

	graph.evaluate_node(node)
	graph.clear_group()
end

-- cover effect(function() end) and effect(function() return 1 end, 1)
return deferred :: ((fn: () -> ()) -> ()) & (<T>(fn: (T) -> T, initial_value: T) -> ())


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

local function derive<T>(source: types.Effect<T>): types.Readable<T>
	graph.set_group(graph.new_group("derive"))
	local node = graph.create_reactive_node(graph.assert_stable_parent(), source, "lazy")

	local function evaluate()
		local result = graph.evaluate_node(node)

		return if result.success then result.value else error(result, 0)
	end

	graph.clear_group()
	return evaluate
end

return derive


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

local function effect<T>(callback: types.Effect<T>, initial_value: T)
	graph.set_group(graph.new_group("effect"))
	local node =
		graph.create_reactive_node(graph.assert_stable_parent(), callback, "eager", initial_value)

	graph.clear_group()
	graph.evaluate_node(node)
end

-- cover effect(function() end) and effect(function() return 1 end, 1)
return effect :: ((fn: () -> ()) -> ()) & (<T>(fn: (T) -> T, initial_value: T) -> ())


local logging = require("../logging")
local no_yield = require("../utils/no_yield")
local result = require("../result")
local scheduler = require("../scheduler")
local types = require("./types")

local dev_mode = _G.__DEV__ == true

-- groups are an internal way of organizing nodes within developer mode to enhance
-- and add additonial debugging information for the user. nodes, should NEVER be part of multiple groups!
export type NodeGroup = {
	read name: string,

	-- nodes that are part of the group
	[number]: Node<unknown> | SourceNode<unknown>,
}

export type SourceNode<T> = {
	last_evaluation: number,
	read state: "clean",
	effect: false,

	cached_value: T,

	--- Other nodes that depend on the result of this node.
	read dependents: { ReactiveNode<T> },
}

export type StableNode<T> = {
	last_evaluation: number,
	read parent: Node<T> | false,
	read children: { Node<T> },

	--- Describes the effect to run to compute the result of this node.
	effect: false,

	read state: "clean",
	read cached_value: T,

	-- Cleanup functions run when the node dies
	read cleanups: { () -> () },
}

export type ReactiveNode<T> = {
	last_evaluation: number,
	read parent: Node<T> | false,
	read children: { Node<T> },

	--- Describes the effect to run to compute the result of this node.
	effect: types.Effect<T>,
	-- async is similar to eager, but unlike eager does not actually update the
	-- value. this is necessary to support a proper async reactive primitive
	read evaluation: "eager" | "deferred" | "lazy" | "dead" | "async",

	-- clean: the cached_value result can be used immediately
	-- dirty: you need to recompute the node
	-- thread: the node is already being recomputed - throw an error!
	-- dead: the node is dead
	state: "clean" | "dirty" | thread | "dead",

	cached_value: T,

	read cleanups: { () -> () },

	depending_on: { SourceNode<T> | ReactiveNode<T> },
	read dependents: { ReactiveNode<T> },
}

export type Node<T> = ReactiveNode<T> | StableNode<T>

local graph
local pending_eval: { ReactiveNode<unknown> } = {}
local processing_node: { [thread]: Node<unknown> } = {}
local node_groups: { [Node<unknown> | SourceNode<unknown>]: NodeGroup } = setmetatable(
	{},
	{ __mode = "k" }
)
local deferred_nodes = {}
local current_group: NodeGroup? = nil
local version = 0

local function bump_version()
	version += 1
	return version
end

local function get_scope()
	return processing_node[coroutine.running()]
end

local function set_scope(node: Node<any>?)
	processing_node[coroutine.running()] = node
end

local function new_group(name: string): NodeGroup
	if not dev_mode then
		return nil :: any
	end

	return setmetatable({
		name = name,
	}, { __mode = "v" }) -- __mode doesn't collect garbage on strings
end

local function set_group(group: NodeGroup)
	if not dev_mode then
		return
	end

	current_group = group
end

local function clear_group()
	if not dev_mode then
		return
	end

	current_group = nil
end

local function get_group(node: Node<unknown> | SourceNode<unknown>): NodeGroup?
	return node_groups[node]
end

local function run_as_unsafe<T..., U...>(owner: Node<any>?, fn: (U...) -> T..., ...: U...): T...
	local previous_processing = get_scope()
	set_scope(owner)

	local no_yield_check = dev_mode
		and (owner == nil or not owner.effect or owner["evaluation"] ~= "async")

	local a, b
	if no_yield_check then
		-- let us catch this, format and then throw
		local ok, r1, r2 = xpcall(no_yield.check, no_yield.error_handler, fn, ...)

		if not ok then
			error(`{r1.message}\n{r1.trace}`)
		end

		a, b = r1, r2
	else
		a, b = fn(...)
	end

	set_scope(previous_processing)
	return a, b
end

local function run_as<T, U...>(owner: Node<any>?, fn: (U...) -> T, ...: U...): result.Identity<T, unknown>
	local previous_processing = get_scope()
	set_scope(owner)

	local no_yield_check = dev_mode
		and (owner == nil or not owner.effect or owner["evaluation"] ~= "async")

	local call: result.Identity<T, unknown>
	if no_yield_check then
		local ok, r1 = xpcall(no_yield.check, no_yield.error_handler, fn, ...)

		call = if ok then result.ok(r1) else result.fail(r1.message, r1.trace)
	else
		call = result.call(fn, ...)
	end

	set_scope(previous_processing)

	return call
end

local deferred_thread: thread?
local function defer_node<T>(node: ReactiveNode<T>)
	if node.state == "dirty" then
		return
	end
	node.state = "dirty" :: any
	table.insert(deferred_nodes, node)

	if deferred_thread == nil then
		deferred_thread = scheduler.defer(function()
			assert(graph)

			graph.flush_deferred_nodes()
			deferred_thread = nil
		end)
	end
end

local function push_dependency<T>(node: SourceNode<T> | ReactiveNode<T>)
	local scope = get_scope()
	if not (scope and scope.effect) then
		return
	end

	assert(scope ~= node, `node tried to add itself as dependency`)

	table.insert(node.dependents, scope)
	table.insert(scope.depending_on, node)
end

local function find_and_swap_pop<T>(t: { T }, v: T)
	local i = table.find(t, v) :: number
	local n = #t
	t[i] = t[n]
	t[n] = nil
end

local function flush_cleanups<T>(node: Node<T>)
	for _, fn in node.cleanups do
		fn()
	end

	table.clear(node.cleanups)
end

local function remove_dependencies<T>(node: Node<T>)
	if not node.effect then
		return
	end

	for idx, dependency in node.depending_on do
		find_and_swap_pop(dependency.dependents, node)
	end
	table.clear(node.depending_on)
end

local function destroy<T>(node: Node<T>)
	flush_cleanups(node)
	remove_dependencies(node)

	if node.parent then
		find_and_swap_pop(node.parent.children, node)

		node.parent = false :: any -- TODO: Luau type state issue
	end

	while node.children[1] do
		destroy(node.children[1])
	end
	table.clear(node.children)
end

local function destroy_owned<T>(node: Node<T>)
	while node.children[1] do
		destroy(node.children[1])
	end
	table.clear(node.children)
end

local function is_similar(a: unknown, b: unknown): boolean
	local both_are_tables = type(a) == "table"
		and type(b) == "table"
		and not table.isfrozen(a :: any)
		and not table.isfrozen(b :: any)

	return if both_are_tables then false else a == b
end

local function update_node<T>(node: ReactiveNode<T>): result.Identity<T, string>
	local should_reevaluate = #node.depending_on == 0
	for _, dependency in node.depending_on do
		-- a dependency was evaluated after this dependency was invalidated
		if dependency.last_evaluation > node.last_evaluation then
			should_reevaluate = true
			break
		elseif dependency.state == "clean" then
			continue
		end
		-- the dependency is dirty, so we have to reevaluate that dependency
		if dependency.state == "dirty" then
			update_node(dependency :: ReactiveNode<T>)
			if dependency.last_evaluation < node.last_evaluation then
				continue
			end
			should_reevaluate = true
			break
		end
		-- if the dependency is running on the same thread, we have a recursive dependency, otherwise
		-- it's fine to just recompute the value and have it invalidate later
		if dependency.state == coroutine.running() then
			return result.fail(
				`a recursive dependency has been found, which may result in an infinite loop. this should be unreachable`,
				debug.traceback(nil, 2)
			)
		end
	end

	if not should_reevaluate then
		node.state = "clean"
		return result.ok(node.cached_value)
	end

	flush_cleanups(node)
	destroy_owned(node)
	remove_dependencies(node)

	node.state = coroutine.running()
	local previous_result = node.cached_value
	local call = run_as(node, node.effect, previous_result)
	-- we mark it as clean and bump the evaluation value since we don't
	-- want to re-run the node even if it errored
	node.state = "clean"

	if call.success then
		push_dependency(node)

		if
			node.evaluation ~= "eager" and is_similar(previous_result, call.value)
			or node.evaluation == "async"
		then
			return result.ok(node.cached_value)
		end

		node.last_evaluation = bump_version()
		node.cached_value = call.value

		return result.ok(node.cached_value)
	else
		logging.warn(`effect error: {call.err}`)
		return result.fail(`effect error: {call.err}`, call.trace)
	end
end

local function is_dirty<T>(node: ReactiveNode<T>)
	if node.state == "clean" then
		return false
	end

	for _, dependency in node.depending_on do
		-- if a dependent was evaluated after this node was evaluated, it's dirty
		if dependency.effect and dependency.state == "dirty" then
			-- make sure the dependency isn't dirty
			update_node(dependency :: ReactiveNode<T>)
		end

		if dependency.last_evaluation > node.last_evaluation then
			return true
		end
	end

	return #node.depending_on == 0
end

local function evaluate_node<T>(node: SourceNode<T> | ReactiveNode<T>): result.Identity<T, string>
	if node.effect == false or node.state == "clean" then
		push_dependency(node :: SourceNode<T> | ReactiveNode<T>)

		return result.ok(node.cached_value)
	elseif coroutine.running() == node.state then
		return result.fail(
			`this creates a recursive dependency, which can result in creating a infinite loop.`,
			debug.traceback(nil, 2)
		)
	elseif type(node.state) == "thread" then
		--todo: should this be allowed?
		return result.ok(node.cached_value)
	elseif node.state == "dead" then
		return result.ok(node.cached_value)
	elseif is_dirty(node :: ReactiveNode<T>) then
		return update_node(node :: ReactiveNode<T>)
	else
		node.state = "clean" :: any
		return result.ok(node.cached_value)
	end
end

local function invalidate_loop<T>(node: SourceNode<T> | ReactiveNode<T>)
	if node.state == "dirty" then
		return
	end

	for _, value in node.dependents do
		invalidate_loop(value)
		if value.evaluation == "eager" or value.evaluation == "async" then
			value.state = "dirty"
			table.insert(pending_eval, value)
		elseif value.evaluation == "deferred" then
			defer_node(value)
		elseif value.evaluation == "lazy" then
			value.state = "dirty"
		end
	end
end

local function invalidate<T>(node: ReactiveNode<T> | SourceNode<T>)
	invalidate_loop(node)

	for _, node in pending_eval do
		for _, dependent in node.depending_on do
			run_as_unsafe(nil, evaluate_node, dependent)
		end

		run_as_unsafe(nil, evaluate_node, node)
	end
	table.clear(pending_eval)
end

graph = {
	-- re-exports of local utilities
	get_scope = get_scope,
	set_scope = set_scope,

	flush_cleanups = flush_cleanups,
	destroy_owned = destroy_owned,

	defer_node = defer_node,

	invalidate = invalidate,
	destroy = destroy,
	run_as_unsafe = run_as_unsafe,
	push_dependency = push_dependency,
	evaluate_node = evaluate_node,
	run_as = run_as,

	new_group = new_group,
	set_group = set_group,
	clear_group = clear_group,
	get_group = get_group,
}

function graph.flush_deferred_nodes()
	for _, node in deferred_nodes do
		local result = evaluate_node(node)

		if result.success == false then
			warn(`deferred node error: {result}`)
		end
	end

	table.clear(deferred_nodes)
end

function graph.create_reactive_node<T>(
	parent: StableNode<T> | false,
	effect: types.Effect<T> | false,
	evaluation: "eager" | "deferred" | "lazy" | "async",
	cached_value: T
): ReactiveNode<T>
	local node: ReactiveNode<T> = {
		parent = parent,
		last_evaluation = bump_version(),

		effect = effect,
		evaluation = evaluation,
		state = "dirty",
		cached_value = cached_value,
		depending_on = {},
		dependents = {},

		children = {} :: { Node<T> },
		cleanups = {},
	}

	if current_group then
		table.insert(current_group, node)
		node_groups[node] = current_group
	end

	if parent then
		table.insert(parent.children, node)
	end

	if evaluation == "deferred" then
		table.insert(deferred_nodes, node)
	end

	return node
end

function graph.create_stable_node<T>(parent: Node<T> | false)
	local node: StableNode<T> = {
		parent = parent,

		effect = false,
		state = "clean",
		last_evaluation = bump_version(),

		cached_value = nil,

		children = {},
		cleanups = {},
	}

	if current_group then
		table.insert(current_group, node)
		node_groups[node] = current_group
	end

	if parent then
		table.insert(parent.children, node)
	end

	return node
end

function graph.create_source_node<T>(value: T): SourceNode<T>
	local node: SourceNode<T> = {
		last_evaluation = bump_version(),
		effect = false,
		cached_value = value,
		state = "clean",
		dependents = {},
	}

	if current_group then
		table.insert(current_group, node)
		node_groups[node] = current_group
	end

	return node
end

function graph.update_source_node<T>(node: SourceNode<T>, value: T)
	if is_similar(node.cached_value, value) then
		return
	end
	node.last_evaluation = bump_version()
	node.cached_value = value
	invalidate(node)
end

function graph.push_cleanup<T>(scope: Node<T>, cleanup: () -> ())
	table.insert(scope.cleanups, cleanup)
end

function graph.assert_scope<T>(): ReactiveNode<T> | StableNode<T>
	return assert(get_scope(), "reactive scope error thing, TODO error message")
end

function graph.assert_stable_parent<T>(): StableNode<T>
	local scope = get_scope()

	if not scope then
		error(`cannot call {debug.info(2, "n")} without a parent scope`, 2)
	elseif scope.effect ~= false then
		error(
			`cannot call {debug.info(2, "n")} while the parent scope is reactive - this might be a component. consider using untrack() while creating the components.`,
			2
		)
	end

	return scope :: StableNode<T>
end

return graph


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

local refs: { [graph.Node<any>]: true? } = {}

local function root<T...>(fn: (destroy: types.Cleanup) -> T...): (types.Cleanup, T...)
	graph.set_group(graph.new_group("root"))
	local node = graph.create_stable_node(graph.get_scope())

	-- prevent gc of root node
	refs[node] = true

	local function destroy()
		assert(refs[node], "root already destroyed")

		refs[node] = nil

		graph.destroy(node)
	end

	local results = {}

	local result = graph.run_as(node, function(destroy)
		results = { fn(destroy) }
		return nil
	end, destroy)

	if not result.success then
		destroy()
		error(result)
	end
	graph.clear_group()

	-- luau type issue w generic type packs
	return destroy, unpack(results) :: any
end

return root


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

local function get_argument_from<T>(...: T): (boolean, T)
	if select("#", ...) == 0 then
		return false, nil :: any
	end

	local first_value = ...
	return true, first_value
end

local function source<T>(initial_value: T): types.Source<T>
	graph.set_group(graph.new_group("source"))
	local node = graph.create_source_node(initial_value)
	graph.clear_group()

	-- This uses varargs so we can tell the difference between f(), f(nil), and f(value)
	return function(...: T): T
		local has_value, value = get_argument_from(...)

		if has_value then
			-- setting the source node
			graph.update_source_node(node, value)
			return value
		else
			-- dependency, returning the value
			graph.push_dependency(node)
			return node.cached_value
		end
	end :: types.Source<T>
end

return source :: (<T>(value: T) -> types.Source<T>) & (<T>() -> types.Source<T>)


export type Readable<T> = () -> T
export type Callable<T> = (value: T) -> ()

export type Effect<T> = (value: T) -> T

export type Source<T> = ((T) -> T) & Readable<T>

export type UsedAs<T> = T | Readable<T>

export type Cleanup = () -> ()

return nil


local graph = require("./graph")

local function untrack<T>(source: () -> T): T
	local scope = graph.get_scope()
	if (not scope) or (scope and not scope.effect) then
		return source()
	end

	-- temporarily unbind effect
	local effect = scope.effect
	scope.effect = false :: any

	local value = source()

	scope.effect = effect

	return value
end

return untrack


type OkResult<T> = {
	success: true,
	value: T,
}

type FailResult<T> = {
	success: false,
	err: T,
	trace: string,
}

export type Identity<T, E> = OkResult<T> | FailResult<E>

local function tostring_result<T>(result: FailResult<T>)
	return `err: {result.err}, trace: {result.trace}`
end

local function ok<T>(value: T): OkResult<T>
	return setmetatable({
		success = true :: true,
		value = value :: T,
	}, { __tostring = tostring_result })
end

local function fail<T>(value: T, trace: string): FailResult<T>
	return setmetatable({
		success = false :: false,
		err = value,
		trace = trace,
	}, { __tostring = tostring_result })
end

local function call<OK, T...>(fn: (T...) -> OK, ...: T...): Identity<OK, unknown>
	local success, value: any = xpcall(fn, function(err: string)
		local message = string.gsub(tostring(err), "^.+:%d+:%s*", "")
		local trace = debug.traceback(nil, 2)

		return {
			message = message,
			trace = trace,
		}
	end, ...)

	if not success then
		return fail(value.message, value.trace)
	else
		return ok(value)
	end
end

return {
	call = call,
	fail = fail,
	ok = ok,
}


local task = (task or require("@lune/task")) :: typeof(task)

type RenderCallback = (delta_time: number) -> ()

local connections: { RenderCallback } = {}

local scheduler = {
	connections = connections,
}

function scheduler.step(delta_time: number)
	for _, callback in connections do
		task.spawn(callback, delta_time)
	end
end

function scheduler.defer<T...>(fn: (T...) -> (), ...: T...)
	return task.defer(fn, ...)
end

function scheduler.delay<T...>(n: number, fn: (T...) -> (), ...: T...): thread
	return task.delay(n, fn, ...)
end

function scheduler.wait<T...>(time: number?): number
	return task.wait(time)
end

function scheduler.cancel(thread: thread)
	return task.cancel(thread)
end

function scheduler.on_render(fn: RenderCallback)
	table.insert(connections, fn)
	return function()
		local index = table.find(connections, fn)
		if index then
			table.remove(connections, index)
		end
	end
end

return scheduler


local graph = require("../reactive/graph")
local logging = require("../logging")
local no_yield = require("./no_yield")
local scheduler = require("../scheduler")

local dev_mode = _G.__DEV__ == true

local function interval<T>(func: (dt: number) -> T, hz: number?, offset: number?): () -> T
	local scope = graph.assert_stable_parent()

	graph.set_group(graph.new_group("interval"))

	local initial_value
	if dev_mode then
		-- throw immediately, lets just format the error
		local ok, r = xpcall(no_yield.check, no_yield.error_handler, func, 0)

		if not ok then
			error(`{r.message}\n{r.trace}`)
		end
	else
		initial_value = func(0)
	end
	local node = graph.create_source_node(initial_value)
	local last_eval = offset and -offset or 0

	graph.clear_group()

	local function on_render(dt: number)
		if hz ~= nil then
			last_eval += dt
			if last_eval <= 1 / hz then
				return
			end
			dt = last_eval
			last_eval = 0
		end

		local new_value
		if not dev_mode then
			new_value = func(dt)
		else
			local ok, err = xpcall(no_yield.check, no_yield.error_handler, func, dt)

			if not ok then
				logging.warn_suppressed(`{err.message}\n{err.trace}`)
				return
			end

			new_value = err
		end

		graph.update_source_node(node, new_value)
	end

	local disconnect = scheduler.on_render(on_render)

	graph.push_cleanup(scope, disconnect)

	return function()
		graph.push_dependency(node)
		return node.cached_value
	end
end

return interval


local fluid_yield_error = "attempted to yield in a callback"

export type Error = {
	trace: string,
	message: string,
	yielded: boolean,
}

-- errors "thread is not yieldable" or "attempt to yield across metamethod/C-call boundary"
-- TODO: Luau type state issue
local function no_yield<T..., R...>(callback: (T...) -> R..., ...: T...): R...
	local packed = table.pack(...)
	-- stylua: ignore
	for _ in function(): nil
		packed = table.pack(callback(table.unpack(packed, 1, packed.n)))
		return nil
	end do
		break
	end

	return table.unpack(packed, 1, packed.n)
end

--[[
	formats yielded errors into "attempted to yield in a callback"

	@example
	```lua
	local function fn()
		coroutine.yield()
	end

	local ok, error = xpcall(no_yield.check, no_yield.error_handler, fn)

	assert(ok == false)

	print(error.message) --> "attempted to yield in a callback"
	```
]]
local function error_handler(err: string): Error
	local message = string.gsub(tostring(err), "^.+:%d+:%s*", "")
	local trace = debug.traceback(nil, 2)

	local yielded = string.match(message, "attempt to yield") ~= nil
		or string.match(message, "thread is not yieldable") ~= nil

	if yielded then
		message = fluid_yield_error
	end

	return {
		trace = trace,
		message = message,
		yielded = yielded,
	}
end

return {
	error_handler = error_handler,
	check = no_yield,
}


export type function CastIntoValue(value: type)
	local function get_return_for_fn(t: type)
		local values = t:returns()
		local head = values.head
		local tail = values.tail

		if head then
			local first = head[1]
			return first
		elseif tail then
			return tail
		else
			return types.singleton(nil)
		end
	end

	if value.tag == "union" then
		local components = value:components()
		local possible_outcomes = {}

		for _, v in components do
			if v.tag == "function" then
				table.insert(possible_outcomes, get_return_for_fn(v))
			else
				table.insert(possible_outcomes, v)
			end
		end

		return types.unionof(unpack(possible_outcomes))
	elseif value.tag == "function" then
		return get_return_for_fn(value)
	else
		return value
	end
end

local function read<T>(value: T): CastIntoValue<T>
	return if type(value) == "function" then (value :: () -> T)() else value
end

return read


local derive = require("../reactive/derive")
local read = require("./read")
local types = require("../reactive/types")
local untrack = require("../reactive/untrack")

local function show<T, U>(
	condition: () -> any,
	truthy: types.UsedAs<T>,
	falsy: types.UsedAs<U>?
): () -> T | U
	return derive(function()
		if condition() then
			return untrack(function()
				return read(truthy)
			end)
		elseif falsy then
			return untrack(function()
				return read(falsy)
			end)
		end

		return nil
	end)
end

return show :: & (<T>(
		condition: () -> any,
		truthy: types.UsedAs<T>
	) -> () -> read.CastIntoValue<T>?) & <T, U>(
		condition: () -> any,
		truthy: types.UsedAs<T>,
		falsy: types.UsedAs<U>
	) -> () -> T | U


local reat = require("./read")
local switch_delay = require("./switch_delay")
local types = require("../reactive/types")

local function show<T, U>(
	condition: () -> any,
	truthy: (active: types.Readable<boolean>) -> (T, number?),
	falsy: ((active: types.Readable<boolean>) -> (U, number?))?
): () -> { T | U }
	return switch_delay(function()
		return not not condition()
	end)({
		[true] = truthy,
		[false] = falsy :: any,
	})
end

return show :: & (<T>(
	condition: () -> any,
	truthy: types.Readable<T>
) -> () -> { reat.CastIntoValue<T> }) & <T, U>(
	condition: () -> any,
	truthy: types.Readable<T>,
	falsy: types.Readable<U>
) -> () -> { T | U }


local derive = require("../reactive/derive")
local read = require("./read")
local types = require("../reactive/types")
local untrack = require("../reactive/untrack")

local function switch<T, U>(condition: () -> T)
	return function(options: { [T]: types.UsedAs<U> }): () -> read.CastIntoValue<U>?
		return derive(function(): read.CastIntoValue<U>
			local value = condition()

			return untrack(function()
				local key = options[value]
				return read(key) :: read.CastIntoValue<U>
			end)
		end)
	end
end

return switch