--!strict
--!optimize 2
--!native
--!nolint LocalShadow
--!nolint DuplicateLocal

local Lexer = require(script.Parent.Lexer)
type Token = Lexer.Token
type TokenKind = Lexer.TokenKind
type Lexer = Lexer.Lexer

local Colors = script.Parent.Colors
local ColorsCss = require(Colors.Css)
local ColorsTailwind = require(Colors.Tailwind)
local ColorsBrick = require(Colors.Brick)

local TupleAnnotations = script.TupleAnnotations
local ColorSeqTupleAnnotation = require(TupleAnnotations.ColorSeq)
local NumSeqTupleAnnotation = require(TupleAnnotations.NumSeq)
local LerpAnnotation = require(TupleAnnotations.Lerp)
local CeilAnnotation = require(TupleAnnotations.Ceil)
local FloorAnnotation = require(TupleAnnotations.Floor)
local RoundAnnotation = require(TupleAnnotations.Round)

local Parser = {}
Parser.__index = Parser

local function ParseCommentMultiEnd(parser: Parser, startEqualsAmount: number): Token?
    -- We keep advancing tokens until we find a closing multiline string
    -- token with the same amount of equals signs as the start token.
    while true do
        local token = parser.Lexer()
        if not token then return end

        if token.Kind == "StringMultiEnd" then
            local endTokenValue = token.Value
            local endEqualsAmount = #string.sub(endTokenValue, 2, -2)

            if startEqualsAmount == endEqualsAmount then
                return parser:CoreAdvance()
            end
        end
    end
end

local function ParseCommentMulti(parser: Parser, token: Token): Token?
    if token.Kind ~= "CommentMultiStart" then return end

    local tokenValue = token.Value
    local startEqualsAmount = #string.sub(tokenValue, 4, -2)

    return ParseCommentMultiEnd(parser, startEqualsAmount)
end

local function ParseCommentSingle(parser: Parser, token: Token): Token?
    if token.Kind ~= "CommentSingle" then return end

    return parser:CoreAdvance()
end

function Parser.CoreAdvance(self: Parser): Token?
    self.DidAvance = true

    while true do
        local token = self.Lexer()
        if not token then return end
        if (token.Kind :: TokenKind) ~= "Error" then return token end
    end
end

function Parser.Advance(self: Parser): Token?
    local token = self:CoreAdvance()
    if not token then return nil end

    local token = ParseCommentMulti(self, token) or token

    return ParseCommentSingle(self, token) or token
end

function Parser.new(lexer: Lexer, rootTreeNode: TreeNode)
    local parser = setmetatable({
        Lexer = lexer,

        TreeNodes = { rootTreeNode },
        CurrentTreeNode = rootTreeNode,

        DidAvance = false
    }, Parser)

    return parser
end

export type OperatorKind = "OpPow" | "OpDiv" | "OpFloorDiv" | "OpMod" | "OpMult" | "OpAdd" | "OpSub"
export type OperatorSymbol = { Kind: "Operator", OperatorKind: OperatorKind }

local OperatorPowSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpPow" }
local OperatorDivSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpDiv" }
local OperatorFloorDivSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpFloorDiv" }
local OperatorModSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpMod" }
local OperatorMultSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpMult" }
local OperatorAddSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpAdd" }
local OperatorSubSymbol: OperatorSymbol = { Kind = "Operator", OperatorKind = "OpSub" }
local OperatorSymbols: { [string]: OperatorSymbol } = {
    ["OpPow"] = OperatorPowSymbol,
    ["OpDiv"] = OperatorDivSymbol,
    ["OpFloorDiv"] = OperatorFloorDivSymbol,
    ["OpMod"] = OperatorModSymbol,
    ["OpMult"] = OperatorMultSymbol,
    ["OpAdd"] = OperatorAddSymbol,
    ["OpSub"] = OperatorSubSymbol
}

type IncompleteEnumShorthand = { Kind: "IncompleteEnumShorthand", Value: string }

local function NewIncompleteEnumShorthand(value: string): IncompleteEnumShorthand
    return { Kind = "IncompleteEnumShorthand", Value = value }
end

local DATATYPE_NONE = require(script.DatatypeNone)
type DatatypeNone = DATATYPE_NONE.DatatypeNone

export type Datatype = string | boolean | EnumItem | Color3 | number | UDim | UDim2 | OperatorSymbol | Vector3 | Rect | Font | Vector2 | DatatypeNone | TupleData | Content | Vector2int16 | Vector3int16 | IncompleteEnumShorthand | ColorSequence | NumberSequence

export type TreeNode = {
    Attributes: { [string]: FinalizedDatatype },
    Properties: { [string]: FinalizedDatatype },
    Name: string?,
    Rules: { TreeNode },
    Derives: { [string]: true },
    Selector: string?,
    Priority: number?,
    Parent: TreeNode
}

type Parser = typeof(setmetatable({} :: {
    Lexer: Lexer,

    TreeNodes: { TreeNode },

    DidAvance: boolean
}, {} :: typeof(Parser)))

local function NewTreeNode(parent: TreeNode): TreeNode
    return {
        Attributes = {},
        Properties = {},
        Derives = {},
        Rules = {},
        Parent = parent
    }
end

local function ParseScopeClose(parser: Parser, token: Token, currTreeNode: TreeNode): (Token?, TreeNode)
    if token.Kind ~= "ScopeClose" then return token, currTreeNode end

    return parser:Advance(), currTreeNode.Parent
end

local function ParseScopeOpen(
    parser: Parser, token: Token, currTreeNode: TreeNode, selector: string?
): (Token?, TreeNode)
    if token.Kind ~= "ScopeOpen" then return token, currTreeNode end

    local prevTreeNode = currTreeNode
    local currTreeNode = NewTreeNode(prevTreeNode)
    currTreeNode.Selector = selector

    table.insert(prevTreeNode.Rules, currTreeNode)

    return parser:Advance(), currTreeNode
end

local function ParseScopeDelimiter(parser: Parser, token: Token, delimToken: Token?): (Token?, Token?)
    if token.Kind == "Comma" then
        local nextToken = parser:Advance()
        if not nextToken then return nil, delimToken end

        return ParseScopeDelimiter(parser, nextToken, token)

    else
        return token, delimToken
    end
end

local Selector = {}
Selector.__index = Selector

function Selector.new(token: Token)
    return setmetatable({
        Content = token.Value,
        CurrentToken = token
    }, Selector)
end

function Selector.Append(self: Selector, token: Token)
    local tokenKind: TokenKind, currentTokenKind: TokenKind = token.Kind, self.CurrentToken.Kind

    local shouldAddSpace = tokenKind ~= "Comma" and tokenKind ~= "StateOrEnumIdentifier" and (
        currentTokenKind == "ScopeToDescendants" or currentTokenKind == "ScopeToChildren" or
        currentTokenKind == "Text" or currentTokenKind == "Comma"
    )

    self.CurrentToken = token

    self.Content ..= if shouldAddSpace then ` {token.Value}` else token.Value
end

type Selector = typeof(setmetatable({} :: {
    Content: string,
    CurrentToken: Token
}, {} :: typeof(Selector)))

local function ParseScopeSelectorStart(
    parser: Parser, token: Token, currTreeNode: TreeNode
): (Token?, TreeNode)
    -- The `Token::Text(_)` case is handled in `ParseProperty`.
    local tokenKind: TokenKind = token.Kind
    if not (
        tokenKind == "NameIdentifier" or tokenKind == "PsuedoIdentifier" or
        tokenKind == "StateOrEnumIdentifier" or tokenKind == "TagOrEnumIdentifier" or
        tokenKind == "ScopeToDescendants" or tokenKind == "ScopeToChildren"
    ) then
        return token, currTreeNode
    end

    local selector = Selector.new(token)

    local token = parser:Advance()
    if not token then return nil, currTreeNode end

    return ParseScopeSelector(parser, token, currTreeNode, selector)
end

function ParseScopeSelector(
    parser: Parser, token: Token, currTreeNode: TreeNode, selector: Selector
): (Token?, TreeNode)
    while true do
        -- Advances the parser until no delimiter token is found.
        local _token, delimToken = ParseScopeDelimiter(parser, token)
        if not _token then return nil, currTreeNode end
        token = _token

        local tokenKind: TokenKind = token.Kind

        if (
            tokenKind == "NameIdentifier" or tokenKind == "PsuedoIdentifier" or
            tokenKind == "StateOrEnumIdentifier" or tokenKind == "TagOrEnumIdentifier" or
            tokenKind == "ScopeToDescendants" or tokenKind == "ScopeToChildren" or
            tokenKind == "Text"
        )  then
            -- Appending the comma token here ensures selectors can't end with delimiters.
            if delimToken ~= nil then
                selector:Append(delimToken)
            end

            selector:Append(token)

            local _token = parser:Advance()
            if not _token then return nil, currTreeNode end
            token = _token

        else
            break
        end
    end

    return ParseScopeOpen(parser, token, currTreeNode, selector.Content)
end

local function ParseEnumTokens(tokenHistory: { Token }): EnumItem | true
    local tokenHistoryLength = #tokenHistory
    local ok, enum
    local firstStopIdx = 0

    for idx = 1, tokenHistoryLength do
        local token = tokenHistory[idx]
        if token.Kind ~= "Text" then continue end

        ok, enum = pcall(function() return Enum[token.Value] end)
        if not ok then return true end

        firstStopIdx = idx
        break
    end

    for idx = firstStopIdx + 1, #tokenHistory do
        local token = tokenHistory[idx]
        if token.Kind ~= "Text" then continue end

        ok, enum = pcall(function() return enum[token.Value] end)
        if not ok then return true end
        break
    end

    return enum :: EnumItem
end

local function ParseFullEnum(
    parser: Parser, token: Token, tokenHistory: { Token }
): (Token?, (EnumItem | true)?)
    local tokenKind: TokenKind = token.Kind
    if tokenKind == "TagOrEnumIdentifier" or tokenKind == "StateOrEnumIdentifier" or tokenKind == "Text" then
        local token = parser:Advance()

        if token then
            table.insert(tokenHistory, token)

            local token, datatype = ParseFullEnum(parser, token, tokenHistory)
            if datatype ~= nil then return token, (datatype :: any) :: (EnumItem | true)? end
        end
    end

    if #tokenHistory == 0 then return token, nil end

    return token, ParseEnumTokens(tokenHistory)
end

local function ParseEnumKeyword(parser: Parser, token: Token): (Token?, (EnumItem | true)?)
    if token.Kind ~= "EnumKeyword" then return token, nil end

    local token = parser:Advance()
    if not token then return end

    local tokenHistory = {}
    return ParseFullEnum(parser, token, tokenHistory)
end

local SHORTHAND_HARDCODES = {
    FlexMode = "UIFlexMode",
    HorizontalFlex = "UIFlexAlignment",
    VerticalFlex = "UIFlexAlignment"
}

local function ParseEnumShorthand(parser: Parser, token: Token, key: string?): (Token?, (EnumItem | true | IncompleteEnumShorthand)?)
    if token.Kind ~= "StateOrEnumIdentifier" then return token, nil end

    local token = parser:Advance()
    if not token then return end

    if token.Kind ~= "Text" then return token, nil end
    local enumItem = token.Value

    if not key then return parser:Advance(), NewIncompleteEnumShorthand(enumItem) end

    local enumName = SHORTHAND_HARDCODES[key] or key
    local ok, enum = pcall(function() return Enum[enumName] end)
    if not ok then return token, true end

    local ok, enum = pcall(function() return enum[enumItem] end)
    if not ok then return token, true end

    return parser:Advance(), enum
end

local function ParseEnumDatatype(parser: Parser, token: Token, key: string?): (Token?, (EnumItem | true)?)
    local _token, datatype = ParseEnumKeyword(parser, token)
    if datatype ~= nil then return _token, (datatype :: any) :: (EnumItem | true)? end

    local _token, datatype = ParseEnumShorthand(parser, token, key)
    if datatype ~= nil then return _token, (datatype :: any) :: (EnumItem | true)? end

    return token, nil
end

local function ParseNumberScale(parser: Parser, token: Token, num: number): (Token?, UDim?)
    if token.Kind ~= "ScaleOrOpMod" then return token, nil end

    return parser:Advance(), UDim.new(num / 100, 0)
end

local function ParseNumberOffset(parser: Parser, token: Token, num: number): (Token?, UDim?)
    if token.Kind ~= "Offset" then return token, nil end

    return parser:Advance(), UDim.new(0, num)
end

local function ParseNumberDatatype(parser: Parser, token: Token): (Token?, (number | UDim)?)
    if token.Kind ~= "Number" then return token, nil end
    local num = tonumber(token.Value) :: number

    local token = parser:Advance()
    if token then
        local _token, datatype = ParseNumberOffset(parser, token, num)
        if datatype ~= nil then return _token, datatype end

        local _token, datatype = ParseNumberScale(parser, token, num)
        if datatype ~= nil then return _token, datatype end
    end

    return token, num
end

local function ParseStringMultiEnd(parser: Parser, startEqualsAmount: number, tokenHistory: { Token }): Token?
    -- We keep advancing tokens until we find a closing multiline string
    -- token with the same amount of equals signs as the start token.
    while true do
        local token = parser.Lexer()
        if not token then return end

        if token.Kind == "StringMultiEnd" then
            local endTokenValue = token.Value
            local endEqualsAmount = #string.sub(endTokenValue, 2, -2)

            if startEqualsAmount == endEqualsAmount then
                return parser:CoreAdvance()
            end
        end

        table.insert(tokenHistory, token)
    end
end

local function ParseStringMultiDatatype(parser: Parser, token: Token): (Token?, string?)
    if token.Kind ~= "StringMultiStart" then return token, nil end

    local tokenValue = token.Value
    local startEqualsAmount = #string.sub(tokenValue, 4, -2)

    local tokenHistory = {}
    local token = ParseStringMultiEnd(parser, startEqualsAmount, tokenHistory)

    -- Joins the string tokens together ignoring the opening and closing tokens.
    local str = ""
    for _, token in tokenHistory do
        str ..= token.Value
    end

    -- Luau strips multiline strings up until the first occurance of a newline character.
    -- So we will mimic this behaviour.
    str = string.gsub(str, "^[ \t\f]*\n+", "")

    return token, str
end

local function ParseStringSingleDatatype(parser: Parser, token: Token): (Token?, string?)
    local tokenKind: TokenKind = token.Kind

    if tokenKind == "StringSingle" then
        return parser:Advance(), string.sub(token.Value, 2, -2)

    elseif tokenKind == "RobloxAsset" then
        return parser:Advance(), token.Value
    end

    return token, nil
end

local function ParseStringDatatype(parser: Parser, token: Token): (Token?, string?)
    local _token, datatype = ParseStringSingleDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local token, datatype = ParseStringMultiDatatype(parser, token)
    if datatype ~= nil then return token, datatype end

    return token, nil
end

local function ParsePredefinedColor(
    parser: Parser, token: Token,
    kind: TokenKind, colorsTable: { [string]: Color3 }
): (Token?, Color3?)
    if token.Kind ~= kind then return end
    return parser:Advance(), colorsTable[string.lower(token.Value)]
end

local function ParsePredefinedColorDatatype(parser: Parser, token: Token): (Token?, Color3?)
    local _token, datatype = ParsePredefinedColor(parser, token, "ColorTailwind", ColorsTailwind)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParsePredefinedColor(parser, token, "ColorCss", ColorsCss)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParsePredefinedColor(parser, token, "ColorBrick", ColorsBrick)
    if datatype ~= nil then return _token, datatype end

    return token, nil
end

local function ParseHexColorDatatype(parser: Parser, token: Token): (Token?, Color3?)
    if token.Kind ~= "ColorHex" then return token, nil end

    return parser:Advance(), Color3.fromHex(token.Value)
end

local function ParseColorDatatype(parser: Parser, token: Token): (Token?, Color3?)
    local _token, datatype = ParsePredefinedColorDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseHexColorDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    return token, nil
end

local function ParseNilDatatype(parser: Parser, token: Token): (Token?, DatatypeNone?)
    local tokenKind: TokenKind = token.Kind
    if tokenKind ~= "Nil" then return token, nil end

    return parser:Advance(), DATATYPE_NONE
end

local function ParseBooleanDatatype(parser: Parser, token: Token): (Token?, boolean?)
    local tokenKind: TokenKind = token.Kind

    if tokenKind == "BoolTrue" then return parser:Advance(), true end
    if tokenKind == "BoolFalse" then return parser:Advance(), false end

    return token, nil
end

local function ParseAttributeNameDatatype(parser: Parser, token: Token): (Token?, string?)
    if token.Kind ~= "AttributeIdentifier" then return token end

    local token = parser:Advance()
    if not token then return end

    if token.Kind ~= "Text" then return token, nil end

    return parser:Advance(), token.Value
end

local function DefaultIfNotVector3Any(target: any, default: Vector3): Vector3
    local typeofTarget = typeof(target)
    return if typeofTarget ~= "Vector3" and typeofTarget ~= "Vector3int16" then default else target
end

local function DefaultIfNotVector2Any(target: any, default: Vector2): Vector2
    local typeofTarget = typeof(target)
    return if typeofTarget ~= "Vector2" and typeofTarget ~= "Vector2int16" then default else target
end

local function DefaultIfNotType<T>(target: T, expectedType: string, default: T): T
    return if type(target) ~= expectedType then default else target
end

local function CoerceToUDim<T>(target: T, default: UDim): UDim
    local targetTypeof = typeof(target)
    if targetTypeof == "number" then return UDim.new((target :: any) :: number, 0) end
    if targetTypeof == "UDim" then return (target :: any) :: UDim end
    return default
end

export type TupleData = { Datatype | { TupleData } }

export type Tuple = {
    Name: string?,
    Parent: Tuple?,
    Data: TupleData,
}

local function NewTuple(name: string?, parent: Tuple?)
    return {
        Name = name,
        Parent = parent or nil,
        Data = {}
    }
end

type TupleAnnotationFunction<T = Datatype> = (data: { Datatype }) -> T

type TupleAnnotationFunctions = {
    udim: TupleAnnotationFunction<UDim>,
    udim2: TupleAnnotationFunction<UDim2>,
    vec2: TupleAnnotationFunction<Vector2>,
    vec3: TupleAnnotationFunction<Vector3>,
    rect: TupleAnnotationFunction<Rect>,
    color3: TupleAnnotationFunction<Color3>,
    rgb: TupleAnnotationFunction<Color3>,
    colorseq: TupleAnnotationFunction<ColorSequence>,
    numseq: TupleAnnotationFunction<NumberSequence>,
    font: TupleAnnotationFunction<Font>,
    content: TupleAnnotationFunction<Content>,
    numrange: TupleAnnotationFunction<NumberRange>,
}

local TupleAnnotations: TupleAnnotationFunctions = {
    udim = function(data)
        local scale = DefaultIfNotType(data[1], "number", 0) :: number
        local offset = DefaultIfNotType(data[2], "number", scale * 100) :: number

        return UDim.new(scale, offset)
    end,

    udim2 = function(data)
        if #data <= 2 then
            local xComponent = CoerceToUDim(data[1], UDim.new())
            local yComponent = CoerceToUDim(data[2], xComponent)
            return UDim2.new(xComponent, yComponent)

        else
            local xScale = DefaultIfNotType(data[1], "number", 0) :: number
            local xOffset = DefaultIfNotType(data[2], "number", xScale * 100) :: number
            local yScale = DefaultIfNotType(data[3], "number", xScale) :: number
            local yOffset = DefaultIfNotType(data[4], "number", yScale * 100) :: number

            return UDim2.new(xScale, xOffset, yScale, yOffset)
        end
    end,

    vec2 = function(data)
        local xComponent = DefaultIfNotType(data[1], "number", 0) :: number
        local yComponent = DefaultIfNotType(data[2], "number", xComponent) :: number

        return Vector2.new(xComponent, yComponent)
    end,

    vec2i16 = function(data)
        local xComponent = DefaultIfNotType(data[1], "number", 0) :: number
        local yComponent = DefaultIfNotType(data[2], "number", xComponent) :: number

        return Vector2int16.new(xComponent, yComponent)
    end,

    vec3 = function(data)
        local xComponent = DefaultIfNotType(data[1], "number", 0) :: number
        local yComponent = DefaultIfNotType(data[2], "number", xComponent) :: number
        local zComponent = DefaultIfNotType(data[3], "number", yComponent) :: number

        return (vector.create(xComponent, yComponent, zComponent) :: any) :: Vector3
    end,

    vec3i16 = function(data)
        local xComponent = DefaultIfNotType(data[1], "number", 0) :: number
        local yComponent = DefaultIfNotType(data[2], "number", xComponent) :: number
        local zComponent = DefaultIfNotType(data[3], "number", yComponent) :: number

        return Vector3int16.new(xComponent, yComponent, zComponent)
    end,

    cframe = function(data)
        local first = data[1]

        if type(first) == "number" then
            local posXComponent = first
            local posYComponent = DefaultIfNotType(data[2], "number", posXComponent)
            local posZComponent = DefaultIfNotType(data[3], "number", posYComponent)

            local orienXXComponent = DefaultIfNotType(data[4], "number", 0)
            local orienXYComponent = DefaultIfNotType(data[5], "number", orienXXComponent)
            local orienXZComponent = DefaultIfNotType(data[6], "number", orienXYComponent)

            local orienYXComponent = DefaultIfNotType(data[7], "number", 0)
            local orienYYComponent = DefaultIfNotType(data[8], "number", orienYXComponent)
            local orienYZComponent = DefaultIfNotType(data[9], "number", orienYYComponent)

            local orienZXComponent = DefaultIfNotType(data[10], "number", 0)
            local orienZYComponent = DefaultIfNotType(data[11], "number", orienZXComponent)
            local orienZZComponent = DefaultIfNotType(data[12], "number", orienZYComponent)

            return CFrame.new(
                posXComponent, posYComponent, posZComponent,
                orienXXComponent, orienXYComponent, orienXZComponent,
                orienYXComponent, orienYYComponent, orienYZComponent,
                orienZXComponent, orienZYComponent, orienZZComponent
            )

        else
            local posComponent = DefaultIfNotVector3Any(data[1], Vector3.zero)
            local orienXComponent = DefaultIfNotVector3Any(data[2], Vector3.zero)
            local orienYComponent = DefaultIfNotVector3Any(data[3], orienXComponent)
            local orienZComponent = DefaultIfNotVector3Any(data[4], orienYComponent)

            return CFrame.new(
                posComponent.X, posComponent.Y, posComponent.Z,
                orienXComponent.X, orienXComponent.Y, orienXComponent.Z,
                orienYComponent.X, orienYComponent.Y, orienYComponent.Z,
                orienZComponent.X, orienZComponent.Y, orienZComponent.Z
            )
        end
    end,

    rect = function(data)
        local first = data[1]

        if type(first) == "number" then
            local minY = DefaultIfNotType(data[2], "number", first) :: number
            local maxX = DefaultIfNotType(data[3], "number", first) :: number
            local maxY = DefaultIfNotType(data[4], "number", minY) :: number

            return Rect.new(first, minY, maxX, maxY)

        else
            local min = DefaultIfNotVector2Any(data[1], Vector2.zero)
            local max = DefaultIfNotVector2Any(data[2], min)

            return Rect.new(min, max)
        end
    end,

    color3 = function(data)
        local first = data[1]

        if typeof(first) == "BrickColor" then
            return (first :: BrickColor).Color

        else
            local red = DefaultIfNotType(first, "number", 0) :: number
            local green = DefaultIfNotType(data[2], "number", red) :: number
            local blue = DefaultIfNotType(data[3], "number", green) :: number

            return Color3.new(red, green, blue)
        end
    end,

    rgb = function(data)
        local red = DefaultIfNotType(data[1], "number", 0) :: number
        local green = DefaultIfNotType(data[2], "number", red)  :: number
        local blue = DefaultIfNotType(data[3], "number", green) :: number

        return Color3.fromRGB(red, green, blue)
    end,

    brickcolor = function(data)
        return BrickColor.new(DefaultIfNotType(data[1], "string", "Black") :: any)
    end,

    colorseq = ColorSeqTupleAnnotation :: TupleAnnotationFunction<ColorSequence>,

    numseq = NumSeqTupleAnnotation :: TupleAnnotationFunction<NumberSequence>,

    font = function(data)
        local fontFamily = data[1]
        local fontFamilyType = type(fontFamily)

        if fontFamilyType == "number" then
            fontFamily = `rbxassetid://{fontFamily}`

        elseif fontFamilyType == "string" then
            if not string.match(fontFamily :: string, "^rbxasset://") then
                fontFamily = `rbxasset://fonts/families/{fontFamily}.json`
            end
        else

            fontFamily = "rbxasset://fonts/families/SourceSansPro.json"
        end

        local rawFontWeight = data[2]
        if type(rawFontWeight) == "table" and (rawFontWeight :: any).Kind == "IncompleteEnumShorthand" then
            rawFontWeight = (rawFontWeight :: IncompleteEnumShorthand).Value
        end
        local rawFontWeightTypeOf = typeof(rawFontWeight)

        local fontWeight =
            if rawFontWeightTypeOf == "string" then
                if rawFontWeight == "Thin" then Enum.FontWeight.Thin
                elseif rawFontWeight == "ExtraLight" then Enum.FontWeight.ExtraLight
                elseif rawFontWeight == "Light" then Enum.FontWeight.Light
                elseif rawFontWeight == "Regular" then Enum.FontWeight.Regular
                elseif rawFontWeight == "Medium" then Enum.FontWeight.Medium
                elseif rawFontWeight == "SemiBold" then Enum.FontWeight.SemiBold
                elseif rawFontWeight == "Bold" then Enum.FontWeight.Bold
                elseif rawFontWeight == "ExtraBold" then Enum.FontWeight.ExtraBold
                elseif rawFontWeight == "Heavy" then Enum.FontWeight.Heavy
                elseif rawFontWeight == "Enum.FontWeight.Thin" then Enum.FontWeight.Thin
                elseif rawFontWeight == "Enum.FontWeight.ExtraLight" then Enum.FontWeight.ExtraLight
                elseif rawFontWeight == "Enum.FontWeight.Light" then Enum.FontWeight.Light
                elseif rawFontWeight == "Enum.FontWeight.Regular" then Enum.FontWeight.Regular
                elseif rawFontWeight == "Enum.FontWeight.Medium" then Enum.FontWeight.Medium
                elseif rawFontWeight == "Enum.FontWeight.SemiBold" then Enum.FontWeight.SemiBold
                elseif rawFontWeight == "Enum.FontWeight.Bold" then Enum.FontWeight.Bold
                elseif rawFontWeight == "Enum.FontWeight.ExtraBold" then Enum.FontWeight.ExtraBold
                elseif rawFontWeight == "Enum.FontWeight.Heavy" then Enum.FontWeight.Heavy
                else Enum.FontWeight.Regular
            elseif rawFontWeightTypeOf == "EnumItem" and (rawFontWeight :: EnumItem).EnumType == Enum.FontWeight then
                rawFontWeight :: Enum.FontWeight
            elseif rawFontWeightTypeOf == "number" then
                if rawFontWeight == 100 then Enum.FontWeight.Thin
                elseif rawFontWeight == 200 then Enum.FontWeight.ExtraLight
                elseif rawFontWeight == 300 then Enum.FontWeight.Light
                elseif rawFontWeight == 400 then Enum.FontWeight.Regular
                elseif rawFontWeight == 500 then Enum.FontWeight.Medium
                elseif rawFontWeight == 600 then Enum.FontWeight.SemiBold
                elseif rawFontWeight == 700 then Enum.FontWeight.Bold
                elseif rawFontWeight == 800 then Enum.FontWeight.ExtraBold
                elseif rawFontWeight == 900 then Enum.FontWeight.Heavy
                else Enum.FontWeight.Regular
            else Enum.FontWeight.Regular

        local rawFontStyle = data[3]
        if type(rawFontStyle) == "table" and (rawFontStyle :: any).Kind == "IncompleteEnumShorthand" then
            rawFontStyle = (rawFontStyle :: IncompleteEnumShorthand).Value
        end
        local rawFontStyleTypeOf = typeof(rawFontStyle)

        local fontStyle =
            if rawFontStyleTypeOf == "string" then
                if rawFontStyle == "Normal" then Enum.FontStyle.Normal
                elseif rawFontStyle == "Italic" then Enum.FontStyle.Italic
                elseif rawFontStyle == "Enum.FontStyle.Italic" then Enum.FontStyle.Italic
                else Enum.FontStyle.Normal
            elseif rawFontStyleTypeOf == "EnumItem" and (rawFontStyle :: EnumItem).EnumType == Enum.FontStyle then
                rawFontStyle :: Enum.FontStyle
            elseif rawFontStyleTypeOf == "number" then
                if rawFontStyle == 1 then Enum.FontStyle.Italic
                else Enum.FontStyle.Normal
            else Enum.FontStyle.Normal

        return Font.new(fontFamily :: string, fontWeight, fontStyle)
    end,

    content = function(data)
        local content = data[1]
        if not content then return Content.none end

        local contentType = type(content)
        return
            if contentType == "number" then Content.fromAssetId(content :: number)
            elseif contentType == "string" then Content.fromUri(content :: string)
            else Content.none
    end,

    numrange = function(data)
        local min = DefaultIfNotType(data[1], "number", 0) :: number
        local max = DefaultIfNotType(data[2], "number", min) :: number

        return NumberRange.new(min, max)
    end,

    lerp = LerpAnnotation,
    floor = FloorAnnotation,
    ceil = CeilAnnotation,
    round = RoundAnnotation
}

local function CoerceTupleToDatatype(tuple: Tuple): Datatype?
    local tupleName, tupleData = tuple.Name, tuple.Data

    if tupleName then
        local annotationFunction = TupleAnnotations[string.lower(tupleName)]
        if annotationFunction then return annotationFunction(tupleData) end
    end

    local tupleDataLength = #tupleData

    return
        if tupleDataLength == 0 then DATATYPE_NONE
        -- If the tuple only has one item then it can be simplified to just that item.
        elseif tupleDataLength == 1 then tupleData[1]
        else tupleData
end

local function ParseTupleClose(
    parser: Parser, token: Token,
    currTuple: Tuple, rootTuple: Tuple
): Token?
    if token.Kind ~= "ParensClose" then return end

    local parentTuple = currTuple.Parent

    local token = parser:Advance()

    if parentTuple then
        local datatype = CoerceTupleToDatatype(currTuple)
        if datatype then table.insert(parentTuple.Data, datatype) end

        if not token then return end

        local _token = ParseTupleDelimiter(parser, token, parentTuple, rootTuple)
        if _token then return _token end

        local _token = ParseTupleClose(parser, token, parentTuple, rootTuple)
        if _token then return _token end

        local _token, _tuple = ParseTupleName(parser, token, parentTuple, rootTuple)
        if _tuple then return _token end
    end

    return token
end

function ParseTupleDelimiter(
    parser: Parser, token: Token,
    currTuple: Tuple, rootTuple: Tuple
): Token?
    local tokenKind = token.Kind
    if tokenKind ~= "SemiColon" and tokenKind ~= "Comma" then return end

    local token = parser:Advance()
    if not token then return end

    -- Handles cases of multiple delimiter tokens next to each other.
    local _token = ParseTupleDelimiter(parser, token, currTuple, rootTuple)
    if _token then return _token end

    local _token = ParseTupleClose(parser, token, currTuple, rootTuple)
    if _token then return _token end

    local _token, _tuple = ParseTupleName(parser, token, currTuple, rootTuple)
    if _tuple then return _token end

    local _token = ParseTupleDatatype(parser, token, currTuple, rootTuple)
    if _token then return _token end

    return token
end

function ParseTupleDatatype(
    parser: Parser, token: Token,
    currTuple: Tuple, rootTuple: Tuple
): Token?
    local token, datatype = ParseDatatypeGroup(parser, token, nil)
    if datatype == nil then return end

    table.insert(currTuple.Data, datatype)

    if not token then return end

	local _token = ParseTupleDelimiter(parser, token, currTuple, rootTuple)
	if _token then return _token end

    local _token = ParseTupleClose(parser, token, currTuple, rootTuple)
    if _token then return _token end

    local _token, _tuple = ParseTupleName(parser, token, currTuple, rootTuple)
    if _tuple then return _token end

    return token
end

local function ParseTupleOpen(
    parser: Parser, token: Token, tupleName: string?,
    parentTuple: Tuple?, rootTuple: Tuple?
): (Token?, Tuple?)
    if token.Kind ~= "ParensOpen" then return end

    local currTuple = NewTuple(tupleName, parentTuple) :: Tuple & boolean

    local token = parser:Advance()
    if not token then return currTuple end

    local rootTuple = rootTuple or currTuple

    local _token = ParseTupleDelimiter(parser, token, currTuple, rootTuple)
    if _token then return _token, currTuple end

    local _token = ParseTupleClose(parser, token, currTuple, rootTuple)
    if _token then return _token, currTuple end

    local _token, _tuple = ParseTupleName(parser, token, currTuple, rootTuple)
    if _tuple then return _token, currTuple end

    local _token = ParseTupleDatatype(parser, token, currTuple, rootTuple)
    if _token then return _token, currTuple end

    return token, currTuple
end

function ParseTupleName(
    parser: Parser, token: Token,
    parentTuple: Tuple?, rootTuple: Tuple?
): (Token?, Tuple?)
    if token.Kind ~= "Text" then
        return ParseTupleOpen(parser, token, nil, parentTuple, rootTuple)
    end

    local tupleName = token.Value

    local token = parser:Advance()
    if not token then return end

    return ParseTupleOpen(parser, token, tupleName, parentTuple, rootTuple)
end

local function ParseOperatorDatatype(parser: Parser, token: Token): (Token?, OperatorSymbol?)
    local operator = OperatorSymbols[token.Kind]
    if not operator then return token, nil end

    return parser:Advance(), operator
end

function ParseDatatype(
    parser: Parser, token: Token, key: string?
): (Token?, Datatype?)
    local _token, datatype = ParseTupleName(parser, token)
    if datatype ~= nil then return _token, CoerceTupleToDatatype(datatype) end

    local _token, datatype = ParseStringDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseNumberDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseOperatorDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseEnumDatatype(parser, token, key)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseColorDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseBooleanDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseNilDatatype(parser, token)
    if datatype ~= nil then return _token, datatype end

    local _token, datatype = ParseAttributeNameDatatype(parser, token)
    if datatype ~= nil then return _token, `${datatype}` end

    return token, nil
end

local function DatatypeIsOperator(datatype: Datatype)
    return type(datatype) == "table" and (datatype :: any).Kind == "Operator"
end

local function CanMergeOperators(left: OperatorSymbol, right: OperatorSymbol)
    local leftOp: OperatorKind, rightOp: OperatorKind = left.OperatorKind, right.OperatorKind

    local leftOpIsAddOrSub = leftOp == "OpAdd" or leftOp == "OpSub"
    local rightOpIsAddOrSub = rightOp == "OpAdd" or rightOp == "OpSub"

    if (leftOpIsAddOrSub and not rightOpIsAddOrSub) or (rightOpIsAddOrSub and not leftOpIsAddOrSub) then
        return false
    end

    return true
end

local function MergeOperators(left: OperatorSymbol, right: OperatorSymbol)
    local leftOp: OperatorKind, rightOp: OperatorKind = left.OperatorKind, right.OperatorKind

    return
        if leftOp == "OpSub" and rightOp == "OpAdd" then OperatorSubSymbol
        elseif leftOp == "OpSub" and rightOp == "OpSub" then OperatorAddSymbol
        else right
end

local function FindOperatorsInDatatypes(
    datatypes: { Datatype },
    operatorGroup: {OperatorGroupItem}
): {{ number | OperatorGroupItem }}
    local indexes: {{ number | OperatorGroupItem }} = {}

    for idx, datatype in datatypes do
        if not DatatypeIsOperator(datatype) then continue end

        for _, operator in operatorGroup do
            if operator.Symbol ~= datatype then continue end

            table.insert(indexes, { idx, operator } :: { number | OperatorGroupItem })
            break
        end
    end

    return indexes
end

type OperatorGroupItem = {
    Symbol: OperatorSymbol,
    Fn: OperatorFn
}
type OperatorFn = (left: number, right: number) -> number

local ADD_SUB_OPERATORS = {
    {
        Symbol = OperatorAddSymbol,
        Fn = function(left: number, right: number) return left + right end
    },
    {
        Symbol = OperatorSubSymbol,
        Fn = function(left: number, right: number) return left - right end
    },
}

local ORDERED_OPERATORS: {{ OperatorGroupItem }} = {
    {
        {
            Symbol = OperatorPowSymbol,
            Fn = function(left: number, right: number) return left ^ right end
        }
    },

    {
        {
            Symbol = OperatorDivSymbol,
            Fn = function(left: number, right: number) return left / right end
        },
        {
            Symbol = OperatorFloorDivSymbol,
            Fn = function(left: number, right: number) return left // right end
        },
        {
            Symbol = OperatorModSymbol,
            Fn = function(left: number, right: number) return left % right end
        },
        {
            Symbol = OperatorMultSymbol,
            Fn = function(left: number, right: number) return left * right end
        },
    },

    ADD_SUB_OPERATORS
}

type OperationFn = (left: any, right: any, operationFn: OperatorFn, operator: OperatorSymbol) -> Datatype

type Operations = {
    [string]: {
        [string]: OperationFn
    }
}

local OperationNumberWithUDim: OperationFn = function(
    left: number, right: UDim, operationFn, operator
)
    if operator.OperatorKind == "OpSub" then
        return UDim.new(left - right.Scale, -right.Offset)
    end

    return UDim.new(operationFn(left, right.Scale),  right.Offset)
end

local DatatypeOperations: Operations = {
    ["number"] = {
        ["number"] = function(left: number, right: number, operationFn)
            return operationFn(left, right)
        end,

        ["UDim"] = OperationNumberWithUDim,

        ["UDim2"] = function(
            left: number, right: UDim2, operationFn, operator
        ): UDim2
            local rightX, rightY = right.X, right.Y

            local solvedRightX = OperationNumberWithUDim(left, rightX, operationFn, operator) :: UDim
            local solvedRightY = OperationNumberWithUDim(left, rightY, operationFn, operator) :: UDim

            return UDim2.new(
                solvedRightX, solvedRightY
            )
        end,

        ["Vector3"] = function(left: number, right: Vector3, operationFn): Vector3
            return Vector3.new(
                operationFn(left, right.X),
                operationFn(left, right.Y),
                operationFn(left, right.Z)
            )
        end,

        ["Vector3int16"] = function(left: number, right: Vector3int16, operationFn): Vector3int16
            return Vector3int16.new(
                operationFn(left, right.X),
                operationFn(left, right.Y),
                operationFn(left, right.Z)
            )
        end,

        ["Vector2"] = function(left: number, right: Vector2, operationFn): Vector2
            return Vector2.new(
                operationFn(left, right.X),
                operationFn(left, right.Y)
            )
        end,

        ["Vector2int16"] = function(left: number, right: Vector2int16, operationFn): Vector2int16
            return Vector2int16.new(
                operationFn(left, right.X),
                operationFn(left, right.Y)
            )
        end,

        ["Rect"] = function(left: number, right: Rect, operationFn): Rect
            local rightMin, rightMax = right.Min, right.Max

            return Rect.new(
                operationFn(left, rightMin.X), operationFn(left, rightMin.Y),
                operationFn(left, rightMax.X), operationFn(left, rightMax.Y)
            )
        end,

        ["Color3"] = function(left: number, right: Color3, operationFn): Color3
            return Color3.new(
                operationFn(left, right.R),
                operationFn(left, right.G),
                operationFn(left, right.B)
            )
        end
    },

    ["UDim"] = {
        ["number"] = function(left: UDim, right: number, operationFn): UDim
            return UDim.new(operationFn(left.Scale, right), left.Offset)
        end,

        ["UDim"] = function(left: UDim, right: UDim, operationFn): UDim
            return UDim.new(operationFn(left.Scale, right.Scale), operationFn(left.Offset, right.Offset))
        end
    },

    ["UDim2"] = {
        ["number"] = function(left: UDim2, right: number, operationFn): UDim2
            local leftX, leftY = left.X, left.Y

            return UDim2.new(
                operationFn(leftX.Scale, right), leftX.Offset,
                operationFn(leftY.Scale, right), leftY.Offset
            )
        end,

        ["UDim2"] = function(left: UDim2, right: UDim2, operationFn): UDim2
            local leftX, leftY, rightX, rightY = left.X, left.Y, right.X, right.Y

            return UDim2.new(
                operationFn(leftX.Scale, rightX.Scale), operationFn(leftX.Offset, rightX.Offset),
                operationFn(leftY.Scale, rightY.Scale), operationFn(leftY.Offset, rightY.Offset)
            )
        end
    },

    ["Vector3"] = {
        ["number"] = function(left: Vector3, right: number, operationFn): Vector3
            return Vector3.new(
                operationFn(left.X, right),
                operationFn(left.Y, right),
                operationFn(left.Z, right)
            )
        end,

        ["Vector3int16"] = function(left: Vector3, right: Vector3int16, operationFn): Vector3
            return Vector3.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y),
                operationFn(left.Z, right.Z)
            )
        end,

        ["Vector3"] = function(left: Vector3, right: Vector3, operationFn): Vector3
            return Vector3.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y),
                operationFn(left.Z, right.Z)
            )
        end,
    },

    ["Vector3int16"] = {
        ["number"] = function(left: Vector3int16, right: number, operationFn): Vector3int16
            return Vector3int16.new(
                operationFn(left.X, right),
                operationFn(left.Y, right),
                operationFn(left.Z, right)
            )
        end,

        ["Vector3"] = function(left: Vector3int16, right: Vector3, operationFn): Vector3int16
            return Vector3int16.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y),
                operationFn(left.Z, right.Z)
            )
        end,

        ["Vector3int16"] = function(left: Vector3int16, right: Vector3int16, operationFn): Vector3int16
            return Vector3int16.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y),
                operationFn(left.Z, right.Z)
            )
        end,
    },

    ["Vector2"] = {
        ["number"] = function(left: Vector2, right: number, operationFn): Vector2
            return Vector2.new(
                operationFn(left.X, right),
                operationFn(left.Y, right)
            )
        end,

        ["Vector2int16"] = function(left: Vector2, right: Vector2int16, operationFn): Vector2
            return Vector2.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y)
            )
        end,

        ["Vector2"] = function(left: Vector2, right: Vector2, operationFn): Vector2
            return Vector2.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y)
            )
        end,
    },

    ["Vector2int16"] = {
        ["number"] = function(left: Vector2int16, right: number, operationFn): Vector2int16
            return Vector2int16.new(
                operationFn(left.X, right),
                operationFn(left.Y, right)
            )
        end,

        ["Vector2"] = function(left: Vector2int16, right: Vector2, operationFn): Vector2int16
            return Vector2int16.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y)
            )
        end,

        ["Vector2int16"] = function(left: Vector2int16, right: Vector2int16, operationFn): Vector2int16
            return Vector2int16.new(
                operationFn(left.X, right.X),
                operationFn(left.Y, right.Y)
            )
        end,
    },

    ["Rect"] = {
        ["number"] = function(left: Rect, right: number, operationFn): Rect
            local leftMin, leftMax = left.Min, left.Max

            return Rect.new(
                operationFn(leftMin.X, right), operationFn(leftMin.Y, right),
                operationFn(leftMax.X, right), operationFn(leftMax.Y, right)
            )
        end,

        ["Rect"] = function(left: Rect, right: Rect, operationFn): Rect
            local leftMin, leftMax = right.Min, right.Max
            local rightMin, rightMax = right.Min, right.Max

            return Rect.new(
                operationFn(leftMin.X, rightMin.X), operationFn(leftMin.Y, rightMin.Y),
                operationFn(leftMax.X, rightMax.X), operationFn(leftMax.Y, rightMax.Y)
            )
        end
    },

    ["Color3"] = {
        ["number"] = function(left: Color3, right: number, operationFn): Color3
            return Color3.new(
                operationFn(left.R, right),
                operationFn(left.G, right),
                operationFn(left.B, right)
            )
        end,

        ["Color3"] = function(left: Color3, right: Color3, operationFn): Color3
            return Color3.new(
                operationFn(left.R, right.R),
                operationFn(left.G, right.G),
                operationFn(left.B, right.B)
            )
        end
    },
}

local function SolveDatatypeGroup(datatypes: { Datatype }): Datatype
    -- Merges Add and Sub operators with the datatype to the right if
    -- the datatype to the left isn't an operator.
    local occurrences = FindOperatorsInDatatypes(datatypes, ADD_SUB_OPERATORS)
    local occurrenceIdxOffset = 0
    for _, occurrence in occurrences do
        local occurrenceIdx = occurrence[1] :: number - occurrenceIdxOffset

        local rightIdx = occurrenceIdx + 1
        if rightIdx > #datatypes then continue end

        local canMerge: boolean
        if occurrenceIdx == 1 then
            canMerge = true

        else
            local left = datatypes[occurrenceIdx - 1] :: Datatype
            canMerge = if DatatypeIsOperator(left) then true else false
        end

        if canMerge then
            local right = table.remove(datatypes, rightIdx) :: Datatype
            occurrenceIdxOffset += 1

            local datatypeOperations = DatatypeOperations["number"]
            if not datatypeOperations then
                datatypes[occurrenceIdx] = right
                continue
            end

            local datatypeOperatorFn = datatypeOperations[typeof(right)]
            if not datatypeOperatorFn then
                datatypes[occurrenceIdx] = right
                continue
            end

            local operationData = occurrence[2] :: OperatorGroupItem
            local operationFn, operator = operationData.Fn, operationData.Symbol
            datatypes[occurrenceIdx] = datatypeOperatorFn(0, right, operationFn, operator)
        end
    end

    for _, operatorGroup in ORDERED_OPERATORS do
        local occurrences = FindOperatorsInDatatypes(datatypes, operatorGroup)
        local occurrenceIdxOffset = 0

        for _, occurrence in occurrences do
            local occurrenceIdx = occurrence[1] :: number - occurrenceIdxOffset

            local rightIdx = occurrenceIdx + 1
            if rightIdx > #datatypes then continue end

            local right = table.remove(datatypes, rightIdx) :: Datatype
            occurrenceIdxOffset += 1

            local left: Datatype, leftIdx: number
            if occurrenceIdx == 1 then
                left = 0
                leftIdx = 1

            else
                leftIdx = occurrenceIdx - 1
                left = table.remove(datatypes, leftIdx) :: Datatype
                if left == DATATYPE_NONE then left = 0 end
                occurrenceIdxOffset += 1
            end

            local datatypeOperations = DatatypeOperations[typeof(left)]
            if not datatypeOperations then
                datatypes[leftIdx] = left
                continue
            end

            local datatypeOperatorFn = datatypeOperations[typeof(right)]
            if not datatypeOperatorFn then
                datatypes[leftIdx] = left
                continue
            end

            local operationData = occurrence[2] :: OperatorGroupItem
            local operationFn, operator = operationData.Fn, operationData.Symbol
            datatypes[leftIdx] = datatypeOperatorFn(left, right, operationFn, operator)
        end
    end

    return datatypes[1]
end

local function ResolveDatatypeGroup(
    datatypes: { Datatype }?
): Datatype?
    if not datatypes then return end
    if #datatypes == 1 then return datatypes[1] end

    return SolveDatatypeGroup(datatypes)
end

local function DatatypesEnsureExistsThenInsert(datatypes: { Datatype }?, toInsert: Datatype)
    if not datatypes then
        datatypes = { toInsert } :: { Datatype }
    else
        table.insert(datatypes, toInsert)
    end

    return datatypes
end

function ParseDatatypeGroup(
    parser: Parser, token: Token, key: string?,
    datatypes: { Datatype }?, pendingOperator: OperatorSymbol?
): (Token?, Datatype?)
    local token, datatype = ParseDatatype(parser, token, key)
    if datatype == nil then
        return token, ResolveDatatypeGroup(datatypes)
    end

    local datatypeIsOperator = DatatypeIsOperator(datatype)

    -- If the datatype is an operator then we need to postpone adding it
    -- to the datatypes group so we can atomise it with the next datatype
    -- if it is an operator.
    if not datatypeIsOperator then
        if pendingOperator then
            -- We can add our pending operator to the datatypes table
            -- since it has no other operator to combine with.
            datatypes = DatatypesEnsureExistsThenInsert(datatypes, pendingOperator)
            pendingOperator = nil -- Operator is no longer pending anymore.

            table.insert(datatypes :: { Datatype }, datatype)

        else
            datatypes = DatatypesEnsureExistsThenInsert(datatypes, datatype)
        end
    end

    if not token then
        return token, ResolveDatatypeGroup(datatypes)
    end
    local tokenKind: TokenKind = token.Kind

    if tokenKind == "ParensClose" or tokenKind == "ScopeClose" or tokenKind == "Comma" or tokenKind == "SemiColon" then
        return token, ResolveDatatypeGroup(datatypes)
    else
        if datatypeIsOperator then
            -- Since our datatype was an operator we need to mark it as pending,
            -- atomising with the existing pending operator if it exists.

            if pendingOperator then
                if CanMergeOperators(pendingOperator, datatype :: OperatorSymbol) then
                    pendingOperator = MergeOperators(pendingOperator, datatype :: OperatorSymbol)
                else
                    datatypes = DatatypesEnsureExistsThenInsert(datatypes, pendingOperator)
                    pendingOperator = datatype :: OperatorSymbol
                end

            else
                pendingOperator = datatype :: OperatorSymbol
            end
        end

        return ParseDatatypeGroup(parser, token, key, datatypes, pendingOperator)
    end
end

local function ParseDelimiters(parser: Parser, token: Token): Token?
    local tokenKind: TokenKind = token.Kind
    if tokenKind ~= "SemiColon" and tokenKind ~= "Comma" then return token end

    local token = parser:Advance()
    if not token then return nil end

    return ParseDelimiters(parser, token)
end

export type FinalizedDatatype = string | boolean | EnumItem | Color3 | number | UDim | UDim2 | Vector3 | Rect | Font | Vector2 | Content | Vector2int16 | Vector3int16 | ColorSequence | NumberSequence

local function FinalizeDatatype(datatype: Datatype, key: string?): FinalizedDatatype?
    if type(datatype) == "table" then
        local kind = (datatype :: any).Kind

        if kind == "Operator" or kind == "None" then
            return nil

        elseif kind == "IncompleteEnumShorthand" then
            local ok, enum = pcall(function() return Enum[key] end)
            if not ok then return nil end

            local ok, enum = pcall(function() return enum[(datatype :: IncompleteEnumShorthand).Value] end)
            if not ok then return nil end

            return enum

        else
            return FinalizeDatatype((datatype :: TupleData)[1], key)
        end

    else
        return datatype
    end
end

local function ParseAttribute(parser: Parser, token: Token, currTreeNode: TreeNode): Token?
    if token.Kind ~= "AttributeIdentifier" then return token end

    local token = parser:Advance()
    if not token then return end

    if token.Kind ~= "Text" then return token end
    local attributeName = token.Value

    local token = parser:Advance()
    if not token then return end

    if token.Kind ~= "Equals" then return token end

    local token = parser:Advance()
    if not token then return end

    local token, datatype = ParseDatatypeGroup(parser, token, attributeName)
    if datatype ~= nil then
        local finalizedDatatype = FinalizeDatatype(datatype, attributeName)
        if finalizedDatatype then currTreeNode.Attributes[attributeName] = finalizedDatatype end
    end

    if not token then return nil end
    return ParseDelimiters(parser, token)
end

local function ParseProperty(parser: Parser, token: Token, currTreeNode: TreeNode): Token?
    if token.Kind ~= "Text" then return token end
    local initialToken = token

    local token = parser:Advance()
    if not token then return nil, currTreeNode end

    if token.Kind ~= "Equals" then
        return ParseScopeSelector(parser, token, currTreeNode, Selector.new(initialToken))
    end

    local token = parser:Advance()
    if not token then return nil end

    local propertyName = initialToken.Value

    local token, datatype = ParseDatatypeGroup(parser, token, propertyName)
    if datatype ~= nil then
        local finalizedDatatype = FinalizeDatatype(datatype, propertyName)
        if finalizedDatatype then currTreeNode.Properties[propertyName] = finalizedDatatype end
    end

    if not token then return nil end
    return ParseDelimiters(parser, token)
end

local function ParsePriorityDeclaration(parser: Parser, token: Token, currTreeNode: TreeNode): Token?
    if token.Kind ~= "PriorityDeclaration" then return token end

    local token = parser:Advance()
    if not token then return end

    local token, priorityValue = ParseDatatypeGroup(parser, token)
    if type(priorityValue) == "number" then
        currTreeNode.Priority = priorityValue
    end

    if not token then return end
    return ParseDelimiters(parser, token)
end

local function ParseNameDeclaration(parser: Parser, token: Token, currTreeNode: TreeNode): Token?
    if token.Kind ~= "NameDeclaration" then return token end

    local token = parser:Advance()
    if not token then return end

    local token, datatype = ParseDatatypeGroup(parser, token)
    if type(datatype) == "string" then
        if string.match(datatype, "%.rsml$") then
            datatype = string.sub(datatype, 1, #datatype - 5)
        end

        currTreeNode.Name = datatype
    end

    if not token then return end
    return ParseDelimiters(parser, token)
end

local function ParseDeriveDeclaration(parser: Parser, token: Token, currTreeNode: TreeNode): Token?
    if token.Kind ~= "DeriveDeclaration" then return token end

    local token = parser:Advance()
    if not token then return end

    local token, datatype = ParseDatatypeGroup(parser, token)
    local datatypeType = type(datatype)

    if datatypeType == "string" then
        currTreeNode.Derives[datatype :: string] = true

    elseif datatypeType == "table" then
        local derives = currTreeNode.Derives

        for _, datatype in datatype :: TupleData do
            if type(datatype) ~= "string" then continue end
            derives[datatype] = true
        end
    end

    return token
end

local function ParseRsml(lexer: Lexer): TreeNode
    local rootTreeNode = NewTreeNode(nil :: any)
    rootTreeNode.Parent = rootTreeNode

    local parser = Parser.new(lexer, rootTreeNode)

    local token: Token? = parser:Advance()
    if not token then return parser.CurrentTreeNode end

    local currTreeNode = parser.CurrentTreeNode

    while true do
        parser.DidAvance = false

        token = ParseAttribute(parser, token, currTreeNode)
        if not token then break end

        token = ParseProperty(parser, token, currTreeNode)
        if not token then break end

        token, currTreeNode = ParseScopeSelectorStart(parser, token, currTreeNode)
        if not token then break end

        token, currTreeNode = ParseScopeOpen(parser, token, currTreeNode)
        if not token then break end

        token, currTreeNode = ParseScopeClose(parser, token, currTreeNode)
        if not token then break end

        token = ParsePriorityDeclaration(parser, token, currTreeNode)
        if not token then break end

        token = ParseNameDeclaration(parser, token, currTreeNode)
        if not token then break end

        token = ParseDeriveDeclaration(parser, token, currTreeNode)
        if not token then break end

        -- Ensures the parser is advanced at least one time per iteration.
        -- This prevents infinite loops.
        if not parser.DidAvance then
            token = parser:Advance()
            if not token then break end
        end
    end

    return rootTreeNode
end

return ParseRsml


--!strict
--!optimize 2

local LexRsml = require(script.Lexer)
local ParseRsml = require(script.Parser)
local ConvertRsml = require(script.Converter)

local DefaultStyles = require(script.DefaultStyles)

return function(content: string, derives: { StyleSheet }?)
    return ConvertRsml(ParseRsml(LexRsml(content)), derives, DefaultStyles)
end


local Root = script.Parent["SymbolTest"]
local processServiceExists, ProcessService = pcall(function()
	return game:GetService("ProcessService")
end)

local Packages = Root.Packages
if not Packages or not Packages:FindFirstChild("Dev") then
	game:GetService("TestService"):Error("Invalid Package configuration. Try running `rotrieve install` to remedy.")
	ProcessService:ExitAsync(1)
end

local runCLI = require(Packages.Dev.Jest).runCLI

local status, result =
	runCLI(Packages["Symbol"], {}, { Packages["Symbol"] }):awaitStatus()

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

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

if processServiceExists then
	ProcessService:ExitAsync(1)
end


local Symbol = require(script.Parent.Symbol)

local GlobalRegistry: { [string]: Symbol.Symbol } = {}

return {
	getOrInit = function(name: string): Symbol.Symbol
		if GlobalRegistry[name] == nil then
			GlobalRegistry[name] = Symbol.new(name)
		end

		return GlobalRegistry[name]
	end,
	-- Used for testing
	__clear = function()
		GlobalRegistry = {}
	end,
}


--!strict
--[[
	A 'Symbol' is an opaque marker type, implemented to behave similarly to JS:
	https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
]]
local Symbol = require(script.Symbol)
export type Symbol = Symbol.Symbol
local GlobalRegistry = require(script["Registry.global"])

local SymbolObject = setmetatable({}, {
	--[[
		Creates a new symbol, using the given name when printed. Symbols are
		opaque, so this will always create a new, unique object
	]]
	__call = function(_, name: string?): Symbol.Symbol
		return Symbol.new(name)
	end,
})

SymbolObject.for_ = GlobalRegistry.getOrInit

return SymbolObject


return {
	displayName = "Symbol",
	testMatch = { "**/__tests__/**/*.spec" },
}


--Typed event with support for table-based payloads.
--!strict

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

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

local TypedEvent = {}
TypedEvent.LastArguments = {} :: {[string]: any}
TypedEvent.QueuedClearArguments = {} :: {[string]: boolean}
TypedEvent.__index = TypedEvent

export type TypedEvent<T...> = {
    BindableEvent: BindableEvent,
    Connections: {[TypedEventConnection.TypedEventConnection<T...>]: RBXScriptConnection},
    CurrentWaits: number,
} & typeof(setmetatable({}, TypedEvent))

--[[
Creates a typed event.
--]]
function TypedEvent.new<T...>(): TypedEvent<T...>
    --Create the object.
    local self = setmetatable({
        BindableEvent = Instance.new("BindableEvent"),
        Connections = {},
        CurrentWaits = 0
    }, TypedEvent) :: TypedEvent<T...>

    --Return the object.
    return self
end

--[[
Establishes a function to be called when the event fires.
Returns a connection object that can be disconnected.
--]]
function TypedEvent.Connect<T...>(self: TypedEvent<T...>, Callback: (T...) -> ()): TypedEventConnection.TypedEventConnection<T...>
    --Create the connection.
    local Connection = TypedEventConnection.new(self :: any, Callback)

    --Set up the bindable event.
    local BindableEventConnection = self.BindableEvent.Event:Connect(function(UUID)
        Connection:Fire(table.unpack(self.LastArguments[UUID]))
    end)

    --Store the connections.
    self.Connections[Connection] = BindableEventConnection

    --Return the connection.
    return Connection
end

--[[
Establishes a function to be called when the event fires.
Returns a connection object that can be disconnected.
However, instead of allowing multiple events to be received by the specified function,
only the first event will be delivered. Using Once also ensures that the connection to
the function will be automatically disconnected prior the function being called.
--]]
function TypedEvent.Once<T...>(self: TypedEvent<T...>, Callback: (T...) -> ()): ()
    local Connection = nil
    Connection = self:Connect(function(...)
        if Connection then
            Connection:Disconnect()
        end
        Callback(...)
    end)
end

--[[
Yields the current thread until the signal fires and returns the arguments provided by the signal.
--]]
function TypedEvent.Wait<T...>(self: TypedEvent<T...>): T...
    --Wait for the event.
    self.CurrentWaits = self.CurrentWaits + 1
    local UUID = self.BindableEvent.Event:Wait()
    self.CurrentWaits = self.CurrentWaits - 1

    --Return the arguments.
    return table.unpack(self.LastArguments[UUID])
end

--[[
Fires the event.
--]]
function TypedEvent.Fire<T...>(self: TypedEvent<T...>, ...: T...): ()
    --Ignore if there are no connections.
    if next(self.Connections) == nil and self.CurrentWaits <= 0 then return end

    --Store the arguments.
    --For deferred events, the arguments need to be stored.
    local UUID = HttpService:GenerateGUID()
    local Arguments = table.pack(...)
    self.LastArguments[UUID] = Arguments

    --Queue clearing the arguments.
    task.defer(function()
        TypedEvent.QueuedClearArguments[UUID] = true
    end)

    --Invoke the event.
    self.BindableEvent:Fire(UUID)
end

--[[
Handles an event being disconnected.
--]]
function TypedEvent.Disconnected<T...>(self: TypedEvent<T...>, Connection: TypedEventConnection.TypedEventConnection<T...>): ()
    if not self.Connections[Connection] then return end
    self.Connections[Connection]:Disconnect()
    self.Connections[Connection] = nil
end

--[[
Destroys the event.
--]]
function TypedEvent.Destroy<T...>(self: TypedEvent<T...>): ()
    local OriginalConnections = self.Connections
    self.Connections = {}
    self.CurrentWaits = 0
    for Connection, _ in OriginalConnections do
        (Connection :: TypedEventConnection.TypedEventConnection<T...>):Disconnect()
    end
    self.BindableEvent:Destroy()
end

--Clear arguments in the background.
RunService.Heartbeat:Connect(function()
    local QueuedClearArguments = TypedEvent.QueuedClearArguments
    TypedEvent.QueuedClearArguments = {}

    for Key, _ in QueuedClearArguments do
        TypedEvent.LastArguments[Key] = nil
    end
end)

return TypedEvent


--Connection for a typed event.
--!strict

local TypedEventConnection = {}
TypedEventConnection.__index = TypedEventConnection

export type TypedEventStub<T...> = {
    Disconnected: (TypedEventStub<T...>, TypedEventConnection<T...>) -> (),
    [any]: any,
}

export type TypedEventConnection<T...> = {
    Connected: boolean,
    ParentEvent: TypedEventStub<T...>,
    ConnectionFunction: (T...) -> (),
} & typeof(setmetatable({}, TypedEventConnection))

--[[
Creates a connection.
--]]
function TypedEventConnection.new<T...>(ParentEvent: TypedEventStub<T...>, ConnectionFunction: (T...) -> ()): TypedEventConnection<T...>
    return setmetatable({
        Connected = true,
        ParentEvent = ParentEvent,
        ConnectionFunction = ConnectionFunction,
    }, TypedEventConnection) :: TypedEventConnection<T...>
end

--[[
Fires the connection.
--]]
function TypedEventConnection.Fire<T...>(self: TypedEventConnection<T...>, ...: T...): ()
    if not self.Connected then return end
    self.ConnectionFunction(...)
end

--[[
Disconnect the connection from the event.
--]]
function TypedEventConnection.Disconnect<T...>(self: TypedEventConnection<T...>): ()
    --Set the event as disconnected.
    if not self.Connected then return end
    self.Connected = false

    --Remove the connection.
    self.ParentEvent:Disconnected(self)
end

--[[
Destroys the connection.
--]]
function TypedEventConnection.Destroy<T...>(self: TypedEventConnection<T...>): ()
    self:Disconnect()
end

return TypedEventConnection


--Main module for Nexus Instance.
--!strict

local METATABLE_METHODS = {
    "__call",
    "__concat",
    "__unm",
    "__add",
    "__sub",
    "__mul",
    "__div",
    "__idiv",
    "__mod",
    "__pow",
    "__tostring",
    "__eq",
    "__lt",
    "__le",
    "__gc",
    "__len",
    "__iter",
}

local TypedEvent = require(script:WaitForChild("Event"):WaitForChild("TypedEvent"))

local NexusInstance = {}
NexusInstance.TypedEvent = TypedEvent

export type TypedEvent<T...> = TypedEvent.TypedEvent<T...>

export type NexusInstanceClass<TClass, TConstructor> = {
    new: TConstructor
} & TClass

export type NexusInstance<TObject> = {
    Changed: TypedEvent<string>,
    BaseClass: any,
    Events: {TypedEvent<unknown>},
    TransformFunctions: {(Index: any, Value: any) -> (any)},
    PropertyTransformFunctions: {[string]: {(Value: any) -> (any)}},
    OnAnyPropertyChangedFunctions: {(PropertyName: any, Value: any) -> ()},
    OnPropertyChangedFunctions: {[string]: {(Value: any) -> ()}},
    HiddenPropertyChanges: {[string]: boolean},
    HiddenNextPropertyChanges: {[string]: boolean},
    PropertyChangedEvents: {[string]: TypedEvent<>},
} & typeof(NexusInstance) & TObject

--[[
Creates an instance version of the given class.
--]]
function NexusInstance.ToInstance<TClass, TConstructor>(Class: TClass): NexusInstanceClass<TClass, TConstructor>
    --Add the constructor.
    local ExtendedClass = (Class :: any) :: NexusInstanceClass<TClass, TConstructor>
    ExtendedClass.new = (function(...)
        --Prepare the object.
        local ChangedEvent = (TypedEvent.new() :: any) :: TypedEvent<string>
        local Properties = {}
        local TransformFunctions = {} :: {(Index: any, Value: any) -> (any)}
        local PropertyTransformFunctions = {} :: {[string]: {(Value: any) -> (any)}}
        local OnAnyPropertyChangedFunctions = {} :: {(PropertyName: any, Value: any) -> ()}
        local OnPropertyChangedFunctions = {} :: {[string]: {(Value: any) -> ()}}
        local HiddenPropertyChanges = {} :: {[string]: boolean}
        local HiddenNextPropertyChanges = {} :: {[string]: boolean}
        local PropertyChangedEvents = {} :: {[string]: TypedEvent<>}

        --Create the object.
        local Object = {
            Changed = ChangedEvent,
            BaseClass = Class,
            Events = {ChangedEvent},
            TransformFunctions = TransformFunctions,
            PropertyTransformFunctions = PropertyTransformFunctions,
            OnAnyPropertyChangedFunctions = OnAnyPropertyChangedFunctions,
            OnPropertyChangedFunctions = OnPropertyChangedFunctions,
            HiddenPropertyChanges = HiddenPropertyChanges,
            HiddenNextPropertyChanges = HiddenNextPropertyChanges,
            PropertyChangedEvents = PropertyChangedEvents,
        }
        local Metatable = {
            __index = function(_, Index: string): any?
                --Return the property.
                local PropertyValue = Properties[Index]
                if PropertyValue ~= nil then
                    return PropertyValue
                end

                --Return the NexusInstance value.
                local NexusInstanceValue = (NexusInstance :: any)[Index]
                if NexusInstanceValue ~= nil then
                    return NexusInstanceValue
                end

                --Return the class's value.
                return (Class :: any)[Index]
            end,
            __newindex = function(self: any, Index: string, Value: any): ()
                --Perform the transform functions.
                for _, TransformFunction in TransformFunctions do
                    Value = TransformFunction(Index, Value)
                end
                if PropertyTransformFunctions[Index] then
                    for _, TransformFunction in PropertyTransformFunctions[Index] do
                        Value = TransformFunction(Value)
                    end
                end

                --Return if the new and old values are the same.
                if self[Index] == Value then
                    return
                end

                --Change the property.
                Properties[Index] = Value

                --Invoke that the property has changed.
                for _, ChangedCallback in OnAnyPropertyChangedFunctions do
                    ChangedCallback(Index, Value)
                end
                local PropertyChangedCallbacks = OnPropertyChangedFunctions[Index]
                if PropertyChangedCallbacks then
                    for _, ChangedCallback in PropertyChangedCallbacks do
                        ChangedCallback(Value)
                    end
                end

                --Return if the property is hidden.
                if HiddenNextPropertyChanges[Index] then
                    HiddenNextPropertyChanges[Index] = nil
                    return
                end
                if HiddenPropertyChanges[Index] then
                    return
                end

                --Invoke the changed event.
                self.Changed:Fire(Index)
                local PropertyChangedEvent = PropertyChangedEvents[Index]
                if PropertyChangedEvent then
                    PropertyChangedEvent:Fire()
                end
            end,
        }
        for _, MetatableMethod in METATABLE_METHODS do
            (Metatable :: any)[MetatableMethod] = (Class :: any)[MetatableMethod]
        end
        local self = setmetatable(Object, Metatable)

        --Call the __new constructor of the class.
        local Constructor = (Class :: any).__new
        if Constructor then
            Constructor(self, ...)
        end

        --Return the object.
        return self
    end :: any) :: TConstructor

    --Return the extended class.
    return ExtendedClass
end

--[[
Creates an event that is disconnected when the instance is destroyed.
--]]
function NexusInstance.CreateEvent<T, TEventArguments...>(self: NexusInstance<T>): TypedEvent<TEventArguments...>
    local NewEvent = TypedEvent.new() :: TypedEvent<TEventArguments...>
    table.insert(self.Events, NewEvent :: any)
    return NewEvent
end

--[[
Adds a generic property transformer.
--]]
function NexusInstance.AddGenericPropertyTransform<T>(self: NexusInstance<T>, Transform: (Index: any, Value: any) -> (any)): ()
    table.insert(self.TransformFunctions, Transform)
end

--[[
Adds a property transformer for a specific property.
--]]
function NexusInstance.AddPropertyTransform<T>(self: NexusInstance<T>, PropertyName: any, Transform: (Value: any) -> (any)): ()
    if not self.PropertyTransformFunctions[PropertyName] then
        self.PropertyTransformFunctions[PropertyName] = {}
    end
    table.insert(self.PropertyTransformFunctions[PropertyName], Transform)
end

--[[
Adds a callback when a property is changed.
This invokes immediately as opposed to using events.
--]]
function NexusInstance.OnAnyPropertyChanged<T>(self: NexusInstance<T>, Callback: (PropertyName: any, Value: any) -> ()): ()
    table.insert(self.OnAnyPropertyChangedFunctions, Callback)
end

--[[
Adds a callback when the given property is changed.
This invokes immediately as opposed to using events.
--]]
function NexusInstance.OnPropertyChanged<T>(self: NexusInstance<T>, PropertyName: string, Callback: (Value: any) -> ()): ()
    if not self.OnPropertyChangedFunctions[PropertyName] then
        self.OnPropertyChangedFunctions[PropertyName] = {}
    end
    table.insert(self.OnPropertyChangedFunctions[PropertyName], Callback)
end

--[[
Hides all changed events for a property.
--]]
function NexusInstance.HidePropertyChanges<T>(self: NexusInstance<T>, PropertyName: string): ()
    self.HiddenPropertyChanges[PropertyName] = true
end

--[[
Hides the next changed events for a property.
--]]
function NexusInstance.HideNextPropertyChange<T>(self: NexusInstance<T>, PropertyName: string): ()
    self.HiddenNextPropertyChanges[PropertyName] = true
end

--[[
Returns an event for when a property is changed.
--]]
function NexusInstance.GetPropertyChangedSignal<T>(self: NexusInstance<T>, PropertyName: string): TypedEvent<>
    if not self.PropertyChangedEvents[PropertyName] then
        self.PropertyChangedEvents[PropertyName] = self:CreateEvent()
    end
    return self.PropertyChangedEvents[PropertyName]
end

--[[
Destroys the instance.
--]]
function NexusInstance.Destroy<T>(self: NexusInstance<T>): ()
    --Call the class's destroy event.
    local DestroyFunction = (self.BaseClass :: any).Destroy
    if DestroyFunction then
        DestroyFunction(self)
    end

    --Disconnect the events.
    task.defer(function()
        for _, Event in self.Events do
            (Event :: TypedEvent<unknown>):Destroy()
        end
        self.Events = {}
    end)
end

return NexusInstance


--> lib: Builder pattern class to spawn child processes

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

local Option = require("../lune_packages/option")
type Option<T> = Option.Option<T>

local CommandBuilder = {}

export type CommandBuilder = typeof(setmetatable({} :: CommandBuilderFields, { __index = CommandBuilder }))
type CommandBuilderFields = {
	program: string,
	args: { string },
	env: { [string]: string },
	stdioStrategy: Option<IoStrategyMapping>,
}
export type StdioStrategy = "pipe" | "inherit" | "forward" | "none"
export type IoStrategyMapping = {
	stdin: Option<StdioStrategy>,
	stdout: Option<StdioStrategy>,
	stderr: Option<StdioStrategy>,
}

local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = {
	stdin = Option.Some("pipe" :: StdioStrategy),
	stdout = Option.Some("pipe" :: StdioStrategy),
	stderr = Option.Some("pipe" :: StdioStrategy),
}
function CommandBuilder.new(program: string)
	return setmetatable(
		{
			program = program,
			args = {},
			env = {},
			stdioStrategy = Option.None :: Option<IoStrategyMapping>,
		} :: CommandBuilderFields,
		{
			__index = CommandBuilder,
		}
	)
end

function CommandBuilder.withArg(self: CommandBuilder, arg: string): CommandBuilder
	table.insert(self.args, arg)
	return self
end

function CommandBuilder.withArgs(self: CommandBuilder, args: { string }): CommandBuilder
	for _, arg in args do
		self:withArg(arg)
	end

	return self
end

function CommandBuilder.withEnvVar(self: CommandBuilder, var: string, value: string): CommandBuilder
	self.env[var] = value
	return self
end

function CommandBuilder.withEnv(self: CommandBuilder, env: { [string]: string }): CommandBuilder
	for var, value in env do
		self:withEnvVar(var, value)
	end

	return self
end

function CommandBuilder.withStdioStrategy(
	self: CommandBuilder,
	strategy: StdioStrategy | IoStrategyMapping
): CommandBuilder
	self.stdioStrategy = Option.Some(if typeof(strategy) == "string"
		then {
			stdout = Option.Some(strategy),
			stderr = Option.Some(strategy),
		}
		else strategy) :: Option<IoStrategyMapping>
	return self
end

local function intoSpawnOptionsStdioKind(strategy: StdioStrategy): process.ExecStdioKind
	if strategy == "pipe" then
		return "default"
	end

	if strategy == "inherit" then
		return "inherit"
	end

	if strategy == "forward" then
		return "forward"
	end

	if strategy == "none" then
		return "none"
	end

	error(`Non-strategy provided: {strategy}`)
end

function CommandBuilder.exec(self: CommandBuilder): process.ExecResult
	print(
		stdio.style("bold") .. "$",
		stdio.style("dim") .. self.program,
		table.concat(self.args, " ") .. stdio.style("reset")
	)
	local child = process.exec(self.program, self.args, {
		shell = if process.os == "windows" then "cmd.exe" else "bash",
		stdio = self.stdioStrategy
			:orOpt(Option.Some(DEFAULT_STDIO_STRATEGY))
			:map(function(mappings: IoStrategyMapping)
				local translatedMappings: process.ExecStdioOptions = {}
				for field, value in mappings do
					translatedMappings[field] = intoSpawnOptionsStdioKind((value :: Option<StdioStrategy>):unwrap())
				end

				return translatedMappings
			end)
			:unwrap(),
	})

	if not child.ok then
		print(`\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`, child.code)
	end

	return child
end

return CommandBuilder


--> Run stylua to check for formatting errors

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

local CommandBuilder = require("./exec")

process.exit(
	CommandBuilder.new("pesde")
		:withArgs({ "run", "./bins/stylua" })
		:withArg("--")
		:withArgs(process.args)
		:withStdioStrategy("forward")
		:exec().code
)


--> Run luau-lsp analysis to check for type errors

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

local CommandBuilder = require("./exec")

process.exit(
	CommandBuilder.new("pesde")
		:withArgs({ "run", "./bins/luau_lsp" })
		:withArgs({ "--", "analyze" })
		:withArgs({ "--settings", ".vscode/settings.json" })
		:withArgs({ "--ignore", "'**/*_packages/**'" })
		:withArgs({ "--platform", "standard" })
		:withArg(".")
		:withStdioStrategy("forward")
		:exec().code
)


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

local exitCode = require("@self/lune_packages/toolchainlib")("argon-rbx/argon", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("jackTabsCode/asphalt", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("seaofvoices/darklua", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("JohnnyMorganz/luau-lsp", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("blake-mealey/mantle", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("rojo-rbx/rojo", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("Kampfkarren/selene", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("JohnnyMorganz/stylua", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local exitCode = require("@self/lune_packages/toolchainlib")("red-blox/zap", _G.PESDE_ROOT)

return process.exit(exitCode)


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

local dirs = require("../lune_packages/dirs")
local pathfs = require("../lune_packages/pathfs")
local unzip = require("../luau_packages/unzip")

local Result = require("../lune_packages/result")
local Option = require("../lune_packages/option")
type Result<T, E> = Result.Result<T, E>
type Option<T> = Option.Option<T>

local revTable = require("./utils/rev_table")
local descriptors = require("./platform/descriptor")
local PlatformDescriptor = descriptors.PlatformDescriptor
local ExecutableDescriptor = descriptors.ExecutableDescriptor

export type CompressionFormat = "TarGz" | "TarXz" | "Zip"

local function detectFormat(fileName: string): Option<CompressionFormat>
	local fileNameParts = string.split(string.lower(fileName), ".")
	revTable(fileNameParts)

	if fileNameParts[1] == "zip" then
		return Option.Some("Zip" :: CompressionFormat)
	end

	if fileNameParts[2] == "tar" then
		if fileNameParts[1] == "gz" then
			return Option.Some("TarGz" :: CompressionFormat)
		end

		if fileNameParts[1] == "xz" then
			return Option.Some("TarXz" :: CompressionFormat)
		end
	end

	return Option.None :: Option<CompressionFormat>
end

-- TODO: Use a type function to make all CompressionFormat lowercase
local extractBinary: {
	[CompressionFormat]: (
		compressed: buffer,
		binaryName: string,
		targetPlatform: descriptors.PlatformDescriptor
	) -> Result<buffer, string>,
} =
	{
		Zip = function(compressed, binaryName, targetPlatform)
			-- Load the compressed data into a `ZipReader`
			local reader = unzip.load(compressed)
			local binaryContents: buffer?

			--- Extracts a specific entry from the ZIP and validates that its `PlatformDescriptor`
			--- matches the expected one
			local function extractEntry(entry: unzip.ZipEntry): buffer?
				local contents = reader:extract(entry, { type = "binary" }) :: buffer
				local executablePlatform = ExecutableDescriptor.fromExecutable(contents)

				if
					executablePlatform:isOkAnd(function(platform)
						return PlatformDescriptor.matches(targetPlatform, platform)
					end)
				then
					return contents
				end

				return nil
			end

			-- Find the entry and attempt to extract it
			local binaryEntry = reader:findEntry(binaryName)
			if binaryEntry then
				binaryContents = extractEntry(binaryEntry)
			end

			-- Fallback for if we cannot find the `ZipEntry`
			if binaryContents == nil then
				-- Walk through the entries to find an executable
				reader:walk(function(entry)
					if binaryContents ~= nil then
						-- If we successfully extracted the binary on a previous iteration,
						-- we skip this entry
						return
					end

					if entry.isDirectory then
						-- Ignore directories
						return
					end

					binaryContents = extractEntry(entry)
				end)
			end

			if binaryContents == nil then
				-- If both the fallback and the initial attempts did not yield an extracted binary,
				-- we return an error
				return Result.Err("ExtractBinaryError::BinaryNotFound" :: string)
			end

			return Result.Ok(binaryContents)
		end,

		TarXz = function(compressed, binaryName, targetPlatform)
			local extractionDir = dirs.createTempDir(nil, "pesde-tooling")
			local tarResult = process.exec(
				"tar",
				{ "-xJf", "-", "-C", extractionDir.path:toString(), "--strip-components=1" },
				{
					stdio = {
						stdin = compressed,
					},
				}
			)

			if not tarResult.ok then
				return Result.Err(
					`ExtractBinaryError::Decompression(msg={tarResult.stderr},code={tarResult.code})` :: string
				)
			end

			local ok, binaryContentsStr =
				pcall(pathfs.fs.readFile, pathfs.Path.from(tostring(extractionDir)):join(binaryName))
			if ok then
				return Result.Ok(buffer.fromstring(binaryContentsStr))
			end

			for _, entry in extractionDir:readDir() do
				local entryPath = extractionDir.path:join(entry)
				if not pathfs.fs.isFile(entryPath) then
					continue
				end

				local contents = buffer.fromstring(pathfs.fs.readFile(entryPath))
				local executablePlatform = ExecutableDescriptor.fromExecutable(contents)

				if
					executablePlatform:isOkAnd(function(platform)
						return PlatformDescriptor.matches(targetPlatform, platform)
					end)
				then
					return Result.Ok(contents)
				end
			end

			return Result.Err("ExtractBinaryError::BinaryNotFound" :: string)
		end,
	}

return { extractBinary = extractBinary, detectFormat = detectFormat }


export type SPDXLicense =
	"MIT"
	| "Apache-2.0"
	| "BSD-2-Clause"
	| "BSD-3-Clause"
	| "GPL-2.0"
	| "GPL-3.0"
	| "LGPL-2.1"
	| "LGPL-3.0"
	| "MPL-2.0"
	| "ISC"
	| "Unlicense"
	| "WTFPL"
	| "Zlib"
	| "CC0-1.0"
	| "CC-BY-4.0"
	| "CC-BY-SA-4.0"
	| "BSL-1.0"
	| "EPL-2.0"
	| "AGPL-3.0"

export type DependencySpecifier = ((
	{ name: string, version: string, index: string? }
	| { workspace: string, version: string }
	| { repo: string, rev: string, path: string? }
) & {
	target: string?,
}) | { wally: string, version: string, index: string? }

export type PackageTarget =
	{
		environment: "luau" | "lune" | "roblox" | "roblox_server",
		lib: string,
	}
	| {
		environment: "luau" | "lune",
		bin: string,
	}

export type PesdeManifest = {
	name: string,
	version: string,
	description: string?,
	license: SPDXLicense?,
	authors: { string }?,
	repository: string?,
	private: boolean?,
	includes: { string }?,
	pesde_version: string?,
	workspace_members: { string }?,
	target: PackageTarget,
	build_files: { string }?,
	scripts: { [string]: string }?,
	indices: { [string]: string },
	wally_indices: { [string]: string }?,
	overrides: { [string]: DependencySpecifier }?,
	patches: { [string]: { [string]: string } }?,
	place: { [string]: string }?,
	dependencies: { [string]: DependencySpecifier }?,
	peer_dependencies: { [string]: DependencySpecifier }?,
	dev_dependencies: { [string]: DependencySpecifier }?,
}

return "<manifest>"


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

local os = require("./os")
local arch = require("./arch")
local toolchain = require("./toolchain")
local result = require("./result")
local detectFromExecutable = require("./detection/executable")

local Result = require("../../lune_packages/result")
local Option = require("../../lune_packages/option")
local OptionExt = require("../utils/ext/option")
type Result<T, E> = Result.Result<T, E>
type Option<T> = Option.Option<T>

local PlatformDescriptor = {}
export type PlatformDescriptor = {
	os: process.OS,
	arch: Option<arch.Arch>,
	toolchain: Option<toolchain.Toolchain>,
}
local ExecutableDescriptor = {}
type ExecutableDescriptor = detectFromExecutable.ExecutableDescriptor

function PlatformDescriptor.currentSystem(): PlatformDescriptor
	return {
		os = process.os,
		arch = Option.Some(process.arch) :: Option<arch.Arch>,
		toolchain = Option.None :: Option<toolchain.Toolchain>,
	}
end

function ExecutableDescriptor.fromString(str: string): result.PlatformResult<ExecutableDescriptor>
	local detectedOs = os.detect(str)
	if detectedOs:isNone() then
		return Result.Err("NoPatternDetected" :: result.PlatformError) :: result.PlatformResult<ExecutableDescriptor>
	end

	return Result.Ok({
		os = detectedOs:unwrap() :: process.OS,
		arch = arch.detect(str):mapOr({}, function(arch)
			return { arch }
		end),
	}) :: result.PlatformResult<ExecutableDescriptor>
end

function ExecutableDescriptor.fromExecutable(contents: buffer): result.PlatformResult<ExecutableDescriptor>
	local detected = Option.from(detectFromExecutable(contents))

	return OptionExt.okOr(
			detected,
			"NoExecutableDetected" :: result.PlatformError
		) :: result.PlatformResult<ExecutableDescriptor>
end

function PlatformDescriptor.matches(self: PlatformDescriptor, executable: ExecutableDescriptor): boolean
	if self.os ~= executable.os then
		return false
	end

	if self.arch:isNone() or table.find(executable.arch, self.arch:unwrap()) then
		return true
	end

	return false
end

return {
	PlatformDescriptor = PlatformDescriptor,
	ExecutableDescriptor = ExecutableDescriptor,
}


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

type Arch = process.Arch | "arm" | "x86"
export type ExecutableDescriptor = {
	os: process.OS,
	arch: { Arch },
}

local ENDIANNESS = {
	function(x: number)
		return x
	end,
	bit32.byteswap,
}

return function(binaryContents: buffer): ExecutableDescriptor?
	-- Windows PE
	do
		local DOS_HEADER = "MZ"
		local PE_HEADER_OFFSET = 0x3c
		local PE_MAGIC_SIGNATURE = "PE\x00\x00"
		local PE_MACHINE_TYPES: { [number]: Arch } = {
			[0x8664] = "x86_64",
			[0x01c0] = "arm",
			[0xaa64] = "aarch64",
			[0x014c] = "x86",
		}

		if buffer.readstring(binaryContents, 0, 2) == DOS_HEADER then
			-- File was a DOS executable, jump to PE header to get the magic offset
			local signatureOffset = buffer.readu32(binaryContents, PE_HEADER_OFFSET)

			-- Check if the value at the magic offset was the PE magic signature
			if buffer.readstring(binaryContents, signatureOffset, 4) == PE_MAGIC_SIGNATURE then
				-- After the first 4 magic bytes, the next 2 bytes are architecture information
				local machineType = buffer.readu16(binaryContents, signatureOffset + 4)
				return {
					os = "windows",
					arch = { PE_MACHINE_TYPES[machineType] },
				}
			end
		end
	end

	-- Linux ELF
	do
		local ELF_MAGIC_START = 0x7f
		local ELF_MAGIC_SIGNATURE = "ELF"
		local ELF_MACHINE_TYPES: { [number]: Arch } = {
			[0x3e] = "x86_64",
			[0x28] = "arm",
			[0xb7] = "aarch64",
			[0x03] = "x86",
		}

		-- ELF files have a magic signature of [0x7f, 'E', 'L', 'F']
		if
			buffer.readu8(binaryContents, 0) == ELF_MAGIC_START
			and buffer.readstring(binaryContents, 1, 3) == ELF_MAGIC_SIGNATURE
		then
			-- Machine type is located after 16 bytes of ident and 2 bytes of type
			local machineType = buffer.readu16(binaryContents, 18)
			return {
				os = "linux",
				arch = { ELF_MACHINE_TYPES[machineType] },
			}
		end
	end

	-- macOS Mach-O
	-- Unlike other types of executables, which are either strictly defined to an endianness
	-- or their endianness doesn't matter, Mach-O's executables' endianness is not defined,
	-- and it matters, so we have to brute force it
	for _, endianness in ENDIANNESS do
		local MACHO_MAGIC_32 = endianness(0xFEEDFACE)
		local MACHO_MAGIC_64 = endianness(0xFEEDFACF)
		local MACHO_MAGIC_UN = endianness(0xCAFEBABE)

		local MACHO_CPU_TYPES: { [number]: Arch? } = {
			[endianness(0x00000007)] = "x86",
			[endianness(0x0000000C)] = "arm",
			[endianness(0x01000007)] = "x86_64",
			[endianness(0x0100000C)] = "aarch64",
		}

		-- First 4 bytes are the magic number
		local headerStart = buffer.readu32(binaryContents, 0)
		if headerStart == MACHO_MAGIC_UN then
			-- Next 4 bytes is the number of images in this binary
			local imagesAmount = endianness(buffer.readu32(binaryContents, 4))
			local arches: { Arch } = {}

			for i = 0, imagesAmount - 1 do
				-- 4 bytes for the CPU type
				-- 4 bytes for the CPU subtype
				-- 4 bytes for the file offset
				-- 4 bytes for the size
				-- 4 bytes for the section alignment
				-- += 20
				local cpuType = buffer.readu32(binaryContents, 8 + i * 20)
				local arch = MACHO_CPU_TYPES[cpuType]
				if arch then
					table.insert(arches, arch)
				end
			end

			return {
				os = "macos",
				arch = arches,
			}
		end

		if headerStart == MACHO_MAGIC_64 or headerStart == MACHO_MAGIC_32 then
			-- Next 4 bytes are the CPU type
			local cpuType = buffer.readu32(binaryContents, 4)
			local arch: Arch? = MACHO_CPU_TYPES[cpuType]

			return {
				os = "macos",
				arch = if arch then { arch } else {},
			}
		end
	end

	return nil
end


local executable = require("@self/executable")
export type ExecutableDescriptor = executable.ExecutableDescriptor

return {
	detect = require("@self/pattern"),
	detectFromExecutable = executable,
}


local String = require("../../utils/string")

local Option = require("../../../lune_packages/option")
type Option<T> = Option.Option<T>

local function charWordSep(char: string)
	return char == " " or char == "-" or char == "_"
end

return function<T>(str: string, substrings: { [T]: { string } }, fullWords: { [T]: { string } }): Option<T>
	local lowercased = string.lower(str)

	-- Look for substring matches
	for item: T, keywords in substrings do
		for _, keyword in keywords do
			if string.find(lowercased, keyword) then
				return Option.Some(item) :: Option<T>
			end
		end
	end

	-- If no substring matches found, look for a full word as a component
	local components = String.splitAtChar(lowercased, charWordSep)
	for _, component in components do
		for item, keywords in fullWords do
			for _, keyword in keywords do
				if string.find(component, keyword) then
					return Option.Some(item) :: Option<T>
				end
			end
		end
	end

	return Option.None :: Option<T>
end


local Result = require("../../lune_packages/result")
type Result<T, E> = Result.Result<T, E>

export type PlatformError = "NoPatternDetected" | "NoExecutableDetected" | "UnknownExecutableField"
export type PlatformResult<T> = Result<T, PlatformError>

return "<PlatformResult>"


local Option = require("../../lune_packages/option")
type Option<T> = Option.Option<T>

local TOOLCHAINS: { Toolchain } = { "msvc", "gnu", "musl" }
export type Toolchain = "msvc" | "gnu" | "musl"

return {
	detect = function(str: string): Option<Toolchain>
		for _, toolchain: Toolchain in TOOLCHAINS do
			if string.find(str, toolchain) then
				return Option.Some(toolchain :: Toolchain)
			end
		end

		return Option.None :: Option<Toolchain>
	end,
}


local function copy<T>(tab: T): T
	if typeof(tab) == "table" then
		local copied = {}

		for k, v in tab do
			copied[k] = copy(v)
		end

		return copied :: any
	end

	return tab
end

return copy


--> Non-exhaustive set of extensions for the `Option<T>` type

local Option = require("../../../lune_packages/option")
local Result = require("../../../lune_packages/result")

local OptionExt = {}

function OptionExt.okOr<T, E>(self: Option.Option<T>, err: E): Result.Result<T, E>
	return self:mapOrElse(function()
		return Result.Err(err)
	end, function(val)
		return Result.Ok(val)
	end) :: Result.Result<T, E>
end

return OptionExt


--> Non-exhaustive set of extensions for the `Result<T, E>` type

local Option = require("../../../lune_packages/option")
local Result = require("../../../lune_packages/result")

local ResultExt = {}

function ResultExt.ok<T, E>(self: Result.Result<T, E>): Option.Option<T>
	return self:mapOr(Option.None, function(val: T)
		return Option.Some(val)
	end) :: Option.Option<T>
end

return ResultExt


return function(tab)
	local len = #tab
	for pos = 1, len // 2 do
		tab[pos], tab[len - pos + 1] = tab[len - pos + 1], tab[pos]
	end
end


--> Non-exhaustive set of util functions for Linux systems

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

local ResultExt = require("./ext/result")

local fs = require("../../lune_packages/pathfs").fs
local Result = require("../../lune_packages/result")
local Option = require("../../lune_packages/option")

local sys = {}
sys.consts = {
	S_IFMT = 0xF000,
	S_IFCHR = 0x2000,
}

export type StdioFd = "stdin" | "stdout" | "stderr"
local STDIO_FD_LOOKUP: { [StdioFd]: number } = {
	stdin = 0,
	stdout = 1,
	stderr = 2,
}

function sys.isCharacterDevice(mode: number)
	return bit32.band(mode, sys.consts.S_IFMT) == sys.consts.S_IFCHR
end

function sys.isTTY(fd: number | StdioFd): boolean
	if process.os ~= "linux" then
		-- idk about windows and macOS :p
		return true
	end

	local inputFd: number = if typeof(fd) == "string" then STDIO_FD_LOOKUP[fd] else fd
	local statFd = fs.readFile("/proc/self/stat")
	local pid = assert(string.match(statFd, "^%S+%s+%S+%s+%S+%s+(%S+)"), "Could not get current process PID")

	local stdoutFdMode = ResultExt.ok(
		Result.try(process.exec, "stat", { "-L", "-c", "%f", `/proc/{pid}/fd/{inputFd}` })
	)
		:andThen(function(child: process.ExecResult)
			if not child.ok then
				return Option.None
			end

			return Option.from(tonumber(child.stdout, 16))
		end)

	return stdoutFdMode:isSomeAnd(sys.isCharacterDevice)
end

return sys


--!strict

--[=[
	Basic luau-lint configuration example

	This shows a minimal configuration for a typical Luau project.
]=]

return {
	-- Enable recommended rules plugin
	plugins = {
		"@luau-lint/recommended-rules",
	},

	-- Configure individual rules
	rules = {
		["generalized-iteration"] = "error",
	},

	-- This is a root configuration file
	root = true,
}