--Sends messages to the local player.
--!strict

local ClientMessages = {}
ClientMessages.__index = ClientMessages

export type ClientMessages = {
    DisplayHintLoopbackEvent: BindableEvent,
    DisplayMessageLoopbackEvent: BindableEvent,
    DisplayNotificationLoopbackEvent: BindableEvent,
} & typeof(setmetatable({}, ClientMessages))

--[[
Creates a client messages instance.
--]]
function ClientMessages.new(NexusAdminRemotes: Folder): ClientMessages
    --Create the object.
    local self = setmetatable({}, ClientMessages) :: ClientMessages

    --Create the bindable objects.
    local MessageEvents = NexusAdminRemotes:WaitForChild("MessageEvents")

    local DisplayHintLoopbackEvent = Instance.new("BindableEvent")
    DisplayHintLoopbackEvent.Name = "DisplayHintLoopback"
    DisplayHintLoopbackEvent.Parent = MessageEvents
    self.DisplayHintLoopbackEvent = DisplayHintLoopbackEvent

    local DisplayMessageLoopbackEvent = Instance.new("BindableEvent")
    DisplayMessageLoopbackEvent.Name = "DisplayMessageLoopback"
    DisplayMessageLoopbackEvent.Parent = MessageEvents
    self.DisplayMessageLoopbackEvent = DisplayMessageLoopbackEvent;

    local DisplayNotificationLoopbackEvent = Instance.new("BindableEvent")
    DisplayNotificationLoopbackEvent.Name = "DisplayNotificationLoopback"
    DisplayNotificationLoopbackEvent.Parent = MessageEvents
    self.DisplayNotificationLoopbackEvent = DisplayNotificationLoopbackEvent;

    --Connect the remote events.
    (MessageEvents:WaitForChild("DisplayMessage") :: RemoteEvent).OnClientEvent:Connect(function(TopText: string, Message: string, DisplayTime: number?): ()
        self:DisplayMessage(TopText, Message, DisplayTime)
    end);
    (MessageEvents:WaitForChild("DisplayHint") :: RemoteEvent).OnClientEvent:Connect(function(Message: string, DisplayTime: number?): ()
        self:DisplayHint(Message, DisplayTime)
    end);
    (MessageEvents:WaitForChild("DisplayNotification") :: RemoteEvent).OnClientEvent:Connect(function(TopText: string, Message: string, DisplayTime: number?): ()
        self:DisplayNotification(TopText, Message, DisplayTime)
    end)

    --Return the object.
    return self
end

--[[
Sends a message to the local player.
--]]
function ClientMessages.DisplayMessage(self: ClientMessages, TopText: string, Message: string, DisplayTime: number?): ()
    self.DisplayMessageLoopbackEvent:Fire(TopText, Message, DisplayTime)
end

--[[
Sends a hint to the local player.
--]]
function ClientMessages.DisplayHint(self: ClientMessages, Message: string, DisplayTime: number?)
    self.DisplayHintLoopbackEvent:Fire(Message, DisplayTime)
end

--[[
Sends a notification to the local player.
--]]
function ClientMessages.DisplayNotification(self: ClientMessages, TopText: string, Message: string, DisplayTime: number?)
    self.DisplayNotificationLoopbackEvent:Fire(TopText, Message, DisplayTime)
end

return ClientMessages


--Registers commands on the client.
--!strict

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

local ClientRegistry = {}
ClientRegistry.__index = ClientRegistry
setmetatable(ClientRegistry, Registry)

export type ClientRegistry = {
    GetRegisteredCommands: RemoteFunction,
    Cmdr: Types.Cmdr,
} & typeof(setmetatable({}, ClientRegistry)) & Types.Registry

--[[
Creates the client registry.
--]]
function ClientRegistry.new(Authorization: Types.Authorization, Configuration: Types.Configuration, Messages: Types.MessagesClient, Cmdr: Types.Cmdr, NexusAdminRemotes: Folder): ClientRegistry
    local self = setmetatable(Registry.new(Authorization, Configuration, Messages, Cmdr, NexusAdminRemotes), ClientRegistry) :: ClientRegistry

    --Set up the events.
    local RegistryEvents = NexusAdminRemotes:WaitForChild("RegistryEvents")
    local GetRegisteredCommands = RegistryEvents:WaitForChild("GetRegisteredCommands") :: RemoteFunction
    self.GetRegisteredCommands = GetRegisteredCommands
    local CommandRegistered = RegistryEvents:WaitForChild("CommandRegistered") :: RemoteEvent
    CommandRegistered.OnClientEvent:Connect(function(Data: Types.NexusAdminCommandData): ()
        self:LoadCommand(Data)
    end)

    --Register the BeforeRun hook for verifying admin levels.
    self.Cmdr.Registry:RegisterHook("BeforeRun", function(CommandContext): string?
        --Return if a result exists from the common function.
        local BeforeRunResult = self:PerformBeforeRun(CommandContext)
        if BeforeRunResult then
            return BeforeRunResult
        end
        return nil
    end)
    return self
end

--[[
Loads a command.
--]]
function ClientRegistry.LoadCommand(self: ClientRegistry, CommandData: Types.NexusAdminCommandData): ()
    Registry.LoadCommand(self :: any, CommandData)

    --Register the command.
    local CmdrCommandData = self:GetReplicatableCmdrData(CommandData)
    local ExistingCommand = self.Cmdr.Registry.Commands[CmdrCommandData.Name]
    if (CommandData :: any).OnInvoke or CommandData.Run then
        CmdrCommandData.ClientRun = self:CreateRunMethod(CommandData)
    elseif ExistingCommand then
        CmdrCommandData.ClientRun = ExistingCommand.ClientRun
    end

    --Force arguments lost during invocation to be included on the client. Extremely shallow.
    if ExistingCommand then
        for i, Argument in ExistingCommand.Args do
            if CmdrCommandData.Args[i] == nil and typeof(Argument) == "function" then
                table.insert(CmdrCommandData.Args, i, Argument)
            end
        end
    end

    self.Cmdr.Registry:RegisterCommandObject(CmdrCommandData)
end

--[[
Loads the current commands from the server.
--]]
function ClientRegistry.LoadServerCommands(self: ClientRegistry): ()
    for _, Command in self.GetRegisteredCommands:InvokeServer() do
        self:LoadCommand(Command)
    end
end

return ClientRegistry


--Adds message UI functionality to the API.
--!strict

local MINIMUM_TEXT_SIZE = 12
local HINT_HEIGHT_RELATIVE = 0.035
local NOTIFICATION_HEIGHT_RELATIVE = 0.0225
local MAX_HINTS = 5

local GuiService = game:GetService("GuiService")
local TextService = game:GetService("TextService")
local TweenService = game:GetService("TweenService")

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

local ThemedFrame = NexusButton.ThemedFrame
local TextButtonFactory = NexusButton.TextButtonFactory

--[[
Performs a tween.
--]]
local function Tween(Ins: Instance, Tween: TweenInfo, Properties: {[string]: any}, FinishCallback: (() ->())?): ()
    task.spawn(function()
        if GuiService.ReducedMotionEnabled then
            for Name, Value in Properties do
                (Ins :: any)[Name] = Value
            end
        else
            TweenService:Create(Ins, Tween, Properties):Play()
            task.wait(Tween.Time)
        end
        if not FinishCallback then return end
        FinishCallback()
    end)
end

--[[
Adds native messages.
--]]
local function AddNativeMessages(API: Types.NexusAdminApiClient, Player: Player): ()
    local NexusAdminMessagesScreenGui = nil

    --Connect displaying messages.
    (API.EventContainer:WaitForChild("MessageEvents"):WaitForChild("DisplayMessageLoopback") :: BindableEvent).Event:Connect(function(TopText: string, Message: string, DisplayTime: number?)
        if API.FeatureFlags:GetFeatureFlag("UseNativeMessageGui") then
            TopText = TopText or ""
            Message = Message or ""
            if not DisplayTime then DisplayTime = (string.len(TopText) + string.len(Message)) * 0.05 + 0.4 end

            --Create the ScreenGui if it doesn't exist.
            if not NexusAdminMessagesScreenGui or not NexusAdminMessagesScreenGui.Parent then
                local NewNexusAdminMessagesScreenGui = Instance.new("ScreenGui")
                NewNexusAdminMessagesScreenGui.Name = "NexusAdminMessagesScreenGui"
                NewNexusAdminMessagesScreenGui.Parent = Player:FindFirstChild("PlayerGui")
                NewNexusAdminMessagesScreenGui.DisplayOrder = 50
                NewNexusAdminMessagesScreenGui.ResetOnSpawn = false
                NexusAdminMessagesScreenGui = NewNexusAdminMessagesScreenGui
            end

            --Create the text.
            local TextSize = math.max(MINIMUM_TEXT_SIZE, NexusAdminMessagesScreenGui.AbsoluteSize.Y * 0.05)
            local TextLabel = Instance.new("TextLabel")
            TextLabel.BackgroundTransparency = 0.5
            TextLabel.BorderSizePixel = 0
            TextLabel.Size = UDim2.new(1, 0, 1, 0)
            TextLabel.Position = UDim2.new(0, 0, -1, 0)
            TextLabel.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
            TextLabel.Font = Enum.Font.SourceSans
            TextLabel.TextWrapped = true
            TextLabel.Text = Message
            TextLabel.RichText = true
            TextLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
            TextLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
            TextLabel.TextStrokeTransparency = 0
            TextLabel.TextSize = TextSize
            TextLabel.Parent = NexusAdminMessagesScreenGui

            local TopTextLabel = Instance.new("TextLabel")
            TopTextLabel.BackgroundTransparency = 1
            TopTextLabel.Size = UDim2.new(1, 0, 0, TextSize)
            TopTextLabel.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
            TopTextLabel.Font = Enum.Font.SourceSansBold
            TopTextLabel.Text = TopText
            TopTextLabel.RichText = true
            TopTextLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
            TopTextLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
            TopTextLabel.TextStrokeTransparency = 0
            TopTextLabel.TextSize = TopTextLabel.AbsoluteSize.Y
            TopTextLabel.Parent = TextLabel

            --Connect the event for resizing.
            local Event = NexusAdminMessagesScreenGui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
                TextSize = math.max(MINIMUM_TEXT_SIZE, NexusAdminMessagesScreenGui.AbsoluteSize.Y * 0.05)
                TextLabel.TextSize = TextSize
                TopTextLabel.Size = UDim2.new(1, 0, 0, TextSize)
                TopTextLabel.TextSize = TextSize
            end)

            --Tween the frame in and out.
            Tween(TextLabel, TweenInfo.new(0.5, Enum.EasingStyle.Quad), {
                Position = UDim2.new(0, 0, 0, 0),
            }, function()
                task.wait(DisplayTime)
                Tween(TextLabel, TweenInfo.new(0.5, Enum.EasingStyle.Quad), {
                    Position = UDim2.new(0, 0, 1, 0),
                }, function()
                    TextLabel:Destroy()
                    Event:Disconnect()
                end)
            end)
        end
    end)
end

--[[
Adds native hints.
--]]
local function AddNativeHints(API: Types.NexusAdminApiClient, Player: Player): ()
    local HintHeight = MINIMUM_TEXT_SIZE
    local HintsQueue = {} :: {{Frame: TextLabel, Duration: number}}
    local NexusAdminHintsScreenGui = nil

    --[[
    Updates the position of the hints.
    --]]
    local function UpdateHintPositions()
        for i, HintData in HintsQueue do
            Tween(HintData.Frame, TweenInfo.new(0.25, Enum.EasingStyle.Quad), {
                Position = UDim2.new(0, 0, 0, (i - 1) * HintHeight)
            })
        end
    end

    --[[
    Removes a hint.
    --]]
    local function RemoveHint(HintData: {Frame: TextLabel, Duration: number}): ()
        --Determine the index of the hint and return if it doesn't exist.
        local Index
        for i, OtherHintData in HintsQueue do
            if HintData == OtherHintData then
                Index = i
                break
            end
        end
        if not Index then return end

        --Remove the hint and update the other hint positions.
        table.remove(HintsQueue,Index)
        Tween(HintData.Frame, TweenInfo.new(0.25, Enum.EasingStyle.Quad), {
            Position = UDim2.new(1, 0, 0, (Index - 1) * HintHeight),
        }, function()
            HintData.Frame:Destroy()
        end)
        UpdateHintPositions()
    end

    --[[
    Inserts a hint into the queue.
    --]]
    local function InsertHint(Frame: TextLabel, Duration: number): ()
        --Insert the hint and update the positions.
        local HintData = {Frame = Frame,Duration=Duration}
        table.insert(HintsQueue, 1, HintData)
        Frame.Parent = NexusAdminHintsScreenGui
        UpdateHintPositions()

        --Remove the overflow hints if they exist.
        local OverflowHint = HintsQueue[MAX_HINTS + 1]
        if OverflowHint then
            HintsQueue[MAX_HINTS + 1] = nil
            Tween(OverflowHint.Frame, TweenInfo.new(0.25, Enum.EasingStyle.Quad), {
                Position = UDim2.new(0, 0, 0, MAX_HINTS * HintHeight),
            }, function()
                Tween(OverflowHint.Frame, TweenInfo.new(0.25, Enum.EasingStyle.Quad), {
                    Position = UDim2.new(1, 0, 0, MAX_HINTS * HintHeight),
                }, function()
                    OverflowHint.Frame:Destroy()
                end)
            end)
        end

        --Remove the notification after the duration.
        task.wait(Duration)
        RemoveHint(HintData)
    end

    --Connect displaying hints.
    (API.EventContainer:WaitForChild("MessageEvents"):WaitForChild("DisplayHintLoopback") :: BindableEvent).Event:Connect(function(Message: string, DisplayTime: number?)
        if API.FeatureFlags:GetFeatureFlag("UseNativeHintGui") then
            Message = Message or ""

            --Create the ScreenGui if it doesn't exist.
            if not NexusAdminHintsScreenGui or not NexusAdminHintsScreenGui.Parent then
                local NewNexusAdminHintsScreenGui = Instance.new("ScreenGui")
                NewNexusAdminHintsScreenGui.Name = "NexusAdminHintsScreenGui"
                NewNexusAdminHintsScreenGui.Parent = Player:FindFirstChild("PlayerGui")
                NewNexusAdminHintsScreenGui.ResetOnSpawn = false
                NewNexusAdminHintsScreenGui.DisplayOrder = 50
                NexusAdminHintsScreenGui = NewNexusAdminHintsScreenGui

                HintHeight = math.max(MINIMUM_TEXT_SIZE, HINT_HEIGHT_RELATIVE * NewNexusAdminHintsScreenGui.AbsoluteSize.Y)
                NewNexusAdminHintsScreenGui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
                    HintHeight = math.max(MINIMUM_TEXT_SIZE, HINT_HEIGHT_RELATIVE * NewNexusAdminHintsScreenGui.AbsoluteSize.Y)
                    for _, HintData in HintsQueue do
                        HintData.Frame.Size = UDim2.new(1, 0, 0, HintHeight)
                    end
                    UpdateHintPositions()
                end)
            end

            --Create the hint frame.
            local HintFrame = Instance.new("TextLabel")
            HintFrame.BackgroundTransparency = 0.5 * GuiService.PreferredTransparency
            HintFrame.BorderSizePixel = 0
            HintFrame.BackgroundColor3 = Color3.fromRGB(0,0,0)
            HintFrame.Position = UDim2.new(0, 0, 0, -HintHeight)
            HintFrame.Size = UDim2.new(1, 0, 0, HintHeight)
            HintFrame.Font = Enum.Font.SourceSans
            HintFrame.TextScaled = true
            HintFrame.TextColor3 = Color3.fromRGB(255, 255, 255)
            HintFrame.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
            HintFrame.Text = Message
            HintFrame.RichText = true
            HintFrame.TextStrokeTransparency = 0

            --Add the hint.
            InsertHint(HintFrame, DisplayTime or string.len(Message) * 0.05 + 0.4)
        end
    end)
end

--[[
Adds native hints.
--]]
local function AddNativeNotifications(API: Types.NexusAdminApiClient, Player: Player): ()
    local CurrentNotifications = {} :: {{Frame: Frame, Height: number}}
    local CloseFactory = TextButtonFactory.CreateDefault(Color3.fromRGB(200, 0, 0))
    CloseFactory:SetTextDefault("Text", "X")
    CloseFactory:SetTextDefault("Font", Enum.Font.SciFi)
    CloseFactory:SetTextDefault("TextStrokeTransparency", 1)

    --[[
    Updates the position of the notifications.
    --]]
    local function UpdateNotificationPositions(): ()
        local CurrentHeightOffset = 0
        for _, Notification in CurrentNotifications do
            Tween(Notification.Frame, TweenInfo.new(0.25, Enum.EasingStyle.Quad), {
                Position = UDim2.new(1, -5, 0.95, -CurrentHeightOffset),
            })
            CurrentHeightOffset += Notification.Height + 10
        end
    end

    --Connect displaying notitifcations.
    local NexusAdminNotificationsScreenGui
    (API.EventContainer:WaitForChild("MessageEvents"):WaitForChild("DisplayNotificationLoopback") :: BindableEvent).Event:Connect(function(TopText: string, Message: string, DisplayTime: number?)
        if API.FeatureFlags:GetFeatureFlag("UseNativeHintGui") then
            TopText = TopText or ""
            Message = Message or ""

            --Create the ScreenGui if it doesn't exist.
            if not NexusAdminNotificationsScreenGui or not NexusAdminNotificationsScreenGui.Parent then
                local NewNexusAdminNotificationsScreenGui = Instance.new("ScreenGui")
                NewNexusAdminNotificationsScreenGui.Name = "NexusAdminNotificationsScreenGui"
                NewNexusAdminNotificationsScreenGui.Parent = Player:FindFirstChild("PlayerGui")
                NewNexusAdminNotificationsScreenGui.ResetOnSpawn = false
                NewNexusAdminNotificationsScreenGui.DisplayOrder = 50
                NexusAdminNotificationsScreenGui = NewNexusAdminNotificationsScreenGui
            end

            --Create the notification frame.
            local ViewportSizeY = NexusAdminNotificationsScreenGui.AbsoluteSize.Y
            local TextSize = math.max(MINIMUM_TEXT_SIZE, NOTIFICATION_HEIGHT_RELATIVE * NexusAdminNotificationsScreenGui.AbsoluteSize.Y)
            local NotificationWidth = (0.4 / NOTIFICATION_HEIGHT_RELATIVE) * TextSize
            local MessageHeight = TextService:GetTextSize(Message, TextSize, Enum.Font.SourceSans, Vector2.new(NotificationWidth - 10, ViewportSizeY)).Y
            local NotificationHeight = TextSize + MessageHeight
            local NotificationFrame = Instance.new("Frame")
            NotificationFrame.BackgroundTransparency = 1
            NotificationFrame.AnchorPoint = Vector2.new(1, 1)
            NotificationFrame.Position = UDim2.new(1, NotificationWidth, 0.95, 0)
            NotificationFrame.Size = UDim2.new(0, NotificationWidth, 0, NotificationHeight)
            NotificationFrame.Parent = NexusAdminNotificationsScreenGui

            local Background = ThemedFrame.new()
            Background.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
            Background.BackgroundTransparency = 0.5 * GuiService.PreferredTransparency
            Background.Size = UDim2.new(1, 0, 1, 0)
            Background.SliceScaleMultiplier = 0.3
            Background.Parent = NotificationFrame

            local TopTextLabel = Instance.new("TextLabel")
            TopTextLabel.BackgroundTransparency = 1
            TopTextLabel.Size = UDim2.new(1, -(2 * TextSize), 0, TextSize)
            TopTextLabel.Position = UDim2.new(0, 5, 0, 0)
            TopTextLabel.Font = Enum.Font.SourceSansBold
            TopTextLabel.TextSize = TextSize
            TopTextLabel.Text = TopText
            TopTextLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
            TopTextLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
            TopTextLabel.TextStrokeTransparency = 0
            TopTextLabel.TextTruncate = Enum.TextTruncate.AtEnd
            TopTextLabel.TextXAlignment = Enum.TextXAlignment.Left
            TopTextLabel.Parent = NotificationFrame

            local MessageLabel = Instance.new("TextLabel")
            MessageLabel.BackgroundTransparency = 1
            MessageLabel.Size = UDim2.new(1, -10, 0, MessageHeight)
            MessageLabel.Position = UDim2.new(0, 5, 0, TextSize)
            MessageLabel.Font = Enum.Font.SourceSans
            MessageLabel.TextWrapped = true
            MessageLabel.TextSize = TextSize
            MessageLabel.Text = Message
            MessageLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
            MessageLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
            MessageLabel.TextStrokeTransparency = 0
            MessageLabel.TextXAlignment = Enum.TextXAlignment.Left
            MessageLabel.Parent = NotificationFrame

            local CloseButton, _ = CloseFactory:Create()
            CloseButton.Size = UDim2.new(0, 0.8 * TextSize, 0, 0.8 * TextSize)
            CloseButton.Position = UDim2.new(1, -0.9 * TextSize, 0, 0.1 * TextSize)
            CloseButton.Parent = NotificationFrame

            --Create the notification object.
            local Notification = {
                Frame = NotificationFrame,
                Height = NotificationHeight,
            }
            table.insert(CurrentNotifications, 1, Notification)
            UpdateNotificationPositions()

            --[[
            Closes the notification.
            --]]
            function Notification:Close(): ()
                --Remove the notification.
                local Removed = false
                for i = #CurrentNotifications, 1, -1 do
                    if CurrentNotifications[i] ~= Notification then continue end
                    table.remove(CurrentNotifications, i)
                    Removed = true
                end
                if not Removed then return end

                --Hide the notification.
                Tween(NotificationFrame, TweenInfo.new(0.25, Enum.EasingStyle.Quad), {
                    Position = UDim2.new(1, NotificationWidth, 0, NotificationFrame.AbsolutePosition.Y + NotificationHeight),
                }, function()
                    NotificationFrame:Destroy()
                end)
                UpdateNotificationPositions()
            end

            --Connect closing.
            CloseButton.MouseButton1Down:Connect(function()
                Notification:Close()
            end)

            --Automatically close the notification.
            if DisplayTime ~= nil then
                local CloseTimeLabel = Instance.new("TextLabel")
                CloseTimeLabel.BackgroundTransparency = 1
                CloseTimeLabel.Size = UDim2.new(0, 2 * TextSize, 0, 0.6 * TextSize)
                CloseTimeLabel.Position = UDim2.new(1, -(3.1 * TextSize), 0, TextSize * 0.2)
                CloseTimeLabel.Font = Enum.Font.SourceSans
                CloseTimeLabel.TextSize = TextSize
                CloseTimeLabel.Text = ""
                CloseTimeLabel.TextColor3 = Color3.fromRGB(200, 200, 200)
                CloseTimeLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
                CloseTimeLabel.TextStrokeTransparency = 0
                CloseTimeLabel.TextXAlignment = Enum.TextXAlignment.Right
                CloseTimeLabel.Parent = NotificationFrame

                for i = DisplayTime, 1, -1 do
                    CloseTimeLabel.Text = tostring(i)
                    task.wait(1)
                end
                CloseTimeLabel.Text = "0"
                Notification:Close()
            end
        end
    end)
end

--[[
Adds notifications to the local admin level being changed.
--]]
local function AddAdminLevelNotifications(API: Types.NexusAdminApiClient, Player: Player): ()
    local WasAuthorized = false

    --[[
    Displays a message for the admin level changing.
    --]]
    local function AdminLevelChanged()
        local IsAuthorized = API.Authorization:IsPlayerAuthorized(Player,1)
        if API.FeatureFlags:GetFeatureFlag("DisplayAdminLevelNotifications") and WasAuthorized ~= IsAuthorized then
            WasAuthorized = IsAuthorized

            --Display a message.
            if IsAuthorized then
                API.Messages:DisplayNotification("Nexus Admin", `You are an admin. Use {API.Configuration.CommandPrefix}cmds or !cmds to view the commands. Use !usage to view using the system.`, 10)
            else
                API.Messages:DisplayNotification("Nexus Admin", "Your admin level has changed and you no longer are an admin.", 5)
            end
        end
    end

    --Initialize the admin level.
    API.Authorization.AdminLevelChanged:Connect(function(OtherPlayer)
        if Player == OtherPlayer then
            AdminLevelChanged()
        end
    end)
    AdminLevelChanged()
end

return function(API: Types.NexusAdminApiClient, Player: Player)
    AddNativeMessages(API, Player)
    AddNativeHints(API, Player)
    AddNativeNotifications(API, Player)
    AddAdminLevelNotifications(API, Player)
end


--Adds the native tooltip API.
--!strict

local TOOLTIP_SIZE_RELATIVE = 0.03

local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local TextService = game:GetService("TextService")

local NexusButton = require(script.Parent.Parent:WaitForChild("Packages"):WaitForChild("NexusButton")) :: any

local ThemedFrame = NexusButton.ThemedFrame

return function(API, Player)
    local TooptipContainer

    --[[
    Adds a tooltip to a frame.
    --]]
    function API.Gui:AddTooltipToFrame(Text, Frame)
        local Bounds = TextService:GetTextSize(Text, 18, "SourceSans", Vector2.new(4096, 4096))
        local BoundsXRelative,BoundsYRelative = Bounds.X / 18, Bounds.Y / 18

        --Create the ScreenGui.
        if not TooptipContainer or not TooptipContainer.Parent then
            local NewTooptipContainer = Instance.new("ScreenGui")
            NewTooptipContainer.Name = "NexusAdminTooltips"
            NewTooptipContainer.Parent = Player:FindFirstChild("PlayerGui")
            NewTooptipContainer.ResetOnSpawn = false
            TooptipContainer = NewTooptipContainer
        end

        --Create the tooltip GUI.
        local AdornFrame = Instance.new("Frame")
        AdornFrame.BackgroundTransparency = 1
        AdornFrame.Visible = false
        AdornFrame.ZIndex = 10
        AdornFrame.Parent = TooptipContainer

        local Background = ThemedFrame.new()
        Background.Size = UDim2.new(1, 0, 1, 0)
        Background.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
        Background.BackgroundTransparency = 0.75 * GuiService.PreferredTransparency
        Background.Parent = AdornFrame

        local TextLabel = Instance.new("TextLabel")
        TextLabel.BackgroundTransparency = 1
        TextLabel.ZIndex = 12
        TextLabel.Size = UDim2.new(1, -12, 1, -12)
        TextLabel.Position = UDim2.new(0, 6, 0, 6)
        TextLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
        TextLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
        TextLabel.TextStrokeTransparency = 0
        TextLabel.Font = Enum.Font.SourceSans
        TextLabel.TextXAlignment = Enum.TextXAlignment.Left
        TextLabel.Text = Text
        TextLabel.Parent = AdornFrame

        --[[
        Returns if the given position is in a frame.
        --]]
        local function MouseInFrame(Frame: GuiObject, X: number, Y: number): boolean
            local PosX,PosY = Frame.AbsolutePosition.X, Frame.AbsolutePosition.Y
            local SizeX,SizeY = Frame.AbsoluteSize.X, Frame.AbsoluteSize.Y
            return (X > PosX and Y > PosY and X < PosX + SizeX and Y < PosY + SizeY)
        end

        --[[
        Returns if the parent is visible.
        --]]
        local function ParentsVisible(Frame, X, Y)
            local FrameParent = Frame.Parent
            if FrameParent and FrameParent:IsA("GuiObject") then
                if not FrameParent.Visible then
                    return false
                end

                if FrameParent.ClipsDescendants and not MouseInFrame(FrameParent :: any, X, Y) then
                    return false
                end

                return ParentsVisible(FrameParent, X, Y)
            end
            return true
        end

        local Events = {} :: {RBXScriptConnection}
        local function Update(X: number | boolean, Y: number?)
            --If the frame was unparented, disconnect the events.
            if not Frame.Parent then
                for _, Event in Events do
                    Event:Disconnect()
                end
                Events = {}
                AdornFrame:Destroy()
                return
            end

            --Update the frame.
            if X ~= true and MouseInFrame(Frame, X :: number, Y :: number) and ParentsVisible(Frame :: any, X :: number, Y :: number) then
                AdornFrame.Visible = true
                AdornFrame.Position = UDim2.new(0, (X :: number) + 20, 0, Y)

                local SizeY = (AdornFrame.Parent :: any).AbsoluteSize.Y * TOOLTIP_SIZE_RELATIVE
                AdornFrame.Size = UDim2.new(0,(SizeY * BoundsXRelative) + 12,0,(SizeY * BoundsYRelative) + 12)
                TextLabel.TextSize = SizeY
            else
                AdornFrame.Visible = false
            end
        end

        --Connect the mouse moving.
        table.insert(Events, UserInputService.InputChanged:Connect(function(Key)
            if Key.UserInputType == Enum.UserInputType.MouseMovement then
                Update(Key.Position.X, Key.Position.Y)
            end
        end))

        --Connect the mouse leaving the frame.
        table.insert(Events, Frame.MouseLeave:Connect(function(X, Y)
            Update(true)
        end))

        --Connect changing the background.
        table.insert(Events, GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
            Background.BackgroundTransparency = 0.75 * GuiService.PreferredTransparency
        end))
    end
end


--Standard scrolling text list implementation.
--!strict

local TextService = game:GetService("TextService")

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

local ApiTextList = {}
ApiTextList.__index = ApiTextList

export type ApiTextList = {
    TextSize: number,
    MaxLineWidth: number,
    CurrentLines: {{[string]: any}},
    CurrentSearchTerm: string,
    Filter: ((SearchTerm: string, Lines: {{[string]: any}}) -> ({{[string]: any}}))?,
    Instances: {
        AdornFrame: Frame,
        ScrollingFrame: ScrollingFrame,
        SearchBar: TextBox?,
        VirtualList: any, --NexusVirtualList is not in the correct place for the client.
    },
    EventConnections: {any},
} & typeof(setmetatable({}, ApiTextList))

--[[
Creats an API text list.
--]]
function ApiTextList.new(Parent: Instance, TextSize: number, TextType: ("TextLabel" | "TextBox")?): ApiTextList
    --Create the frames.
    local AdornFrame = Instance.new("Frame")
    AdornFrame.BackgroundTransparency = 1
    AdornFrame.Size = UDim2.new(1, 5, 1, 0)
    AdornFrame.Parent = Parent

    local ScrollingFrame = Instance.new("ScrollingFrame")
    ScrollingFrame.BackgroundTransparency = 1
    ScrollingFrame.BorderSizePixel = 0
    ScrollingFrame.Size = UDim2.new(1, -10, 1, 0)
    ScrollingFrame.ScrollBarThickness = math.min(math.ceil(0.8 * TextSize), 10)
    ScrollingFrame.Parent = AdornFrame

    local VirtualList = NexusVirtualList.CreateVirtualScrollList(ScrollingFrame, CreateScrollingTextEntryCreator(TextSize, TextType or "TextBox"))
    VirtualList:SetEntryHeight(TextSize)

    --Create and return the object.
    return setmetatable({
        TextSize = TextSize,
        MaxLineWidth = 0,
        CurrentLines = {},
        CurrentSearchTerm = "",
        Instances = {
            AdornFrame = AdornFrame,
            ScrollingFrame = ScrollingFrame,
            VirtualList = VirtualList,
        },
        EventConnections = {},
    }, ApiTextList) :: ApiTextList
end

--[[
Enables searching in the text list.
--]]
function ApiTextList.EnableSearching(self: ApiTextList, Filter: ((SearchTerm: string, Lines: {{[string]: any}}) -> ({{[string]: any}}))?): ()
    --Resize the scrolling frame.
    local TextSize = self.TextSize
    local ScrollingFrame = self.Instances.ScrollingFrame
    ScrollingFrame.Size = UDim2.new(1, -10, 1, -10 - (1.3 * TextSize))
    ScrollingFrame.Position = UDim2.new(0, 5, 0, 5 + (1.3 * TextSize))

    --Create the search box.
    local SearchIcon = Instance.new("ImageLabel")
    SearchIcon.Size = UDim2.new(0, TextSize, 0, TextSize)
    SearchIcon.Position = UDim2.new(0, 0.25 * TextSize, 0, 0.25 * TextSize)
    SearchIcon.BackgroundTransparency = 1
    SearchIcon.Image = "rbxasset://textures/DevConsole/Search.png"
    SearchIcon.Parent = self.Instances.AdornFrame

    local SearchBar = Instance.new("TextBox")
    SearchBar.BackgroundTransparency = 0.5
    SearchBar.Size = UDim2.new(1, -2 * TextSize, 0, 1.2 * TextSize)
    SearchBar.Position = UDim2.new(0, 1.5 * TextSize, 0, 0.15 * TextSize)
    SearchBar.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
    SearchBar.BorderSizePixel = 0
    SearchBar.Text = ""
    SearchBar.Font = Enum.Font.SourceSans
    SearchBar.TextSize = TextSize
    SearchBar.TextColor3 = Color3.fromRGB(255, 255, 255)
    SearchBar.ClearTextOnFocus = false
    SearchBar.ClipsDescendants = true
    SearchBar.TextXAlignment = Enum.TextXAlignment.Left
    SearchBar.Parent = self.Instances.AdornFrame
    self.Instances.SearchBar = SearchBar

    --Store the filter.
    self.Filter = Filter or function(SearchTerm, Lines)
        --Return the original lines if the search term is empty.
        if SearchTerm == "" then
            return Lines
        end

        --Filter the lines.
        local NewLines = {}
        for _, Line in Lines do
            local Text = Line.Text
            if not Text then continue end
            if not string.find(string.lower(Text), string.lower(SearchTerm)) then continue end
            table.insert(NewLines, Line)
        end
        return NewLines
    end

    SearchBar:GetPropertyChangedSignal("Text"):Connect(function()
        self.CurrentSearchTerm = SearchBar.Text
        self:UpdateDisplayedLines()
    end)
end

--[[
Moves the API text list.
--]]
function ApiTextList.Move(self: ApiTextList, Size: UDim2?, Position: UDim2?, AnchorPoint: Vector2?): ()
    local AdornFrame = self.Instances.AdornFrame
    AdornFrame.Size = Size or AdornFrame.Size
    AdornFrame.Position = Position or AdornFrame.Position
    AdornFrame.AnchorPoint = AnchorPoint or AdornFrame.AnchorPoint
end

--[[
Updates the displayed lines.
--]]
function ApiTextList.UpdateDisplayedLines(self: ApiTextList): ()
    --Filter the lines.
    local Lines = self.CurrentLines
    if self.Filter then
        Lines = self.Filter(self.CurrentSearchTerm, Lines)
    end

    --Split the lines.
    local SplitLines = {}
    for _, Line in Lines do
        for _, SubLine in string.split(Line.Text or "", "\n") do
            local LineData = {}
            for Key, Value in Line do
                LineData[Key] = Value
            end
            LineData.Text = SubLine
            table.insert(SplitLines, LineData)
        end
    end

    --Show the lines.
    self.Instances.VirtualList:SetData(SplitLines)
end

--[[
Adds lines to the window.
--]]
function ApiTextList.AddLines(self: ApiTextList, Lines: {string | {[string]: any}}, Order: ("NewestFirst" | "OldestFirst")?): ()
    --Convert the lines.
    local NewLines = {} :: {{[string]: any}}
    for _, Line in Lines do
        --Convert the line.
        if typeof(Line) == "string" then
            Line = {
                Text = Line,
            }
        end

        --Add the line.
        local CurrentLine = Line :: {[string]: any}
        table.insert(NewLines, CurrentLine)
        if Order == "NewestFirst" then
            table.insert(self.CurrentLines, 1, CurrentLine)
        else
            table.insert(self.CurrentLines, CurrentLine)
        end
    end

    --Update the max width of the lines.
    for _, Line in NewLines do
        local Text = Line.Text
        local Font = Line.Font or Enum.Font.SourceSans
        local LineLength = TextService:GetTextSize(Text, self.TextSize, Font, Vector2.new(math.huge, self.TextSize)).X
        self.MaxLineWidth = math.max(self.MaxLineWidth, LineLength)
    end

    --Set the lines.
    self.Instances.VirtualList:SetScrollWidth(UDim.new(0, self.MaxLineWidth + 5))
    self:UpdateDisplayedLines()
end

--[[
Sets the lines of the window.
--]]
function ApiTextList.SetLines(self: ApiTextList, Lines: {string | {[string]: any}}): ()
    self.CurrentLines = {}
    self.MaxLineWidth = 0
    self:AddLines(Lines)
end

--[[
Displays logs and automatically updates when new entries are added.
--]]
function ApiTextList.DisplayLogs(self: ApiTextList, Logs: Types.Logs, Order: ("NewestFirst" | "OldestFirst")?): ()
    --Add the initial messages.
    local LogEntries = Logs:GetLogs()
    if Order == "NewestFirst" then
        local NewLogs = {}
        for i = #LogEntries, 1, -1 do
            table.insert(NewLogs, LogEntries[i])
        end
        LogEntries = NewLogs
    end
    self:AddLines(LogEntries, Order)

    --Connect logs being added.
    table.insert(self.EventConnections, Logs.LogAdded:Connect(function(LogEntry)
        self:AddLines({LogEntry}, Order)
    end))
end

--[[
Destroys the API text list.
--]]
function ApiTextList.Destroy(self: ApiTextList): ()
    for _, EventConnection in self.EventConnections do
        EventConnection:Disconnect()
    end
    self.Instances.AdornFrame:Destroy()
    self.Instances.VirtualList:Destroy()
end

return ApiTextList


--Standard window used with the Window API.
--!strict

local MINIMUM_TEXT_SIZE = 12

local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")

local NexusButton = require(script.Parent.Parent.Parent:WaitForChild("Packages"):WaitForChild("NexusButton"))
local ApiTextList = require(script.Parent:WaitForChild("ApiTextList"))

local ButtonFactory = NexusButton.ButtonFactory
local TextButtonFactory = NexusButton.TextButtonFactory
local ThemedFrame = NexusButton.ThemedFrame

local RefreshFactory = ButtonFactory.CreateDefault(Color3.fromRGB(0, 200, 0))
local CloseFactory = TextButtonFactory.CreateDefault(Color3.fromRGB(200, 0, 0))
CloseFactory:SetTextDefault("Text", "X")
CloseFactory:SetTextDefault("Font", Enum.Font.SciFi)
CloseFactory:SetTextDefault("TextStrokeTransparency", 1)

local ApiWindow = {}
ApiWindow.__index = ApiWindow

export type ApiWindow = {
    IsOpen: boolean,
    InitialViewportSize: Vector2,
    InitialHeight: number,
    InitialWidth: number,
    TextHeight: number,
    StartSide: Enum.NormalId,
    ContentsAdorn: Frame,
    Instances: {
        ScreenGui: ScreenGui,
        WindowFrame: Frame,
        TopBarAdorn: TextButton,
        BackgroundAdorn: Frame,
        ContentsAdorn: Frame,
        TitleText: TextLabel,
        Background: any, --ThemedFrame is not in the correct place for the client.
        TopBarBackground: any, --ThemedFrame is not in the correct place for the client.
        ResizeCorner: any?, --ThemedFrame is not in the correct place for the client.
        RefreshButton: any?, --NexusButton is not in the correct place for the client.
        CloseButton: any?, --NexusButton is not in the correct place for the client.
        TextLists: {ApiTextList.ApiTextList},
    },
    EventConnections: {RBXScriptConnection},
} & typeof(setmetatable({}, ApiWindow))

--[[
Creates an API Window.
--]]
function ApiWindow.new(): ApiWindow
    --Create the base window.
    local Camera = Workspace.CurrentCamera
    local WindowHeight = math.max(math.ceil(0.5 * Camera.ViewportSize.Y), 5 * MINIMUM_TEXT_SIZE)
    local TopBarHeightPixel = math.ceil(WindowHeight * 0.1)
    local CornerSizePixel = math.floor(TopBarHeightPixel * 0.4)

    local WindowScreenGui = Instance.new("ScreenGui")
    WindowScreenGui.Name = "NexusAdminWindow"
    WindowScreenGui.ResetOnSpawn = false
    WindowScreenGui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")

    local WindowFrame = Instance.new("Frame")
    WindowFrame.BackgroundTransparency = 1
    WindowFrame.Parent = WindowScreenGui

    local TopBarAdorn = Instance.new("TextButton")
    TopBarAdorn.Size = UDim2.new(1, 0, 0, TopBarHeightPixel)
    TopBarAdorn.BackgroundTransparency = 1
    TopBarAdorn.Text = ""
    TopBarAdorn.Parent = WindowFrame

    local BackgroundAdorn = Instance.new("Frame")
    BackgroundAdorn.Position = UDim2.new(0, 0, 0, TopBarHeightPixel)
    BackgroundAdorn.Size = UDim2.new(1, 0, 1, -TopBarHeightPixel)
    BackgroundAdorn.BackgroundTransparency = 1
    BackgroundAdorn.Parent = WindowFrame

    local ContentsAdorn = Instance.new("Frame")
    ContentsAdorn.Position = UDim2.new(0,0,0,TopBarHeightPixel)
    ContentsAdorn.Size = UDim2.new(1, 0, 1, -TopBarHeightPixel - CornerSizePixel)
    ContentsAdorn.BackgroundTransparency = 1
    ContentsAdorn.Parent = WindowFrame

    --Create the backgrounds.
    local Background = ThemedFrame.new()
    Background.BackgroundColor3 = Color3.fromRGB(0,0,0)
    Background.BackgroundTransparency = 0.75 * GuiService.PreferredTransparency
    Background.Size = UDim2.new(1,0,1,0)
    Background.Theme = "CutBottomRightCorner"
    Background.Parent = BackgroundAdorn

    BackgroundAdorn:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
        Background.SliceScaleMultiplier = 1.5 * (TopBarAdorn.AbsoluteSize.Y / BackgroundAdorn.AbsoluteSize.Y)
    end)

    local TopBarBackground = ThemedFrame.new()
    TopBarBackground.BackgroundColor3 = Color3.fromRGB(0, 170, 255)
    TopBarBackground.BackgroundTransparency = 0.5 * GuiService.PreferredTransparency
    TopBarBackground.Size = UDim2.new(1,0,1,0)
    TopBarBackground.Theme = "CutTopLeftCorner"
    TopBarBackground.SliceScaleMultiplier = 1.5
    TopBarBackground.Parent = TopBarAdorn

    --Create the top bar text.
    local TitleText = Instance.new("TextLabel")
    TitleText.BackgroundTransparency = 1
    TitleText.Size = UDim2.new(1, -(TopBarHeightPixel * 0.4), 1, -2 * (TopBarHeightPixel * 0.1))
    TitleText.Position = UDim2.new(0, 4 * (TopBarHeightPixel * 0.1), 0, TopBarHeightPixel * 0.1)
    TitleText.ClipsDescendants = true
    TitleText.Text = "Window"
    TitleText.Font = Enum.Font.SciFi
    TitleText.TextColor3 = Color3.fromRGB(255, 255, 255)
    TitleText.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
    TitleText.TextStrokeTransparency = 0
    TitleText.TextXAlignment = Enum.TextXAlignment.Left
    TitleText.TextSize = TopBarHeightPixel * (36 / 44)
    TitleText.ZIndex = 3
    TitleText.Parent = TopBarAdorn

    --Create the object.
    local self = setmetatable({
        IsOpen = false,
        InitialViewportSize = Camera.ViewportSize,
        InitialHeight = WindowHeight,
        InitialWidth = math.ceil(0.375 * Camera.ViewportSize.Y),
        TextHeight = math.max(MINIMUM_TEXT_SIZE, math.ceil(Camera.ViewportSize.Y * 0.5 * 0.045)),
        StartSide = Enum.NormalId.Left,
        ContentsAdorn = ContentsAdorn,
        Instances = {
            ScreenGui = WindowScreenGui,
            WindowFrame = WindowFrame,
            TopBarAdorn = TopBarAdorn,
            BackgroundAdorn = BackgroundAdorn,
            ContentsAdorn = ContentsAdorn,
            TitleText = TitleText,
            Background = Background,
            TopBarBackground = TopBarBackground,
            TextLists = {},
        },
        EventConnections = {},
    }, ApiWindow) :: ApiWindow

    --Connect the drag events.
    local MouseDown = false
    local LastPosition = nil
    table.insert(self.EventConnections, TopBarAdorn.MouseButton1Down:Connect(function()
        MouseDown = true
        LastPosition = UserInputService:GetMouseLocation()
    end))

    table.insert(self.EventConnections, UserInputService.InputChanged:Connect(function(Input)
        if not LastPosition then return end
        if not MouseDown then return end
        if Input.UserInputType ~= Enum.UserInputType.MouseMovement and Input.UserInputType ~= Enum.UserInputType.Touch then return end

        local MousePosition = UserInputService:GetMouseLocation()
        local Delta = MousePosition - LastPosition
        LastPosition =  MousePosition
        WindowFrame.Position = UDim2.new(0, WindowFrame.Position.X.Offset + Delta.X, 0, WindowFrame.Position.Y.Offset + Delta.Y)
    end))

    table.insert(self.EventConnections, UserInputService.InputEnded:Connect(function(Input)
        if Input.UserInputType ~= Enum.UserInputType.MouseButton1 and Input.UserInputType ~= Enum.UserInputType.Touch then return end

        MouseDown = false
        LastPosition = nil
    end))

    --Connect the transparency changing.
    table.insert(self.EventConnections, GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
        Background.BackgroundTransparency = 0.75 * GuiService.PreferredTransparency
        TopBarBackground.BackgroundTransparency = 0.5 * GuiService.PreferredTransparency
    end))

    --Return the object.
    return self
end

--[[
Updates the top bar.
--]]
function ApiWindow.UpdateTopBar(self: ApiWindow): ()
    --Move the refresh button.
    local TopBarHeightPixel = self.Instances.TopBarAdorn.Size.Y.Offset
    if self.Instances.RefreshButton then
        if self.Instances.CloseButton then
            self.Instances.RefreshButton.Position = UDim2.new(1, -TopBarHeightPixel * 1.8, 0, TopBarHeightPixel * 0.075)
        else
            self.Instances.RefreshButton.Position = UDim2.new(1, -TopBarHeightPixel * 0.9, 0, TopBarHeightPixel * 0.075)
        end
    end

    --Update the title text width.
    if self.Instances.RefreshButton and self.Instances.CloseButton then
        self.Instances.TitleText.Size = UDim2.new(1, -(TopBarHeightPixel * 2.4), 1, -2 * (TopBarHeightPixel * 0.1))
    elseif self.Instances.RefreshButton or self.Instances.CloseButton then
        self.Instances.TitleText.Size = UDim2.new(1, -(TopBarHeightPixel * 1.4), 1, -2 * (TopBarHeightPixel * 0.1))
    else
        self.Instances.TitleText.Size = UDim2.new(1, -(TopBarHeightPixel * 0.4), 1, -2 * (TopBarHeightPixel * 0.1))
    end
end

--[[
Allows the window to be resized.
--]]
function ApiWindow.EnableResizing(self: ApiWindow, MinWidth: number?, MinHeight: number?, MaxWidth: number?, MaxHeight: number?): ()
    --Create the resize corner.
    local WindowHeight = self.InitialViewportSize.Y * 0.5
    local TopBarHeightPixel = WindowHeight * 0.1
    local CornerSizePixel = TopBarHeightPixel * 0.4
    local ResizeCornerSizePixel = CornerSizePixel * 1.5

    MinWidth = MinWidth or WindowHeight * 0.6
    MinHeight = MinHeight or TopBarHeightPixel * 2
    MaxWidth = MaxWidth or math.huge
    MaxHeight = MaxHeight or math.huge

    local ResizeCornerAdorn = Instance.new("TextButton")
    ResizeCornerAdorn.Size = UDim2.new(0, ResizeCornerSizePixel, 0, ResizeCornerSizePixel)
    ResizeCornerAdorn.Position = UDim2.new(1, -ResizeCornerSizePixel, 1, -ResizeCornerSizePixel)
    ResizeCornerAdorn.BackgroundTransparency = 1
    ResizeCornerAdorn.Text = ""
    ResizeCornerAdorn.Parent = self.Instances.WindowFrame

    local ResizeCorner = ThemedFrame.new()
    ResizeCorner.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
    ResizeCorner.BackgroundTransparency = 0.5 * GuiService.PreferredTransparency
    ResizeCorner.Size = UDim2.new(1, 0, 1, 0)
    ResizeCorner.Theme = "CutBottomRightCorner"
    ResizeCorner.SliceScaleMultiplier = 2
    ResizeCorner.Parent = ResizeCornerAdorn
    self.Instances.ResizeCorner = ResizeCorner

    self.Instances.ContentsAdorn.Size = UDim2.new(1, 0, 1, -TopBarHeightPixel - ResizeCornerSizePixel)

    --Connect the events.
    local MouseDown = false
    local LastPosition = nil
    local IntendedSizeX: number?, IntendedSizeY: number? = nil, nil
    table.insert(self.EventConnections, ResizeCornerAdorn.MouseButton1Down:Connect(function()
        MouseDown = true
        LastPosition = UserInputService:GetMouseLocation()
        IntendedSizeX, IntendedSizeY = self.Instances.WindowFrame.Size.X.Offset, self.Instances.WindowFrame.Size.Y.Offset
    end))

    table.insert(self.EventConnections, UserInputService.InputChanged:Connect(function(Input)
        if not LastPosition then return end
        if not MouseDown then return end
        if Input.UserInputType ~= Enum.UserInputType.MouseMovement and Input.UserInputType ~= Enum.UserInputType.Touch then return end

        local MousePosition = UserInputService:GetMouseLocation()
        local Delta = MousePosition - LastPosition
        LastPosition =  MousePosition
        IntendedSizeX, IntendedSizeY = IntendedSizeX + Delta.X, IntendedSizeY + Delta.Y
        self.Instances.WindowFrame.Size = UDim2.new(0,math.clamp(IntendedSizeX :: number, MinWidth :: number, MaxWidth :: number),0,math.clamp(IntendedSizeY + Delta.Y, MinHeight :: number, MaxHeight :: number))
    end))

    table.insert(self.EventConnections, UserInputService.InputEnded:Connect(function(Input)
        if Input.UserInputType ~= Enum.UserInputType.MouseButton1 and Input.UserInputType ~= Enum.UserInputType.Touch then return end
        MouseDown = false
        LastPosition = nil
        IntendedSizeX, IntendedSizeY = nil, nil
    end))

    table.insert(self.EventConnections, GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
        ResizeCorner.BackgroundTransparency = 0.5 * GuiService.PreferredTransparency
    end))
end

--[[
Enables the close button.
--]]
function ApiWindow.EnableClosing(self: ApiWindow, OnClose: (() -> ())?): ()
    --Create the button.
    local TopBarHeightPixel = self.Instances.TopBarAdorn.Size.Y.Offset
    local CloseButton = CloseFactory:Create()
    CloseButton.Size = UDim2.new(0, TopBarHeightPixel * 0.8, 0, TopBarHeightPixel * 0.7)
    CloseButton.Position = UDim2.new(1, -TopBarHeightPixel * 0.9, 0, TopBarHeightPixel * 0.075)
    CloseButton.Parent = self.Instances.TopBarAdorn
    self.Instances.CloseButton = CloseButton

    --Connect the button.
    local Closed = false
    table.insert(self.EventConnections, CloseButton.MouseButton1Down:Connect(function()
        if Closed then return end
        Closed = true
        if OnClose then
            OnClose()
        end
        self:Close()
    end))

    --Update the top bar.
    self:UpdateTopBar()
end

--[[
Enables the refresh button.
--]]
function ApiWindow.EnableRefreshing(self: ApiWindow, OnRefresh: () -> ()): ()
    --Create the button.
    local TopBarHeightPixel = self.Instances.TopBarAdorn.Size.Y.Offset
    local RefreshButton = RefreshFactory:Create()
    RefreshButton.Size = UDim2.new(0,TopBarHeightPixel * 0.8,0,TopBarHeightPixel * 0.7)
    RefreshButton.Parent = self.Instances.TopBarAdorn
    self.Instances.RefreshButton = RefreshButton

    local RefreshIcon = Instance.new("ImageLabel")
    RefreshIcon.BackgroundTransparency = 1
    RefreshIcon.Size = UDim2.new(0.8, 0, 0.8, 0)
    RefreshIcon.SizeConstraint = Enum.SizeConstraint.RelativeYY
    RefreshIcon.AnchorPoint = Vector2.new(0.5, 0.5)
    RefreshIcon.Position = UDim2.new(0.5, 0, 0.5, 0)
    RefreshIcon.Image = "rbxassetid://86084882582277"
    RefreshIcon.ZIndex = 4
    RefreshIcon.Parent = RefreshButton:GetAdornFrame()

    --Connect the button.
    local DB = false
    table.insert(self.EventConnections, RefreshButton.MouseButton1Down:Connect(function()
        if DB then return end
        DB = true
        OnRefresh()
        task.wait()
        DB = false
    end))

    --Update the top bar.
    self:UpdateTopBar()
end

--[[
Sets the title of the window.
--]]
function ApiWindow.SetTitle(self: ApiWindow, Name: string): ()
    self.Instances.TitleText.Text = Name
    self.Instances.ScreenGui.Name = `NexusAdminWindow_{Name}`
end

--[[
Sets the display order of the window.
--]]
function ApiWindow.SetDisplayOrder(self: ApiWindow, DisplayOrder: number): ()
    self.Instances.ScreenGui.DisplayOrder = DisplayOrder
end

--[[
Sets the relative size with the X relative to the Y.
--]]
function ApiWindow.SetRelativeSize(self: ApiWindow, RelativeX: number, RelativeY: number): ()
    self.InitialWidth = (RelativeX * self.InitialViewportSize.Y)
    self.InitialHeight = (RelativeY * self.InitialViewportSize.Y)
end

--[[
Adds a text list to the menu.
--]]
function ApiWindow.AddTextList(self: ApiWindow, TextType: ("TextLabel" | "TextBox")?): ApiTextList.ApiTextList
    local TextList = ApiTextList.new(self.ContentsAdorn, self.TextHeight, TextType)
    table.insert(ApiTextList, TextList)
    return TextList
end

--[[
Shows the window.
--]]
function ApiWindow.Show(self: ApiWindow, InitialPosition: UDim2?, InitialAnchorPoint: Vector2?, StartSide: Enum.NormalId?): ()
    if self.IsOpen then return end
    self.IsOpen = true

    --Determine the target position.
    local TargetPosition = Vector2.new(0.1 * self.InitialViewportSize.X, 0.2 * self.InitialViewportSize.Y)
    if InitialPosition then
        TargetPosition = Vector2.new(InitialPosition.X.Offset + (InitialPosition.X.Scale * self.InitialViewportSize.X), InitialPosition.Y.Offset + (InitialPosition.Y.Scale * self.InitialViewportSize.Y))
    end
    if InitialAnchorPoint then
        TargetPosition += Vector2.new(-InitialAnchorPoint.X * self.InitialWidth, -InitialAnchorPoint.Y * self.InitialHeight)
    end

    --Set the window size and position.
    self.StartSide = StartSide or self.StartSide
    self.Instances.WindowFrame.Size = UDim2.new(0, self.InitialWidth, 0, self.InitialHeight)
    if self.StartSide == Enum.NormalId.Left then
        self.Instances.WindowFrame.Position = UDim2.new(0, -self.InitialWidth, 0, TargetPosition.Y)
    elseif self.StartSide == Enum.NormalId.Right then
        self.Instances.WindowFrame.Position = UDim2.new(1, 0, 0, TargetPosition.Y)
    elseif self.StartSide == Enum.NormalId.Top then
        self.Instances.WindowFrame.Position = UDim2.new(0, TargetPosition.X, 0, -self.InitialHeight)
    elseif self.StartSide == Enum.NormalId.Bottom then
        self.Instances.WindowFrame.Position = UDim2.new(0, TargetPosition.X, 1, 0)
    end

    --Move the window.
    local FinalPosition = UDim2.new(0, TargetPosition.X, 0, TargetPosition.Y)
    if GuiService.ReducedMotionEnabled then
        self.Instances.WindowFrame.Position = FinalPosition
    else
        TweenService:Create(self.Instances.WindowFrame, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
            Position = FinalPosition,
        }):Play()
    end
end

--[[
Closes the window.
--]]
function ApiWindow.Close(self: ApiWindow): ()
    if not self.IsOpen then return end
    self.IsOpen = false

    --Determine the target position.
    local FinalPosition = UDim2.new()
    local CurrentSize, CurrentPosition = self.Instances.WindowFrame.AbsoluteSize, self.Instances.WindowFrame.AbsolutePosition
    if self.StartSide == Enum.NormalId.Left then
        FinalPosition = UDim2.new(0, -CurrentSize.X, 0, CurrentPosition.Y)
    elseif self.StartSide == Enum.NormalId.Right then
        FinalPosition = UDim2.new(1, 0, 0, CurrentPosition.Y)
    elseif self.StartSide == Enum.NormalId.Top then
        FinalPosition = UDim2.new(0, CurrentPosition.X, 0, -CurrentSize.Y)
    elseif self.StartSide == Enum.NormalId.Bottom then
        FinalPosition = UDim2.new(0, CurrentPosition.X, 1, 0)
    end

    --Move the window.
    if GuiService.ReducedMotionEnabled then
        self.Instances.WindowFrame.Position = FinalPosition
    else
        TweenService:Create(self.Instances.WindowFrame, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.In), {
            Position = FinalPosition,
        }):Play()
    end

    --Destroy the window.
    task.delay(0.5, function()
        self:Destroy()
    end)
end

--[[
Destroys the API Window.
--]]
function ApiWindow.Destroy(self: ApiWindow): ()
    --Disconnect the events.
    for _, EventConnection in self.EventConnections do
        EventConnection:Disconnect()
    end

    --Destroy the instances.
    self.Instances.ScreenGui:Destroy()
    self.Instances.Background:Destroy()
    self.Instances.TopBarBackground:Destroy()
    if self.Instances.ResizeCorner then
        self.Instances.ResizeCorner:Destroy()
    end
    if self.Instances.RefreshButton then
        self.Instances.RefreshButton:Destroy()
    end
    if self.Instances.CloseButton then
        self.Instances.CloseButton:Destroy()
    end
    for _, TextList in self.Instances.TextLists do
        TextList:Destroy()
    end
end

return ApiWindow


--Creates text entries in an ApiTextList.
--!strict

export type Properties = {[string]: any}
export type TextListEntry = {
    GuiInstance: GuiObject,
    Update: (self: TextListEntry, Index: number, Data: Properties) -> (TextListEntry),
    Destroy: (self: TextListEntry) -> (),
}

return function(TextHeight: number, TextType: "TextLabel" | "TextBox"): (InitialIndex: number, InitialData: Properties) -> (TextListEntry)
    local TextDefaults = {
        TextColor3 = Color3.fromRGB(255, 255, 255),
        TextStrokeColor3 = Color3.fromRGB(0, 0, 0),
        TextStrokeTransparency = 0,
        Font = Enum.Font.SourceSans,
    }

    local TextListEntry = {}
    TextListEntry.__index = TextListEntry

    --[[
    Creates a text list entry.
    --]]
    function TextListEntry.new(InitialIndex: number, InitialData: Properties): TextListEntry
        local TextLabel = Instance.new(TextType) :: any
        TextLabel.BackgroundTransparency = 1
        if TextType == "TextBox" then
            (TextLabel :: TextBox).ClearTextOnFocus = false
            (TextLabel :: TextBox).TextEditable = false
        end
        (TextLabel :: TextLabel).TextSize = TextHeight;
        (TextLabel :: TextLabel).TextXAlignment = Enum.TextXAlignment.Left;

        local self = (setmetatable({
            GuiInstance = TextLabel,
        }, TextListEntry) :: any) :: TextListEntry
        self:Update(InitialIndex, InitialData)
        return self
    end

    --[[
    Updates the text list entry.
    --]]
    function TextListEntry.Update(self: TextListEntry, Index: number, Data: Properties): ()
        local TextLabel = (self.GuiInstance :: any)
        TextLabel.Name = `Line{Index}`

        for Key, _ in Data do
            if not TextDefaults[Key] then
                TextDefaults[Key] = TextLabel[Key]
            end
        end
        for Key, Value in TextDefaults do
            TextLabel[Key] = Data[Key] or Value
        end
    end

    --[[
    Destroys the text list entry.
    --]]
    function TextListEntry.Destroy(self: TextListEntry, Index: number, Data: Properties): ()
        self.GuiInstance:Destroy()
    end

    --Return the constructor.
    return TextListEntry.new
end


--API for creating standard windows.
--!strict

local ApiWindow = require(script.Parent:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("ApiWindow"))

local Window = {}
Window.__index = Window

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

--[[
Creates a window with a given name.
--]]
function Window.CreateWindow(self: Window, TitleName: string): ApiWindow.ApiWindow
    local NewWindow = ApiWindow.new()
    NewWindow:SetTitle(TitleName)
    return NewWindow
end

return Window


--Legacy code for parsing arguments. The code has been fitted to use the
--new API locations, but does not have full unit tests.

local ArgumentParser = {}

local PlayerMap = {}
local AllUniquePlayers = {}
game:GetService("Players").PlayerAdded:Connect(function(Player)
    if PlayerMap[Player.Name] == nil then
        PlayerMap[Player.Name] = Player

        local Players = {}
        for _,Player in pairs(PlayerMap) do
            table.insert(Players,Player)
        end
        AllUniquePlayers = Players
    end
end)

function ArgumentParser:CreateParser(BaseString,ReferencePlayer)
    local CurrentPoint = 1
    local SplitStrings = {}

    local function FilterString(String)
        return _G.GetNexusAdminServerAPI().Filter:FilterString(String,ReferencePlayer)
    end

    string.gsub(BaseString,"([^%s]+%s*)",function(SubString)
        table.insert(SplitStrings,SubString)
    end)

    local function SetPointer(NewPoint)
        if type(NewPoint) ~= "number" then
            NewPoint = CurrentPoint
        end
        NewPoint = math.floor(NewPoint)

        if NewPoint < 1 then
            NewPoint = 1
        end
        if NewPoint > #SplitStrings then
            NewPoint = #SplitStrings + 1
        end

        CurrentPoint = NewPoint
    end

    local function GetNextElement()
        local NextElement = SplitStrings[CurrentPoint] or ""
        SetPointer(CurrentPoint + 1)
        return string.match(NextElement,"([^%s]+)") or ""
    end

    local Parser = {}
    function Parser:ResetPointer()
        SetPointer(1)
    end

    function Parser:SetPointer(NewPoint)
        SetPointer(NewPoint)
    end

    function Parser:OffsetPointer(Offset)
        if type(Offset) ~= "number" then return end
        SetPointer(CurrentPoint + Offset)
    end

    function Parser:GetCurrentPointer()
        return CurrentPoint
    end

    function Parser:GetTotalArguments()
        return #SplitStrings
    end

    function Parser:HasNext()
        return CurrentPoint <= #SplitStrings
    end

    function Parser:GetNextNumber()
        local NextElement = GetNextElement()
        return tonumber(NextElement)
    end

    function Parser:GetNextNumbers()
        local NextElement = GetNextElement()
        local NumberArray = {}

        string.gsub(NextElement,"([^,]+)",function(SubString)
            local Number = tonumber(SubString)
            if Number then table.insert(NumberArray,Number) end
        end)

        return NumberArray
    end

    local function ConvertToBool(BoolString)
        if BoolString == "true" or BoolString == "t" or BoolString == "yes" or BoolString == "y" then
            return true
        elseif BoolString == "false" or BoolString == "f" or BoolString == "no" or BoolString == "n" then
            return false
        end
    end

    function Parser:GetNextBool()
        local NextElement = string.lower(GetNextElement())

        return ConvertToBool(NextElement)
    end

    function Parser:GetNextBools()
        local NextElement = string.lower(GetNextElement())
        local BoolArray = {}

        string.gsub(NextElement,"([^,]+)",function(SubString)
            local BoolElement = ConvertToBool(SubString)
            if BoolElement ~= nil then table.insert(BoolArray,BoolElement) end
        end)

        return BoolArray
    end

    function Parser:GetNextString(ApplyFilter)
        local NextElement = GetNextElement()
        if NextElement ~= "" then
            if ApplyFilter then
                return FilterString(NextElement)
            else
                return NextElement
            end
        end
    end

    function Parser:GetRemainder(ApplyFilter)
        local Remainder = ""

        for i = CurrentPoint, #SplitStrings do
            Remainder = Remainder..SplitStrings[i]
        end
        SetPointer(#SplitStrings + 1)

        if Remainder ~= "" then
            if ApplyFilter then
                return FilterString(Remainder)
            else
                return Remainder
            end
        end
    end

    function Parser:GetNextPlayers(IncludeDisconnected)
        local NextElement = GetNextElement()
        local BasePlayers = game:GetService("Players"):GetPlayers()
        local AllPlayers = (IncludeDisconnected and AllUniquePlayers or BasePlayers)
        local PlayersLeft = game:GetService("Players"):GetPlayers()
        local ReturnPlayers = {}

        local function AddPlayer(Player)
            for i,OtherPlayer in pairs(PlayersLeft) do
                if Player == OtherPlayer then
                    table.remove(PlayersLeft,i)
                    break
                end
            end

            table.insert(ReturnPlayers,Player)
        end

        local function RemovePlayer(Player)
            for i,OtherPlayer in pairs(ReturnPlayers) do
                if Player == OtherPlayer then
                    table.remove(ReturnPlayers,i)
                    break
                end
            end

            table.insert(PlayersLeft,Player)
        end

        local PlayerArguments = {}
        string.gsub(NextElement,"([^,]+)",function(PlayerString)
            table.insert(PlayerArguments,PlayerString)
        end)

        for _,PlayerString in pairs(PlayerArguments) do
            local Remove = (string.sub(PlayerString,1,1) == "-")
            if Remove then PlayerString = string.sub(PlayerString,2) end

            local function ApplyPlayer(Player)
                if Remove then
                    RemovePlayer(Player)
                else
                    AddPlayer(Player)
                end
            end

            local LowerPlayerString = string.lower(PlayerString)
            local FirstCharacter = string.sub(PlayerString,1,1)
            if FirstCharacter == "%" then
                local TeamName = string.lower(string.sub(PlayerString,2))
                local TeamNameLength = string.len(TeamName)

                local Teams = game:GetService("Teams"):GetTeams()
                for _,Team in pairs(Teams) do
                    if string.sub(string.lower(Team.Name),1,TeamNameLength) == TeamName then
                        for _,Player in pairs(Team:GetPlayers()) do
                            ApplyPlayer(Player)
                        end
                    end
                end
            elseif FirstCharacter == "$" then
                local GroupId = tonumber(string.sub(PlayerString,2))

                if GroupId then
                    for _,Player in pairs(BasePlayers) do
                        local Worked,Return = pcall(function() return Player:GetRankInGroup(GroupId) end)
                        if Worked == true and Return and Return > 0 then
                            ApplyPlayer(Player)
                        end
                    end
                end
            elseif PlayerString == "admins" then
                for _,Player in pairs(BasePlayers) do
                    if _G.GetNexusAdminServerAPI().Authorization.GetAdminLevel(Player) > -1 then
                        ApplyPlayer(Player)
                    end
                end
            elseif PlayerString == "nonadmins" then
                for _,Player in pairs(BasePlayers) do
                    if _G.GetNexusAdminServerAPI().Authorization.GetAdminLevel(Player) <= -1 then
                        ApplyPlayer(Player)
                    end
                end
            elseif PlayerString == "random" then
                if #BasePlayers == 1 then
                    ApplyPlayer(BasePlayers[1])
                elseif #BasePlayers >= 1 then
                    ApplyPlayer(BasePlayers[math.random(1,#BasePlayers)])
                end
            elseif PlayerString == "urandom" then
                if #PlayersLeft == 1 then
                    ApplyPlayer(PlayersLeft[1])
                elseif #PlayersLeft >= 1 then
                    ApplyPlayer(PlayersLeft[math.random(1,#PlayersLeft)])
                end
            elseif PlayerString == "me" then
                if ReferencePlayer then
                    ApplyPlayer(ReferencePlayer)
                end
            elseif PlayerString == "others" then
                for _,Player in pairs(AllPlayers) do
                    if Player ~= ReferencePlayer then
                        ApplyPlayer(Player)
                    end
                end
            elseif LowerPlayerString == "all" then
                for _,Player in pairs(AllPlayers) do
                    ApplyPlayer(Player)
                end
            else
                local PlayerName = string.lower(PlayerString)
                local PlayerNameLength = string.len(PlayerString)

                for _,Player in pairs(AllPlayers) do
                    if string.sub(string.lower(Player.Name),1,PlayerNameLength) == PlayerName then
                        ApplyPlayer(Player)
                    end
                end
            end
        end

        return ReturnPlayers
    end

    return Parser
end

function ArgumentParser:StringToArguments(String,ArguementTypes,PlayerSaidBy,IgnoreFilter)
    local Parser = ArgumentParser:CreateParser(String,PlayerSaidBy)
    local FilterStrings = (IgnoreFilter ~= true)
    local Arguments = {}

    local function GetArgument(ArgumentType)
        if ArgumentType == "Number" then
            return Parser:GetNextNumber()
        elseif ArgumentType == "TableOfNumbers" then
            return Parser:GetNextNumbers()
        elseif ArgumentType == "String" then
            return Parser:GetNextString(FilterStrings)
        elseif ArgumentType == "LongString" then
            return Parser:GetRemainder()
        elseif ArgumentType == "Bool" or ArgumentType == "Boolean" then
            return Parser:GetNextBool()
        elseif ArgumentType == "TableOfPlayers" then
            return Parser:GetNextPlayers()
        elseif ArgumentType == "TableOfAllPlayersByName" then
            local Players = Parser:GetNextPlayers(true)
            for i,Player in pairs(Players) do
                Players[i] = Player.Name
            end
            return Players
        elseif ArgumentType == "TableOfAllPlayersById" then
            local Players = Parser:GetNextPlayers(true)
            for i,Player in pairs(Players) do
                Players[i] = Player.UserId
            end
            return Players
        elseif ArgumentType == "Color3" then
            local Colors = Parser:GetNextNumbers()
            if #Colors >= 3 then
                return Color3.new(Colors[1]/255,Colors[2]/255,Colors[3]/255)
            end
        elseif ArgumentType == "Vector3" then
            local Coordinates = Parser:GetNextNumbers()
            if #Coordinates >= 3 then
                return Color3.new(Coordinates[1],Coordinates[2],Coordinates[3])
            end
        end
    end

    for i,ArgumentType in pairs(ArguementTypes or {}) do
        if type(ArgumentType) == "table" then
            for _,SubArgumentType in pairs(ArgumentType) do
                local NextArgument = GetArgument(SubArgumentType)
                if NextArgument then
                    Arguments[i] = NextArgument
                    break
                else
                    Parser:OffsetPointer(-1)
                end
            end
        else
            Arguments[i] = GetArgument(ArgumentType)
        end
    end

    return Arguments
end

return ArgumentParser


--Handles users being authorized.
--!strict

local NexusInstance = require(script.Parent.Parent:WaitForChild("Packages"):WaitForChild("NexusInstance"))
local Configuration = require(script.Parent:WaitForChild("Configuration"))

local TypedEvent = NexusInstance.TypedEvent

local Authorization = {}
Authorization.__index = Authorization

export type Authorization = {
    Configuration: Configuration.Configuration,
    AdminLevels: {[string]: number},
    AdminLevelChanged: NexusInstance.TypedEvent<Player>,
} & typeof(setmetatable({}, Authorization))

--[[
Creates an authorization instance.
--]]
function Authorization.new(Configuration: Configuration.Configuration): Authorization
    return setmetatable({
        Configuration = Configuration,
        AdminLevels = {},
        AdminLevelChanged = TypedEvent.new() :: any,
    }, Authorization) :: Authorization
end

--[[
Returns the admin level for a player.
--]]
function Authorization.GetAdminLevel(self: Authorization, Player: Player): number
    if self.Configuration then
        return self.AdminLevels[tostring(Player.UserId)] or self.Configuration.DefaultAdminLevel
    else
        return self.AdminLevels[tostring(Player.UserId)] or -1
    end
end

--[[
Returns if the user is authorized.
--]]
function Authorization.IsPlayerAuthorized(self: Authorization, Player: Player, AdminLevel: number): boolean
    return self:GetAdminLevel(Player) >= AdminLevel
end

--[[
Waits for an admin level to be defined.
Returns it when it is initialized.
--]]
function Authorization.YieldForAdminLevel(self: Authorization, Player: Player): number
    while not self.AdminLevels[tostring(Player.UserId)] do
        self.AdminLevelChanged:Wait()
    end
    return self.AdminLevels[tostring(Player.UserId)]
end

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

return Authorization


--Stores and parses the configuration.
--!strict

local DEFAULT_COMMAND_CONFIGURATIONS = {
    --Default Admin Level for Server Lock (slock).
    DefaultServerLockAdminLevel = 0,
} :: {[string]: any}

local Configuration = {}
Configuration.__index = Configuration

export type Configuration = {
    Version: string,
    VersionNumberId: number,
    CmdrVersion: string,
    RawConfiguration: {[string]: any},
    CommandPrefix: string,
    ActivationKeys: {Enum.KeyCode},
    DefaultAdminLevel: number,
    AdministrativeLevel: number,
    BuildUtilityLevel: number,
    BasicCommandsLevel: number,
    UsefulFunCommandsLevel: number,
    FunCommandsLevel: number,
    PersistentCommandsLevel: number,
    Admins: {[number]: number},
    AdminNames: {[number]: string},
    GroupAdminLevels: {[number]: {[number]: number}},
    BannedUsers: {[number]: string | boolean},
    CommandConfigurations: {
        DefaultServerLockAdminLevel: number,
        PersistentBanImmune: number,
        [string]:any,
    },
    CommandLevelOverrides: {[string]: {[string]: number}},
    FeatureFlagOverrides: {[string]: any},
} & typeof(setmetatable({}, Configuration))

--[[
Creates the configuration.
--]]
function Configuration.new(ConfigurationTable: {[string]: any}): Configuration
    ConfigurationTable = ConfigurationTable or {}
    local self = setmetatable({
        Version = "Version 2.11.0",
        VersionNumberId = 2.11,
        CmdrVersion = "Version 1.12.0",
        RawConfiguration = ConfigurationTable,
        CommandPrefix = ConfigurationTable.CommandPrefix or ":",
        ActivationKeys = ConfigurationTable.ActivationKeys or {Enum.KeyCode.BackSlash},
        DefaultAdminLevel = ConfigurationTable.DefaultAdminLevel or -1,
        AdministrativeLevel = ConfigurationTable.AdministrativeLevel or 1,
        BuildUtilityLevel = ConfigurationTable.BuildUtilityLevel or 1,
        BasicCommandsLevel = ConfigurationTable.BasicCommandsLevel or 1,
        UsefulFunCommandsLevel = ConfigurationTable.UsefulFunCommandsLevel or 2,
        FunCommandsLevel = ConfigurationTable.FunCommandsLevel or 3,
        PersistentCommandsLevel = ConfigurationTable.PersistentCommandsLevel or ConfigurationTable.PersistentCommands or 4,
        Admins = ConfigurationTable.Admins or {},
        AdminNames = ConfigurationTable.AdminNames or {
            [-1] = "Non-Admin", --Basic commands, like !help and !quote
            [0] = "Debug Admin", --Only debug commands
            [1] = "Moderator", --Some commands, can't kick, no "fun" commands
            [2] = "Admin", --Most commands, can't ban or shutdown
            [3] = "Super Admin",--All commands, can be kicked by full admin
            [4] = "Owner Admin", --All commands, except :admin
            [5] = "Creator Admin", --All commands, including :admin
        },
        GroupAdminLevels = ConfigurationTable.GroupAdminLevels or {},
        BannedUsers = ConfigurationTable.BannedUsers or {},
        CommandLevelOverrides = ConfigurationTable.CommandLevelOverrides or {},
        FeatureFlagOverrides = ConfigurationTable.FeatureFlagOverrides or {},
        CommandConfigurations = ConfigurationTable.CommandConfigurations or DEFAULT_COMMAND_CONFIGURATIONS,
    }, Configuration) :: Configuration

    --Confirm all DEFAULT_COMMAND_CONFIGURATIONS exist within CommandConfigurations.
    for Name, Value in DEFAULT_COMMAND_CONFIGURATIONS do
        if self.CommandConfigurations[Name] ~= nil then continue end
        self.CommandConfigurations[Name] = Value
    end

    --DEFAULT_COMMAND_CONFIGURATIONS that default to other specified configurations.
    if not self.CommandConfigurations.PersistentBanImmuneLevel then
        self.CommandConfigurations.PersistentBanImmuneLevel = self.PersistentCommandsLevel
    end

    --Correct the administrative commands (V.2.0.0 and newer).
    if ConfigurationTable.CommandLevelOverrides and not ConfigurationTable.CommandLevelOverrides.Administrative then
        ConfigurationTable.CommandLevelOverrides.Administrative = {
            admin = 5,
            unadmin = 5,
            ban = 3,
            unban = 3,
            kick = 2,
            slock = 2,
            keybind = -1,
            unkeybind = -1,
            keybinds = -1,
            alias = -1,
            cmds = -1,
            cmdbar = -1,
            debug = 0,
            logs = 0,
            admins = 0,
            bans = nil,
            usage = -1,
            featureflag = nil,
        }
    end

    --Add F2 to the ActivationKeys if F2 is not defined but BackSlash is.
    local ActivationKeysMap = {}
    for _, Key in self.ActivationKeys do
        ActivationKeysMap[Key] = true
    end
    if not ActivationKeysMap[Enum.KeyCode.F2] and ActivationKeysMap[Enum.KeyCode.BackSlash] then
        table.insert(self.ActivationKeys, Enum.KeyCode.F2)
    end

    --Return the configuration.
    return self
end

--[[
Raw configuration used table. This is intended for custom
configuration entries used for custom deployments.
--]]
function Configuration.GetRaw(self: Configuration): {[string]: any}
    return self.RawConfiguration
end

--[[
Returns the admin level to use for an integrated command.
--]]
function Configuration.GetCommandAdminLevel(self: Configuration, Category: string, Command: string): number
    --Get the category default.
    local CategoryDefault = self[`{Category}Level`] :: number
    if not CategoryDefault then
        error(`{Category}" is not a category supported by the configuration.`)
    end

    --Return the override or the default.
    if self.CommandLevelOverrides[Category] and self.CommandLevelOverrides[Category][Command] then
        return self.CommandLevelOverrides[Category][Command]
    else
        return CategoryDefault
    end
end

return Configuration


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

local Filter = {}
Filter.__index = Filter

export type Filter = {
    TextService: TextService,
    Chat: Chat,
} & typeof(setmetatable({}, Filter))

--[[
Creates a filter instance.
--]]
function Filter.new(): Filter
    return setmetatable({
        TextService = game:GetService("TextService"),
        Chat = game:GetService("Chat"),
    }, Filter) :: Filter
end

--[[
Escapes a string for rich text.
--]]
function Filter.EscapeRichText(self: Filter, String: string): string
    while true do
        local NewString, _ = string.gsub(String, "<([^/>]+)>([^<]+)</([^>]+)>", function(StartTag: string, Contents: string, EndTag: string): string
            local StartTagStart = string.match(StartTag, "^[^%s]+")
            local EndTagStart = string.match(EndTag, "^[^%s]+")
            if StartTagStart and EndTagStart and string.lower(StartTagStart) == string.lower(EndTagStart) then
                return `<{StartTag}>{Contents}</{EndTag}>`
            end
            return `<{StartTag}>{Contents}</{EndTag}>`
        end :: any)
        if NewString == String then break end
        String = NewString
    end
    local NewString, _ = string.gsub(String, "<[^/]+/>", "<&1/>")
    return NewString
end

return Filter


--Stores a log of events.
--!strict

local NexusInstance = require(script.Parent.Parent:WaitForChild("Packages"):WaitForChild("NexusInstance"))

local TypedEvent = NexusInstance.TypedEvent

local Logs = {}
Logs.__index = Logs

export type Logs = {
    MaxLogs: number,
    Logs: {any},
    LogAdded: NexusInstance.TypedEvent<any>,
} & typeof(setmetatable({}, Logs))

--[[
Creates a logs instance.
--]]
function Logs.new(MaxLogs: number?): Logs
    return setmetatable({
        MaxLogs = MaxLogs or 10000,
        Logs = {},
        LogAdded = TypedEvent.new() :: any,
    }, Logs) :: Logs
end

--[[
Returns the logs.
--]]
function Logs.GetLogs(self: Logs): {any}
    return self.Logs
end

--[[
Adds a log.
--]]
function Logs.Add(self: Logs, Log: any): ()
    table.insert(self.Logs, 1, Log)
    self.Logs[(self.MaxLogs :: number) + 1] = nil
    self.LogAdded:Fire(Log)
end

--[[
Destroys the logs.
--]]
function Logs.Destroy(self: Logs): ()
    self.LogAdded:Destroy()
end

return Logs


--Utility for time.
--!strict

local TIME_DURATIONS = {
    {
        Label = "second",
        Seconds = 1,
        MaxDisplay = 60,
    },
    {
        Label = "minute",
        Seconds = 60,
        MaxDisplay = 60,
    },
    {
        Label = "hour",
        Seconds = 60 * 60,
        MaxDisplay = 24,
    },
    {
        Label = "day",
        Seconds = 24 * 60 * 60,
        MaxDisplay = 10 ^ 99, --No limit.
    },
}

local Time = {}

export type Time = typeof(Time)

--[[
Returns the current timestamp, or the timestamp for the given time.
--]]
function Time.GetTimeString(self: Time, Time: number?): string
    local Date = os.date("*t", Time) :: any
    return string.format("%02.0f:%02.0f:%02.0f", Date.hour % 24, Date.min, Date.sec)
end

--[[
Returns the current timestamp, or the timestamp for the given time.
--]]
function Time.GetDateTimeString(self: Time, Time: number?): string
    local Date = os.date("*t", Time) :: any
    return string.format("%04.0f-%02.0f-%02.0f %02.0f:%02.0f:%02.0f", Date.year, Date.month, Date.day, Date.hour % 24, Date.min, Date.sec)
end

--[[
Returns the a human-readable display for the given seconds.
--]]
function Time.GetDurationString(self: Time, Seconds: number): string
    local DurationString = ""
    for _, Label in TIME_DURATIONS do
        local Total = math.floor(Seconds / Label.Seconds)
        if Total <= 0 then break end
        if DurationString ~= "" then
            DurationString = `, {DurationString}`
        end
        local DisplayTotal = Total % Label.MaxDisplay
        DurationString = `{DisplayTotal} {Label.Label}{DisplayTotal == 1 and "" or "s"}{DurationString}`
    end
    return DurationString
end

return Time


--Type representing a registered feature flag.
--!strict

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

--[[
Registers the types.
--]]
return function(API: Types.NexusAdminApi)
    --Create the feature flag types.
    local FeatureFlagType = {
        --[[
        Transforms the string to a list of fast flags.
        --]]
        Transform = function(Text: string): {string}
            return API.Cmdr.Util.MakeFuzzyFinder(API.FeatureFlags:GetAllFeatureFlags())(Text)
        end,

        --[[
        Returns if the input is valid and an error message
        for when it is invalid.
        --]]
        Validate = function(FeatureFlags: {string}): (boolean, string)
            return #FeatureFlags > 0, "No feature flags with that name were found."
        end,

        --[[
        Returns the results for auto completing.
        --]]
        Autocomplete = function(FeatureFlags: {string}): {string}
            return FeatureFlags
        end,

        --[[
        Returns the value to use.
        --]]
        Parse = function(FeatureFlags: {string}): string
            return FeatureFlags[1]
        end,
    }
    local FeatureFlagsType = {
        Listable = true,
        Transform = FeatureFlagType.Transform,
        Validate = FeatureFlagType.Validate,
        Autocomplete = FeatureFlagType.Autocomplete,

        --[[
        Returns the value to use.
        --]]
        Parse = function(FeatureFlags: {string}): {string}
            return FeatureFlags
        end,
    }

    --Register the types.
    API.Cmdr.Registry:RegisterType("nexusAdminFastFlag", FeatureFlagType :: any)
    API.Cmdr.Registry:RegisterType("nexusAdminFastFlags", FeatureFlagsType :: any)
end


--Implementation of a command.
--!strict

local Players = game:GetService("Players")

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

return {
    Keyword = "admins",
    Category = "Administrative",
    Description = "Opens up a window containing the list of admins.",
    ClientRun = function(CommandContext: Types.CmdrCommandContext)
        local Util = IncludedCommandUtil.ForContext(CommandContext)
        local Api = Util:GetClientApi()

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

        local function FilterAdmins(SearchTerm: string, _: {{[string]: any}})
            SearchTerm = string.lower(SearchTerm)

            --Populate the data and sort by the level.
            local AdminLevelsData = {}
            for AdminLevel, LevelName in Api.Configuration.AdminNames do
                if AdminLevel >= 0 then
                    table.insert(AdminLevelsData,{
                        Name = LevelName,
                        Level = AdminLevel,
                        Players = {},
                    })
                end
            end
            table.sort(AdminLevelsData, function(a, b) return a.Level > b.Level end)

            --Determine the highest levels for all the players.
            for _, Player in Players:GetPlayers() do
                if string.find(string.lower(Player.Name), SearchTerm) then
                    for _, LevelData in AdminLevelsData do
                        if Api.Authorization:IsPlayerAuthorized(Player, LevelData.Level) then
                            table.insert(LevelData.Players, Player)
                            break
                        end
                    end
                end
            end

            --Create and return the lines.
            local Lines = {} :: {{[string]: any}}
            for _, LevelData in AdminLevelsData do
                table.insert(Lines, {Text = `{LevelData.Name} ({LevelData.Level})`, Font = Enum.Font.SourceSansBold})
                if #LevelData.Players == 0 then
                    table.insert(Lines, {Text = "(No one)", Font = Enum.Font.SourceSansItalic})
                else
                    table.sort(LevelData.Players, function(a: Player, b: Player) return string.lower(a.Name) < string.lower(b.Name) end)
                    for _, Player in LevelData.Players do
                        table.insert(Lines, {Text = `{Player.Name} ({Player.UserId})`})
                    end
                end
                table.insert(Lines, {Text = ""})
            end
            table.remove(Lines, #Lines)
            return Lines
        end

        local function UpdateText()
            TextList:SetLines(FilterAdmins("", {}))
        end

        TextList:EnableSearching(FilterAdmins)
        task.spawn(UpdateText)
        Window:EnableRefreshing(UpdateText)
        Window:EnableClosing()
        Window:EnableResizing()
        Window:Show()
    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 = "alias",
    Category = "Administrative",
    Description = "Creates a new, single command out of a command and given arguments. (Reimplmented for Nexus Admin)",
    Arguments = {
        {
            Type = "string";
            Name = "Name";
            Description = "The key or input type you'd like to bind the command to."
        },
        {
            Type = "string";
            Name = "Command";
            Description = "The command text you want to run. Separate multiple commands with \"&&\". Accept arguments with $1, $2, $3, etc."
        },
    },
    ClientRun = function(CommandContext: Types.CmdrCommandContext, Name: string, Command: string): string
        local Util = IncludedCommandUtil.ForContext(CommandContext)
        local Api = Util:GetApi()

        --Register the command in Nexus Admin for chat executing.
        local NameComponents = string.split(Name, "|")
        Api.Registry:LoadCommand({
            Keyword = NameComponents[1],
            Group = "Aliases",
            Description = NameComponents[2],
            Prefix = Api.Configuration.CommandPrefix,
        } :: any);

        --Register the Cmdr alias.
        (CommandContext.Cmdr.Registry :: any):RegisterCommandObject((CommandContext.Cmdr.Util :: any).MakeAliasCommand(Name,Command), true)
        return `Created alias {Name}`
    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 = "bans",
    Category = "Administrative",
    Description = "Opens up a window containing the list of banned users.",
    ServerLoad = function(Api: Types.NexusAdminApiServer)
        local GetBansRemoteFunction = Instance.new("RemoteFunction")
        GetBansRemoteFunction.Name = "GetBans"
        GetBansRemoteFunction.Parent = Api.EventContainer

        function GetBansRemoteFunction.OnServerInvoke(Player): {string}
            if Api.Authorization:IsPlayerAuthorized(Player, Api.Configuration:GetCommandAdminLevel("Administrative", "bans")) then
                local BannedNames = {}
                for BannedId, BannedPlayerData in Api.CommandData.BannedUserIds do
                    table.insert(BannedNames, `{BannedPlayerData[3]} ({BannedId})`)
                end
                table.sort(BannedNames, function(a, b) return string.lower(a) < string.lower(b) end)
                return BannedNames
            else
                return {"Unauthorized"}
            end
        end
    end,
    ClientRun = function(CommandContext: Types.CmdrCommandContext)
        local Util = IncludedCommandUtil.ForContext(CommandContext)
        local Api = Util:GetClientApi()

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

        local function UpdateText()
            TextList:SetLines({"Loading..."})
            TextList:SetLines((Api.EventContainer:WaitForChild("GetBans") :: RemoteFunction):InvokeServer())
        end

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