-- Component for managing settings.

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

---

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local CommonConstants = require(CommonModules.Constants)
local CommonEnums = require(CommonModules.Enums)
local Prompt = require(CommonModules.Prompt)
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)
local Window = require(CommonModules.Window)

local CommonIncludes = Common.Includes
local Roact = require(CommonIncludes.RoactRodux.Roact)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local Checkbox = require(CommonComponents.Checkbox)
local ConnectTheme = require(CommonComponents.ConnectTheme)
local ExportText = require(CommonComponents.ExportText)
local TextInput = require(CommonComponents.TextInput)

local StandardComponents = CommonComponents.StandardComponents
local StandardScrollingFrame = require(StandardComponents.ScrollingFrame)
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIListLayout = require(StandardComponents.UIListLayout)
local StandardUIPadding = require(StandardComponents.UIPadding)

local Components = root.Components
local ImportSettings = require(Components.ImportSettings)

local Modules = root.Modules
local Constants = require(Modules.Constants)
local Enums = require(Modules.Enums)
local ManagedUserData = require(Modules.ManagedUserData)
local Store = require(Modules.Store)
local WidgetInfo = require(Modules.WidgetInfo)

---

local COLORPANE_SETTINGS_LIST = {
    [CommonEnums.ColorPaneUserDataKey.AskNameBeforePaletteCreation] = true,
    [CommonEnums.ColorPaneUserDataKey.SnapValue] = true,
}

local COMPANION_SETTINGS_LIST = {
    [Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData] = true,
    [Enums.CompanionUserDataKey.CacheColorPropertiesAPIData] = true,
}

local UI_TRANSLATIONS = Translator.GenerateTranslationTable({
    "AskNameBeforePaletteCreation_SettingDescription",
    "AutoLoadColorProperties_SettingDescription",
    "CacheAPIData_SettingDescription",
    "ImportSettings_ButtonText",
    "ExportSettings_ButtonText",
    "ImportSettings_WindowTitle",
    "ExportSettings_WindowTitle",
    "ExportSettings_CopyPrompt",
    "ConfirmSettingsImport_WindowTitle",
    "ConfirmSettingsImport_PromptText",
    "Import_ButtonText",
    "SnapValue_SettingDescription",
})

local colorPaneUserData = ManagedUserData.ColorPane
local companionUserData = ManagedUserData.Companion

local round = function(n: number, optionalE: number?): number
    local e: number = optionalE or 0
    local p: number = 10^e

    if (p >= 0) then
        return math.floor((n / p) + 0.5) * p
    else
        return math.floor((n * p) + 0.5) / p
    end
end

---

--[[
    props

        LayoutOrder
        Text

    store props

        theme: StudioTheme
]]
local SectionHeader = ConnectTheme(function(props)
    local textSize = TextService:GetTextSize(
        props.Text,
        Style.Constants.StandardTextSize,
        Style.Fonts.Standard,
        Vector2.new(math.huge, math.huge)
    )

    return Roact.createElement("Frame", {
        Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize + (2 * Style.Constants.PagePadding)),
        LayoutOrder = props.LayoutOrder,
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding, 0}
        }),

        HeaderText = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0, 0.5),
            Position = UDim2.new(0, 0, 0.5, 0),
            Size = UDim2.new(0, textSize.X, 1, 0),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            Text = props.Text,
        }),

        Line = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.new(1, -(textSize.X + Style.Constants.SpaciousElementPadding), 0, 1),
            BackgroundTransparency = 0,
            BorderSizePixel = 0,

            BackgroundColor3 = props.theme:GetColor(Enum.StudioStyleGuideColor.MainText)
        }),
    })
end)

--[[
    store props

        theme: StudioTheme
]]

local Settings = Roact.PureComponent:extend("Settings")

Settings.init = function(self)
    local initSettings = {}

    for key in pairs(COLORPANE_SETTINGS_LIST) do
        initSettings[key] = colorPaneUserData:getValue(key)
    end

    for key in pairs(COMPANION_SETTINGS_LIST) do
        initSettings[key] = companionUserData:getValue(key)
    end

    self.listLength, self.updateListLength = Roact.createBinding(0)
    self.importSettingsWindow = Window.new(WidgetInfo.ImportSettings.Id, WidgetInfo.ImportSettings.Info)
    self.exportSettingsWindow = Window.new(WidgetInfo.ExportSettings.Id, WidgetInfo.ExportSettings.Info)
    self.confirmImportSubscription = nil
    self:setState(initSettings)
end

Settings.didMount = function(self)
    self.colorPaneValueChanged = colorPaneUserData.valueChanged:subscribe(function(setting)
        local key, newValue = setting.Key, setting.Value
        if (not COLORPANE_SETTINGS_LIST[key]) then return end

        self:setState({
            [key] = newValue,
        })
    end)

    self.companionValueChanged = companionUserData.valueChanged:subscribe(function(setting)
        local key, newValue = setting.Key, setting.Value
        if (not COMPANION_SETTINGS_LIST[key]) then return end

        self:setState({
            [key] = newValue,
        })
    end)

    self.importOpenedWithoutMounting = self.importSettingsWindow.openedWithoutMounting:subscribe(function()
        self.importSettingsWindow:close()
    end)

    self.exportOpenedWithoutMounting = self.exportSettingsWindow.openedWithoutMounting:subscribe(function()
        self.exportSettingsWindow:close()
    end)

    self.importClosedWithoutUnmounting = self.importSettingsWindow.closedWithoutUnmounting:subscribe(function()
        self.importSettingsWindow:unmount()
    end)

    self.exportClosedWithoutUnmounting = self.exportSettingsWindow.closedWithoutUnmounting:subscribe(function()
        self.exportSettingsWindow:unmount()
    end)
end

Settings.willUnmount = function(self)
    if (self.state.restorePromptSubscription) then
        self.state.restorePromptSubscription:unsubscribe()
    end

    if (self.confirmImportSubscription) then
        self.confirmImportSubscription:unsubscribe()
    end

    self.colorPaneValueChanged:unsubscribe()
    self.companionValueChanged:unsubscribe()
    self.importOpenedWithoutMounting:unsubscribe()
    self.importClosedWithoutUnmounting:unsubscribe()
    self.exportOpenedWithoutMounting:unsubscribe()
    self.exportClosedWithoutUnmounting:unsubscribe()

    self.importSettingsWindow:destroy()
    self.exportSettingsWindow:destroy()
end

Settings.render = function(self)
    local theme: StudioTheme = self.props.theme

    return Roact.createElement(StandardScrollingFrame, {
        AnchorPoint = Vector2.new(0.5, 0),
        Position = UDim2.new(0.5, 0, 0, 0),
        Size = UDim2.new(1, 0, 1, 0),
        BorderSizePixel = 0,

        CanvasSize = self.listLength:map(function(length)
            return UDim2.new(0, 0, 0, length)
        end),

        useMainBackgroundColor = true,
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        UIListLayout = Roact.createElement(StandardUIListLayout, {
            Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),
            HorizontalAlignment = Enum.HorizontalAlignment.Left,

            [Roact.Change.AbsoluteContentSize] = function(obj)
                self.updateListLength(obj.AbsoluteContentSize.Y + (Style.Constants.PagePadding * 2))
            end,

            preset = 1,
        }),

        ColorPaneSectionHeader = Roact.createElement(SectionHeader, {
            LayoutOrder = 1,
            Text = "ColorPane",
        }),

        AskNameBeforePaletteCreationCheckbox = Roact.createElement(Checkbox, {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardInputHeight),
            LayoutOrder = 2,

            value = self.state[CommonEnums.ColorPaneUserDataKey.AskNameBeforePaletteCreation],
            text = UI_TRANSLATIONS["AskNameBeforePaletteCreation_SettingDescription"],

            onChecked = function(newValue)
                colorPaneUserData:setValue(CommonEnums.ColorPaneUserDataKey.AskNameBeforePaletteCreation, newValue)
            end,
        }),

        SnapValue = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardInputHeight),
            LayoutOrder = 3,
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            Label = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(1, 0.5),
                Size = UDim2.new(1, -(40 + Style.Constants.SpaciousElementPadding), 1, 0),
                Position = UDim2.new(1, 0, 0.5, 0),

                Text = UI_TRANSLATIONS["SnapValue_SettingDescription"],
                TextXAlignment = Enum.TextXAlignment.Left,
                TextYAlignment = Enum.TextYAlignment.Top,

                TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText)
            }),

            Input = Roact.createElement(TextInput, {
                AnchorPoint = Vector2.new(0, 0.5),
                Size = UDim2.new(0, 40, 1, 0),
                Position = UDim2.new(0, 0, 0.5, 0),

                Text = self.state[CommonEnums.ColorPaneUserDataKey.SnapValue] * 100,
                TextXAlignment = Enum.TextXAlignment.Center,

                isTextAValidValue = function(text)
                    local n = tonumber(text)
                    if (not n) then return false end

                    n = n / 100
                    return ((n >= CommonConstants.MIN_SNAP_VALUE) and (n <= CommonConstants.MAX_SNAP_VALUE))
                end,

                onSubmit = function(text)
                    local n = tonumber(text)
                    n = math.clamp(n / 100, CommonConstants.MIN_SNAP_VALUE, CommonConstants.MAX_SNAP_VALUE)
                    n = round(n, math.log10(CommonConstants.MIN_SNAP_VALUE))

                    colorPaneUserData:setValue(CommonEnums.ColorPaneUserDataKey.SnapValue, n)
                end,
            })
        }),

        CompanionSectionHeader = Roact.createElement(SectionHeader, {
            LayoutOrder = 4,
            Text = "Companion",
        }),

        AutoLoadColorPropertiesCheckbox = Roact.createElement(Checkbox, {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
            LayoutOrder = 5,

            value = self.state[Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData],
            text = UI_TRANSLATIONS["AutoLoadColorProperties_SettingDescription"],

            onChecked = function(newValue)
                companionUserData:setValue(Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData, newValue)
            end,
        }),

        CacheAPIDataCheckbox = Roact.createElement(Checkbox, {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
            LayoutOrder = 6,

            value = self.state[Enums.CompanionUserDataKey.CacheColorPropertiesAPIData],
            text = UI_TRANSLATIONS["CacheAPIData_SettingDescription"],

            onChecked = function(newValue)
                companionUserData:setValue(Enums.CompanionUserDataKey.CacheColorPropertiesAPIData, newValue)
            end,
        }),

        ControlsSectionHeader = Roact.createElement(SectionHeader, {
            LayoutOrder = 7,
            Text = "Controls",
        }),

        ImportSettingsButton = Roact.createElement(Button, {
            Size = UDim2.new(0, 100, 0, Style.Constants.StandardButtonHeight),
            LayoutOrder = 8,

            displayType = "text",
            text = UI_TRANSLATIONS["ImportSettings_ButtonText"],

            onActivated = function()
                if (self.importSettingsWindow:isMounted() or self.confirmImportSubscription) then return end

                self.importSettingsWindow:mount(
                    UI_TRANSLATIONS["ImportSettings_WindowTitle"],
                    Roact.createElement(ImportSettings, {
                        onPromptClosed = function(settings)
                            self.importSettingsWindow:unmount()
                            if (not settings) then return end

                            local confirmImport = Prompt(
                                WidgetInfo.ConfirmImportSettingsPrompt.Id,
                                WidgetInfo.ConfirmImportSettingsPrompt.Info,
                                {
                                    Title = UI_TRANSLATIONS["ConfirmSettingsImport_WindowTitle"],
                                    PromptText = UI_TRANSLATIONS["ConfirmSettingsImport_PromptText"],
                                    ConfirmText = UI_TRANSLATIONS["Import_ButtonText"],
                                },
                                Store
                            )

                            self.confirmImportSubscription = confirmImport:subscribe(function(confirm)
                                if (not confirm) then
                                    self.confirmImportSubscription = nil
                                    return
                                end

                                for key, value in pairs(settings[Constants.COLORPANE_USERDATA_KEY]) do
                                    colorPaneUserData:setValue(key, value)
                                end

                                for key, value in pairs(settings[Constants.COMPANION_USERDATA_KEY]) do
                                    companionUserData:setValue(key, value)
                                end

                                print("Settings import complete!")
                                self.confirmImportSubscription = nil
                            end)
                        end,
                    }),
                    Store
                )
            end,
        }),

        ExportSettingsButton = Roact.createElement(Button, {
            Size = UDim2.new(0, 100, 0, Style.Constants.StandardButtonHeight),
            LayoutOrder = 9,

            displayType = "text",
            text = UI_TRANSLATIONS["ExportSettings_ButtonText"],

            onActivated = function()
                if (self.exportSettingsWindow:isMounted()) then return end

                local settingsTable = {
                    [Constants.COLORPANE_USERDATA_KEY] = colorPaneUserData:getAllValues(),
                    [Constants.COMPANION_USERDATA_KEY] = companionUserData:getAllValues(),
                }

                self.exportSettingsWindow:mount(
                    UI_TRANSLATIONS["ExportSettings_WindowTitle"],
                    Roact.createElement(ExportText, {
                        promptText = UI_TRANSLATIONS["ExportSettings_CopyPrompt"],
                        text = HttpService:JSONEncode(settingsTable),
                    }),
                    Store
                )
            end,
        }),
    })
end

return ConnectTheme(Settings)


local Workspace = game:GetService("Workspace")

---

local root = script.Parent.Parent
local Common = root.Common
local CommonIncludes = Common.Includes
local ColorLib = require(CommonIncludes.Color)

local Color, Gradient = ColorLib.Color, ColorLib.Gradient
local Terrain = Workspace.Terrain

---

local colorProperties = {}
local classHierarchy = {}
local materialEnumItems = Enum.Material:GetEnumItems()

---

local ColorAPIData = {}

ColorAPIData.init = function(apiData)
    ColorAPIData.init = nil

    local classes = apiData.Classes

    for i = 1, #classes do
        local class = classes[i]
        local className = class.Name
        local classMembers = class.Members

        classHierarchy[className] = (class.Superclass ~= "<<<ROOT>>>") and class.Superclass or nil

        for j = 1, #classMembers do
            local member = classMembers[j]
            if (member.MemberType ~= "Property") then continue end

            local memberName = member.Name
            local memberSecurity = member.Security
            local memberTags = member.Tags
            local memberValueType = member.ValueType

            -- check for color properties
            if (
                ((className == "DataModelMesh") and (memberName == "VertexColor")) or
                (memberValueType.Name == "BrickColor") or
                (memberValueType.Name == "Color3") or
                (memberValueType.Name == "ColorSequence")
            ) then
                -- check tags
                if (memberTags and (
                    table.find(memberTags, "Deprecated") or
                    table.find(memberTags, "ReadOnly") or
                    table.find(memberTags, "Hidden") or
                    table.find(memberTags, "NotScriptable")
                )) then continue end

                -- check security
                if (
                    (memberSecurity.Write == "NotAccessibleSecurity") or
                    (memberSecurity.Write == "RobloxSecurity") or
                    (memberSecurity.Write == "RobloxScriptSecurity") or
                    (memberSecurity.Read == "RobloxSecurity") or
                    (memberSecurity.Read == "RobloxScriptSecurity")
                ) then continue end

                if (not colorProperties[className]) then
                    colorProperties[className] = {}
                end

                colorProperties[className][memberName] = member
            end
        end
    end

    -- add custom Terrain properties
    if (not colorProperties.Terrain) then
        colorProperties.Terrain = {}
    end

    for _, materialEnumItem in pairs(materialEnumItems) do
        local success = pcall(function()
            Terrain:GetMaterialColor(materialEnumItem)
        end)

        if (success) then
            local memberName = materialEnumItem.Name .. " Material"

            local newPropertyData = {
                Name = memberName,
                Custom = true,

                ValueType = {
                    Name = "Color3"
                },

                Get = function(obj)
                    return obj:GetMaterialColor(materialEnumItem)
                end,

                Set = function(obj, color)
                    obj:SetMaterialColor(materialEnumItem, color)
                end
            }

            colorProperties.Terrain[memberName] = newPropertyData
        end
    end
end

ColorAPIData.GetProperty = function(className: string, propertyName: string)
    return colorProperties[className][propertyName]
end

ColorAPIData.GetProperties = function(classNames: {string})
    local properties = {}

    for i = 1, #classNames do
        local className = classNames[i]

        while (className) do
            if (not properties[className]) then
                local classProperties = colorProperties[className]

                if (classProperties) then
                    properties[className] = {}

                    for propertyName, property in pairs(classProperties) do
                        properties[className][propertyName] = property
                    end
                end
            end

            className = classHierarchy[className]
        end
    end

    return properties
end

ColorAPIData.TransformColorValue = function(className: string, propertyName: string, value: any)
    local propertyInfo = colorProperties[className][propertyName]
    if (not propertyInfo) then return end

    local valueType = propertyInfo.ValueType.Name

    if ((className == "DataModelMesh") and (propertyName == "VertexColor")) then
        return Vector3.new(value:components())
    elseif (valueType == "Color3") then
        return value:toColor3()
    elseif (valueType == "BrickColor") then
        return value:toBrickColor()
    elseif (valueType == "ColorSequence") then
        return value:colorSequence(nil, "RGB")
    else
        error("Unsupported value type")
    end
end

ColorAPIData.TransformPropertyValue = function(className: string, propertyName: string, value: any)
    local propertyInfo = colorProperties[className][propertyName]
    if (not propertyInfo) then return end

    local valueType = propertyInfo.ValueType.Name

    if ((className == "DataModelMesh") and (propertyName == "VertexColor")) then
        return Color.new(value.X, value.Y, value.Z)
    elseif (valueType == "Color3") then
        return Color.fromColor3(value)
    elseif (valueType == "BrickColor") then
        return Color.fromBrickColor(value)
    elseif (valueType == "ColorSequence") then
        return Gradient.fromColorSequence(value)
    else
        error("Unsupported value type")
    end
end

---

return ColorAPIData


--!strict
--[[
    Provides access to the ColorPane API throughout the project.
]]

local root = script.Parent.Parent

local Common = root.Common
local CommonModules = Common.Modules
local PluginProvider = require(CommonModules.PluginProvider)

local Includes = root.Includes
local InitColorPane = require(Includes.ColorPane)

---

local plugin: Plugin = PluginProvider()
local ColorPane = InitColorPane(plugin, "ColorPane_Companion")

---

return ColorPane


--!strict
--[[
    Provides a physical interface in the form of BindableFunctions and BindableEvents
    for ColorPane instances to retrieve and update user data values.
]]

local StudioService: StudioService = game:GetService("StudioService")

---

local root = script.Parent.Parent

local Common = root.Common
local CommonModules = Common.Modules
local ColorPaneUserDataInterfaceValidator = require(CommonModules.ColorPaneUserDataInterfaceValidator)
local ColorPaneUserDataInterfaceVersion = require(CommonModules.ColorPaneUserDataInterfaceVersion)
local CommonConstants = require(CommonModules.Constants)
local CommonTypes = require(CommonModules.Types)
local PluginProvider = require(CommonModules.PluginProvider)

local Modules = root.Modules
local ManagedUserData = require(Modules.ManagedUserData)

---

local plugin: Plugin = PluginProvider()
local colorPaneUserData = ManagedUserData.ColorPane

local interfaceFolder: Folder = Instance.new("Folder")
local getVersionFunction: BindableFunction = Instance.new("BindableFunction")
local getValueFunction: BindableFunction = Instance.new("BindableFunction")
local getAllValuesFunction: BindableFunction = Instance.new("BindableFunction")
local setValueFunction: BindableFunction = Instance.new("BindableFunction")
local valueChangedEvent: BindableEvent = Instance.new("BindableEvent")

---

getVersionFunction.OnInvoke = function(): number
    return ColorPaneUserDataInterfaceVersion
end

getValueFunction.OnInvoke = function(key: string): any
    return colorPaneUserData:getValue(key)
end

getAllValuesFunction.OnInvoke = function(): CommonTypes.ColorPaneUserData
    return colorPaneUserData:getAllValues()
end

setValueFunction.OnInvoke = function(key: string, value: any): boolean
    return colorPaneUserData:setValue(key, value)
end

local valueChangedSubscription = colorPaneUserData.valueChanged:subscribe(function(value: CommonTypes.KeyValue)
    valueChangedEvent:Fire(value)
end)

---

interfaceFolder.Name = CommonConstants.COLORPANE_USERDATA_INTERFACE_NAME
getVersionFunction.Name = "GetVersion"
getValueFunction.Name = "GetValue"
getAllValuesFunction.Name = "GetAllValues"
setValueFunction.Name = "SetValue"
valueChangedEvent.Name = "ValueChanged"

interfaceFolder.Archivable = false
getVersionFunction.Archivable = false
getValueFunction.Archivable = false
getAllValuesFunction.Archivable = false
setValueFunction.Archivable = false
valueChangedEvent.Archivable = false

getVersionFunction.Parent = interfaceFolder
getValueFunction.Parent = interfaceFolder
getAllValuesFunction.Parent = interfaceFolder
setValueFunction.Parent = interfaceFolder
valueChangedEvent.Parent = interfaceFolder

-- check if the interface is valid before we put it out into the world
do
    local interfaceIsValid: boolean, invalidReason: string? = ColorPaneUserDataInterfaceValidator(interfaceFolder)

    if (not interfaceIsValid) then
        error("[CPCompanion] The user data interface is invalid: " .. invalidReason::string)
    else
        interfaceFolder.Parent = StudioService
    end
end

plugin.Unloading:Connect(function()
    valueChangedSubscription:unsubscribe()
    getVersionFunction:Destroy()
    getValueFunction:Destroy()
    getAllValuesFunction:Destroy()
    setValueFunction:Destroy()
    valueChangedEvent:Destroy()
    interfaceFolder:Destroy()
end)

return nil


--!strict
--[[
    Provides the default user data values for Companion user data.
]]

local Modules = script.Parent
local Types = require(Modules.Types)

---

return {
    AutoLoadColorPropertiesAPIData = false,
    CacheColorPropertiesAPIData = false,
    RobloxApiDump = nil,
}::Types.CompanionUserData


--!strict
--[[
    Defines the set of validators for Companion user data values.
]]

local root = script.Parent.Parent

local Common = root.Common
local CommonIncludes = Common.Includes
local t = require(CommonIncludes.t)

local Modules = root.Modules
local Enums = require(Modules.Enums)

---

return {
    --[[
        Checks if a value is valid for the AutoLoadColorPropertiesAPIData value.

        @param value The value to check
        @return If the value is valid
        @return An error message, if the value was invalid
    ]]
    [Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData] = t.boolean,

    --[[
        Checks if a value is valid for the CacheColorPropertiesAPIData value.

        @param value The value to check
        @return If the value is valid
        @return An error message, if the value was invalid
    ]]
    [Enums.CompanionUserDataKey.CacheColorPropertiesAPIData] = t.boolean,

    --[[
        Checks if a value is valid for the RobloxApiDump value.

        @param value The value to check
        @return If the value is valid
        @return An error message, if the value was invalid
    ]]
    [Enums.CompanionUserDataKey.RobloxApiDump] = t.optional(t.table),
}


--!strict
--[[
    Companion plugin constants.
]]

return {
    COLORPANE_USERDATA_KEY = "ColorPane_UserData",
    COMPANION_USERDATA_KEY = "ColorPaneCompanion_UserData",
    META_UPDATE_SOURCE_KEY = "__updateSource",
}


--!strict

local root = script.Parent.Parent
local Common = root.Common
local CommonModules = Common.Modules
local PluginProvider = require(CommonModules.PluginProvider)

---

local plugin: Plugin = PluginProvider()
local pluginMenu = plugin:CreatePluginMenu("ColorPane", "ColorPane")
local showDocumentationPluginAction = pluginMenu:AddNewAction("ColorProperties_ShowDocumentation", "Show Documentation")

---

local DocumentationPluginMenu = {}
DocumentationPluginMenu.Menu = pluginMenu
DocumentationPluginMenu.Action = showDocumentationPluginAction

DocumentationPluginMenu.ShowPropertyDocumentation = function(className: string, propertyName: string)
    -- The URL that actually opens is developer.roblox.com (the old wiki)
    -- and not create.roblox.com/docs (the current wiki)
    -- For properties on the old wiki, the URL is
    --   developer.roblox/com/api-reference/property/CLASS_NAME/PROPERTY_NAME
    -- which will be properly redirected (for now)
    plugin:OpenWikiPage("api-reference/property/" .. className .. "/" .. propertyName)
end

---

return DocumentationPluginMenu


--!strict
--[[
    Enums used in the Companion plugin.
]]

return {
    CompanionUserDataKey = {
        AutoLoadColorPropertiesAPIData = "AutoLoadColorPropertiesAPIData",
        CacheColorPropertiesAPIData = "CacheColorPropertiesAPIData",
        RobloxApiDump = "RobloxApiDump",
    }
}


--!strict
--[[
    Manages the initialisation and synchronisation of user data
]]

local root = script.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Cryo = require(CommonIncludes.Cryo)
local t = require(CommonIncludes.t)

local CommonModules = Common.Modules
local ColorPaneUserDataDefaultValues = require(CommonModules.ColorPaneUserDataDefaultValues)
local ColorPaneUserDataDiffs = require(CommonModules.ColorPaneUserDataDiffs)
local ColorPaneUserDataFactory = require(CommonModules.ColorPaneUserDataFactory)
local ColorPaneUserDataValidators = require(CommonModules.ColorPaneUserDataValidators)
local CommonEnums = require(CommonModules.Enums)
local CommonTypes = require(CommonModules.Types)
local PluginProvider = require(CommonModules.PluginProvider)
local UserData = require(CommonModules.UserData)
local Util = require(CommonModules.Util)

local Modules = root.Modules
local CompanionUserDataDefaultValues = require(Modules.CompanionUserDataDefaultValues)
local CompanionUserDataValidators = require(Modules.CompanionUserDataValidators)
local Constants = require(Modules.Constants)
local Enums = require(Modules.Enums)
local Types = require(Modules.Types)
local UserDataSynchroniser = require(Modules.UserDataSynchroniser)

---

local LEGACY_USERDATA_KEY: string = "ColorPane_Settings"
local LEGACY_USERDATA_BACKUP_KEY: string = "ColorPane_Settings_Backup"

local colorPaneUserDataObj: UserData.UserData
local companionUserDataObj: UserData.UserData
local plugin: Plugin = PluginProvider()

--[[
    Migrates legacy ColorPane user data to the current format,
    and returns the user data values.

    @param legacyUserData The user data in an old format
    @return The set of ColorPane user data values
    @return The set of Companion user data values
]]
local migrateLegacyData = function(legacyUserData): (CommonTypes.ColorPaneUserData, Types.CompanionUserData)
    -- we shouldn't directly modify this data
    legacyUserData = Util.table.deepCopy(legacyUserData)

    local isValid: boolean, failReason: string? = t.interface({
        AskNameBeforePaletteCreation = t.optional(ColorPaneUserDataValidators.AskNameBeforePaletteCreation),
        SnapValue = t.optional(ColorPaneUserDataValidators.SnapValue),
        UserPalettes = t.optional(ColorPaneUserDataValidators.UserColorPalettes),
        UserGradients = t.optional(ColorPaneUserDataValidators._userGradients),
        AutoLoadColorProperties = t.optional(t.boolean),
        CacheAPIData = t.optional(t.boolean),
    })(legacyUserData)

    if (isValid) then
        local newColorPaneValues: CommonTypes.ColorPaneUserData = {
            AskNameBeforePaletteCreation = legacyUserData.AskNameBeforePaletteCreation,
            SnapValue = legacyUserData.SnapValue,
            UserColorPalettes = legacyUserData.UserPalettes,
            UserGradientPalettes = {},
        }

        local newCompanionValues: Types.CompanionUserData = {
            AutoLoadColorPropertiesAPIData = legacyUserData.AutoLoadColorProperties,
            CacheColorPropertiesAPIData = legacyUserData.CacheAPIData,
        }

        if (legacyUserData.UserGradients) then
            local newPalette = {
                name = "(Transferred from ColorPane)",
                gradients = {}
            }

            for i, gradient in ipairs(legacyUserData.UserGradients) do
                for _, keypoint in ipairs(gradient.keypoints) do
                    keypoint.time = keypoint.Time
                    keypoint.color = keypoint.Color

                    keypoint.Time = nil
                    keypoint.Color = nil
                end

                newPalette.gradients[i] = gradient
            end

            newColorPaneValues.UserGradientPalettes = {newPalette}
        end

        return newColorPaneValues, newCompanionValues
    else
        error("Could not migrate old ColorPane user data: " .. failReason::string)
    end
end

--[[
    Initialises ColorPane user data.

    @param userData The current ColorPane user data
    @return The user's ColorPane data
    @return If the current ColorPane data should be overwritten
]]
local initColorPaneUserData = function(userData, colorPaneDefaultUserDataValues): (CommonTypes.ColorPaneUserData, boolean)
    if (userData == nil) then
        return colorPaneDefaultUserDataValues, true
    elseif (type(userData) ~= "table") then
        warn("ColorPane user data is invalid and will be re-created")
        return colorPaneDefaultUserDataValues, true
    else
        local modified: boolean = false
        userData[Constants.META_UPDATE_SOURCE_KEY] = nil

        -- check for extraneous values
        for key: string in pairs(userData) do
            if (CommonEnums.ColorPaneUserDataKey[key] == nil) then
                warn("extraneous ColorPane user data key " .. key .. " will be dropped")
                userData[key] = nil
                modified = true
            end
        end

        -- check for missing or invalid values
        for key: string in pairs(CommonEnums.ColorPaneUserDataKey) do
            local isValid: boolean, failReason: string? =
                t.optional(ColorPaneUserDataValidators[key])(userData[key])

            if (not isValid) then
                if (key == CommonEnums.ColorPaneUserDataKey.UserColorPalettes) then
                    local palettes = userData[key]
                    local isArrayOfThings: boolean = t.array(t.any)(palettes)

                    -- check if the value is an array
                    if (not isArrayOfThings) then
                        userData[key] = nil
                    else
                        -- check which elements are non-conformant
                        for i = #palettes, 1, -1 do
                            local palette = palettes[i]
                            local isPalette: boolean = ColorPaneUserDataValidators.ColorPalette(palette)

                            if (not isPalette) then
                                table.remove(palettes, i)
                                warn("ColorPane color palette at index " .. i .. " is invalid and will be removed")
                            end
                        end
                    end
                elseif (key == CommonEnums.ColorPaneUserDataKey.UserGradientPalettes) then
                    local palettes = userData[key]
                    local isArrayOfThings: boolean = t.array(t.any)(palettes)

                    -- check if the value is an array
                    if (not isArrayOfThings) then
                        userData[key] = nil
                    else
                        -- check which elements are non-conformant
                        for i = #palettes, 1, -1 do
                            local palette = palettes[i]
                            local isPalette: boolean = ColorPaneUserDataValidators.GradientPalette(palette)

                            if (not isPalette) then
                                table.remove(palettes, i)
                                warn("ColorPane gradient palette at index " .. i .. " is invalid and will be removed")
                            end
                        end
                    end
                else
                    userData[key] = nil
                    warn("ColorPane user data value " .. key .. " is invalid and will be replaced, reason is: " .. failReason::string)
                end

                modified = true
            elseif ((userData[key] == nil) and (colorPaneDefaultUserDataValues[key] ~= nil)) then
                modified = true
            end
        end

        if (modified) then
            return Cryo.Dictionary.join(colorPaneDefaultUserDataValues, userData), true
        else
            return userData, false
        end
    end
end

--[[
    Initialises Companion user data.

    @param userData The current Companion user data
    @return The user's Companion data
    @return If the current Companion data should be overwritten
]]
local initCompanionUserData = function(userData, companionDefaultUserDataValues): (Types.CompanionUserData, boolean)
    if (userData == nil) then
        return companionDefaultUserDataValues, false
    elseif (type(userData) ~= "table") then
        warn("Companion user data is invalid and will be re-created")
        return companionDefaultUserDataValues, false
    else
        local modified: boolean = false
        userData[Constants.META_UPDATE_SOURCE_KEY] = nil

        -- check for extraneous values
        for key: string in pairs(userData) do
            if (Enums.CompanionUserDataKey[key] == nil) then
                warn("extraneous Companion user data key " .. key .. " will be dropped")
                userData[key] = nil
                modified = true
            end
        end

        -- check for missing or invalid values
        for key: string in pairs(Enums.CompanionUserDataKey) do
            local isValid: boolean, failReason: string? =
                t.optional(CompanionUserDataValidators[key])(userData[key])

            if (not isValid) then
                userData[key] = nil
                modified = true
                warn("Companion user data value " .. key .. " is invalid and will be replaced, reason is: " .. failReason::string)
            elseif ((userData[key] == nil) and (companionDefaultUserDataValues[key] ~= nil)) then
                modified = true
            end
        end

        if (modified) then
            return Cryo.Dictionary.join(companionDefaultUserDataValues, userData), true
        else
            return userData, false
        end
    end
end

--[[
    Initialises all user data.

    @return The user's ColorPane data
    @return If the current ColorPane data should be overwritten
    @return The user's Companion data
    @return If the current Companion user data should be overwritten
]]
local initUserData = function(): (CommonTypes.ColorPaneUserData, boolean, Types.CompanionUserData, boolean)
    local colorPaneDefaultUserDataValues = Util.table.deepCopy(ColorPaneUserDataDefaultValues)
    local companionDefaultUserDataValues = Util.table.deepCopy(CompanionUserDataDefaultValues)
    local legacyUserData = plugin:GetSetting(LEGACY_USERDATA_KEY)

    -- if there's legacy user data, we'll migrate it
    -- and skip the rest of the initialisation
    if (legacyUserData) then
        local colorPaneUserData, companionUserData = migrateLegacyData(legacyUserData)

        plugin:SetSetting(LEGACY_USERDATA_BACKUP_KEY, legacyUserData)
        plugin:SetSetting(LEGACY_USERDATA_KEY, nil)

        print("Successfully migrated user data, a backup is available under the setting key "
            .. LEGACY_USERDATA_BACKUP_KEY .. " if something went wrong")

        return
            Cryo.Dictionary.join(colorPaneDefaultUserDataValues, colorPaneUserData), true,
            Cryo.Dictionary.join(companionDefaultUserDataValues, companionUserData), true
    end

    -- the rest of the initialisation
    local savedColorPaneUserData = plugin:GetSetting(Constants.COLORPANE_USERDATA_KEY)
    local savedCompanionUserData = plugin:GetSetting(Constants.COMPANION_USERDATA_KEY)

    local colorPaneUserData, overwriteColorPaneUserData =
        initColorPaneUserData(savedColorPaneUserData, colorPaneDefaultUserDataValues)

    local companionUserData, overwriteCompanionUserData =
        initCompanionUserData(savedCompanionUserData, companionDefaultUserDataValues)

    return
        colorPaneUserData, overwriteColorPaneUserData,
        companionUserData, overwriteCompanionUserData
end

---

do
    local colorPaneUserData, colorPaneInitialWrite,
        companionUserData, companionInitialWrite = initUserData()

    colorPaneUserDataObj = ColorPaneUserDataFactory(colorPaneUserData)

    companionUserDataObj = UserData.new(
        Enums.CompanionUserDataKey,
        CompanionUserDataValidators,
        {},
        companionUserData
    )

    UserDataSynchroniser.new(
        colorPaneUserDataObj,
        Constants.COLORPANE_USERDATA_KEY,
        ColorPaneUserDataDiffs.GetModifiedValues,
        colorPaneInitialWrite
    )

    UserDataSynchroniser.new(
        companionUserDataObj,
        Constants.COMPANION_USERDATA_KEY,

        function(this, that)
            local modifiedValues = {}

            for key in pairs(Enums.CompanionUserDataKey) do
                if (key == Enums.CompanionUserDataKey.RobloxApiDump) then
                    if (
                        -- API dump tables are very large, so we'll avoid comparing them
                        -- and just check if one is nil and one isn't nil
                        -- (this definitely won't come back to haunt me)
                        ((type(this[key]) == "table") and (type(that[key]) == "nil")) or
                        ((type(this[key]) == "nil") and (type(that[key]) == "table"))
                    ) then
                        modifiedValues[key] = that[key]
                    end
                else
                    if (this[key] ~= that[key]) then
                        modifiedValues[key] = that[key]
                    end
                end
            end

            return modifiedValues
        end,

        companionInitialWrite
    )
end

return {
    ColorPane = colorPaneUserDataObj,
    Companion = companionUserDataObj,
}


local ChangeHistoryService = game:GetService("ChangeHistoryService")
local Selection = game:GetService("Selection")

---

local root = script.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Signal = require(CommonIncludes.Signal)

local CommonModules = Common.Modules
local PluginProvider = require(CommonModules.PluginProvider)

local Modules = root.Modules
local ColorAPIData = require(Modules.ColorAPIData)
local RobloxAPI = require(Modules.RobloxAPI)

---

local plugin: Plugin = PluginProvider()
local apiIsReady = false
local recordingId: string?

local currentSelection: {Instance} = {}
local currentSelectionProperties = {}
local currentSelectionCommonPropertyValues = {}
local currentSelectionPropertyValuesChangedConnections = {}

local dataRequestFinishedSubscription

local internalSelectionChanged
local selectionChangedSignal: Signal.Signal<nil>, fireSelectionChanged: Signal.FireSignal<nil> = Signal.createSignal()
local selectionColorsChangedSignal: Signal.Signal<nil>, fireSelectionColorsChanged: Signal.FireSignal<nil> = Signal.createSignal()

local getSafeSelection = function(): {Instance}
    local selection: {Instance} = Selection:Get()

    for i = #selection, 1, -1 do
        local obj = selection[i]

        --[[
            Make sure we pass the security check and ClassName isn't blank
            (because apparently that's a thing)
        ]]
        local passesSecurityCheck, validClassName = pcall(function()
            return (obj.ClassName ~= "")
        end)

        if (not (passesSecurityCheck and validClassName)) then
            table.remove(selection, i)
        end
    end

    return selection
end

local getSelectionColorProperties = function(selection)
    local classes = {}

    for i = 1, #selection do
        local className = selection[i].ClassName

        if (not table.find(classes, className)) then
            table.insert(classes, className)
        end
    end

    return ColorAPIData.GetProperties(classes)
end

local generateSelectionCommonColorPropertyValue = function(className: string, propertyName: string)
    local propertyInfo = ColorAPIData.GetProperty(className, propertyName)
    if (not propertyInfo) then return end

    local anchorValue

    for i = 1, #currentSelection do
        local object = currentSelection[i]

        if (object:IsA(className)) then
            local objectValue = propertyInfo.Custom and propertyInfo.Get(object) or object[propertyName]

            if (anchorValue == nil) then
                anchorValue = objectValue
            elseif (anchorValue ~= objectValue) then
                anchorValue = false
                break
            end
        end
    end

    if (anchorValue) then
        return ColorAPIData.TransformPropertyValue(className, propertyName, anchorValue)
    else
        return nil
    end
end

local generateSelectionCommonColorPropertyValues = function(properties)
    local values = {}

    for className, classProperties in pairs(properties) do
        for propertyName, propertyInfo in pairs(classProperties) do
            local anchorValue

            for i = 1, #currentSelection do
                local object = currentSelection[i]

                if (object:IsA(className)) then
                    local objectValue = propertyInfo.Custom and propertyInfo.Get(object) or object[propertyName]

                    if (anchorValue == nil) then
                        anchorValue = objectValue
                    elseif (anchorValue ~= objectValue) then
                        anchorValue = false
                        break
                    end
                end
            end

            if (anchorValue) then
                if (not values[className]) then
                    values[className] = {}
                end

                values[className][propertyName] = ColorAPIData.TransformPropertyValue(className, propertyName, anchorValue)
            end
        end
    end

    return values
end

---

local SelectionManager = {}

local onSelectionChanged = function()
    currentSelection = getSafeSelection()
    currentSelectionProperties = getSelectionColorProperties(currentSelection)
    currentSelectionCommonPropertyValues = generateSelectionCommonColorPropertyValues(currentSelectionProperties)

    for i = 1, #currentSelectionPropertyValuesChangedConnections do
        currentSelectionPropertyValuesChangedConnections[i]:Disconnect()
    end

    table.clear(currentSelectionPropertyValuesChangedConnections)

    for className, classProperties in pairs(currentSelectionProperties) do
        for propertyName, propertyInfo in pairs(classProperties) do
            for i = 1, #currentSelection do
                local object: Instance = currentSelection[i]

                if (object:IsA(className) and (not propertyInfo.Custom)) then
                    -- update common properties table
                    table.insert(currentSelectionPropertyValuesChangedConnections, object:GetPropertyChangedSignal(propertyName):Connect(function()
                        local newCommonValue = generateSelectionCommonColorPropertyValue(className, propertyName)

                        if (newCommonValue and (not currentSelectionCommonPropertyValues[className])) then
                            currentSelectionCommonPropertyValues[className] = {}
                        elseif ((not newCommonValue) and (not currentSelectionCommonPropertyValues[className])) then
                            return
                        end

                        currentSelectionCommonPropertyValues[className][propertyName] = newCommonValue
                        fireSelectionColorsChanged()
                    end))
                end
            end
        end
    end

    fireSelectionChanged()
end

SelectionManager.SelectionChanged = selectionChangedSignal
SelectionManager.SelectionColorsChanged = selectionColorsChangedSignal

SelectionManager.Connect = function()
    if (internalSelectionChanged) then return end

    internalSelectionChanged = Selection.SelectionChanged:Connect(onSelectionChanged)
    onSelectionChanged()
end

SelectionManager.Disconnect = function()
    if (not internalSelectionChanged) then return end

    for i = 1, #currentSelectionPropertyValuesChangedConnections do
        currentSelectionPropertyValuesChangedConnections[i]:Disconnect()
    end

    table.clear(currentSelectionPropertyValuesChangedConnections)

    internalSelectionChanged:Disconnect()
    internalSelectionChanged = nil
end

SelectionManager.GetSelectionCommonColorPropertyValues = function(): {[string]: {[string]: any}}
    return currentSelectionCommonPropertyValues
end

SelectionManager.GetSelectionColorPropertyData = function()
    if (not apiIsReady) then
        return {
            Properties = {},
            Duplicated = {},
            Sorted = {},
        }
    end

    local duplicateProperties = {}
    local sortedProperties = {}

    for className, classProperties in pairs(currentSelectionProperties) do
        for propertyName in pairs(classProperties) do
            if (duplicateProperties[propertyName] == nil) then
                for otherClassName, otherClassProperties in pairs(currentSelectionProperties) do
                    if (otherClassName == className) then continue end

                    if otherClassProperties[propertyName] then
                        duplicateProperties[propertyName] = true
                        break
                    end
                end

                if (duplicateProperties[propertyName] == nil) then
                    duplicateProperties[propertyName] = false
                end
            end

            table.insert(sortedProperties, {className, propertyName})
        end
    end

    table.sort(sortedProperties, function(a, b)
        if (a[2] ~= b[2]) then
            return a[2] < b[2]
        else
            return a[1] < b[1]
        end
    end)

    return {
        Properties = currentSelectionProperties,
        Duplicated = duplicateProperties,
        Sorted = sortedProperties,
    }
end

SelectionManager.BeginRecording = function()
    assert(recordingId == nil, "Recording already started")

    recordingId = ChangeHistoryService:TryBeginRecording( "Change color properties")
    assert(recordingId ~= nil, "Could not begin recording")
end

SelectionManager.StopRecording = function(operation: Enum.FinishRecordingOperation)
    assert(recordingId, "Recording has not started")

    ChangeHistoryService:FinishRecording(recordingId, operation)
    recordingId = nil
end

SelectionManager.SetSelectionProperty = function(className: string, propertyName: string, newColor: any)
    if (not recordingId) then return end

    local propertyInfo = ColorAPIData.GetProperty(className, propertyName)
    if (not propertyInfo) then return end

    for i = 1, #currentSelection do
        local object: Instance = currentSelection[i]

        if (object:IsA(className)) then
            local transformedColor = ColorAPIData.TransformColorValue(className, propertyName, newColor)

            if (propertyInfo.Custom) then
                propertyInfo.Set(object, transformedColor)
            else
                object[propertyName] = transformedColor
            end
        end
    end
end

---

dataRequestFinishedSubscription = RobloxAPI.DataRequestFinished:subscribe(function(didLoad)
    if (not didLoad) then return end

    apiIsReady = true
    onSelectionChanged()
end)

plugin.Unloading:Connect(function()
    if (recordingId) then
        SelectionManager.StopRecording(Enum.FinishRecordingOperation.Cancel)
    end

    dataRequestFinishedSubscription:unsubscribe()
    SelectionManager.Disconnect()
end)

return SelectionManager


--!strict
--[[
    Companion store, required by Window.
    (Only keeps track of the theme.)
]]

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local PluginProvider = require(CommonModules.PluginProvider)

local CommonIncludes = Common.Includes
local Rodux = require(CommonIncludes.RoactRodux.Rodux)

---

type table = {[any]: any}

local Studio: Studio = settings().Studio

local plugin: Plugin = PluginProvider()
local themeChanged: RBXScriptConnection

local Store = Rodux.Store.new(Rodux.createReducer({
    theme = Studio.Theme,
}, {
    SetTheme = function(_: table, action: table): table
        return {
            theme = action.theme,
        }
    end,
}))

---

themeChanged = Studio.ThemeChanged:Connect(function()
    Store:dispatch({
        type = "SetTheme",
        theme = Studio.Theme,
    })
end)

plugin.Unloading:Connect(function()
    themeChanged:Disconnect()
end)

return Store


--!strict
--[[
    Constructs the plugin toolbar and buttons.
]]

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local PluginProvider = require(CommonModules.PluginProvider)
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)

---

local plugin: Plugin = PluginProvider()
local toolbar: PluginToolbar = plugin:CreateToolbar("ColorPane Companion")

---

return {
    --[[
        The plugin toolbar.
    ]]
    Toolbar = toolbar,

    --[[
        Button for opening the color editor.
    ]]
    ColorEditButton = toolbar:CreateButton(
        "CPCompanion_ColorEditor",
        Translator.FormatByKey("ColorEditor_ToolbarButtonHintText"),
        Style.Images.ColorEditorToolbarButtonIcon,
        Translator.FormatByKey("ColorEditor_ToolbarButtonText")
    ),

    --[[
        Button for opening the gradient editor.
    ]]
    GradientEditButton = toolbar:CreateButton(
        "CPCompanion_CSEditor",
        Translator.FormatByKey("GradientEditor_ToolbarButtonHintText"),
        Style.Images.GradientEditorToolbarButtonIcon,
        Translator.FormatByKey("GradientEditor_ToolbarButtonText")
    ),

    --[[
        Button for the color properties panel.
    ]]
    ColorPropertiesButton = toolbar:CreateButton(
        "CPCompanion_ColorProperties",
        Translator.FormatByKey("ColorProperties_ToolbarButtonHintText"),
        Style.Images.ColorPropertiesToolbarButtonIcon,
        Translator.FormatByKey("ColorProperties_ToolbarButtonText")
    ),

    --[[
        Button to configure certain settings.
    ]]
    SettingsButton = toolbar:CreateButton(
        "CPCompanion_Settings",
        Translator.FormatByKey("Settings_ToolbarButtonHintText"),
        Style.Images.SettingsToolbarButtonIcon,
        Translator.FormatByKey("Settings_ToolbarButtonText")
    ),
}


--!strict

--[[
    User data values for the Companion plugin.
]]
export type CompanionUserData = {
    AutoLoadColorPropertiesAPIData: boolean,
    CacheColorPropertiesAPIData: boolean,
    RobloxApiDump: {}?,
}

---

return {}


--!strict
--[[
    Class for UserData synchronisation with
    plugin settings.
]]

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

---

local root = script.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Cryo = require(CommonIncludes.Cryo)
local Signal = require(CommonIncludes.Signal)

local CommonModules = Common.Modules
local PluginProvider = require(CommonModules.PluginProvider)
local Types = require(CommonModules.Types)
local UserData = require(CommonModules.UserData)

local Modules = root.Modules
local Constants = require(Modules.Constants)

---

type UserData = UserData.UserData
type Values = Types.UserDataValues

type UserDataSynchroniserImpl = {
    __index: UserDataSynchroniserImpl,

    --[[
        Pulls user data from the plugin settings,
        and updates the current user data values accordingly.

        @param self The user data synchroniser
    ]]
    _pullSettings: (UserDataSynchroniser) -> (),

    --[[
        Writes the current user data values to the plugin settings.

        @param self The user data synchroniser
    ]]
    _writeSettings: (UserDataSynchroniser) -> (),

    --[[
        Creates a new synchroniser for a UserData.

        @param userData The UserData object
        @param pluginSettingKey The plugin setting key to write settings to
        @param diffCallback Callback to determine which values to use when two versions of the data differ
        @param initialWrite If the saved user data should be overwritten by `userData`
        @return A user data synchroniser
    ]]
    new: (UserData, string, (Values, Values) -> Values, boolean?) -> UserDataSynchroniser,
}

export type UserDataSynchroniser = typeof(setmetatable(
    {}::{
        _userData: UserData,
        _pluginSettingKey: string,
        _diffCallback: (Values, Values) -> Values,

        _synchroniserId: string,
        _valueChangedSubscription: Signal.Subscription,
        _heartbeat: RBXScriptConnection,
        _lastSyncTime: number,
        _syncing: boolean,
    },

    {}::UserDataSynchroniserImpl
))

---

--[[
    The amount of time in seconds between user data syncs.
]]
local SYNC_FREQUENCY: number = 0.5

local plugin: Plugin = PluginProvider()

---

local UserDataSynchroniser: UserDataSynchroniserImpl = {}::UserDataSynchroniserImpl
UserDataSynchroniser.__index = UserDataSynchroniser

UserDataSynchroniser._pullSettings = function(self: UserDataSynchroniser)
    local userData = self._userData
    local freshUserData = plugin:GetSetting(self._pluginSettingKey)
    local freshSynchroniserId: string = freshUserData[Constants.META_UPDATE_SOURCE_KEY]

    if (freshSynchroniserId ~= self._synchroniserId) then
        -- only update values if the changes came from somewhere else
        local modifiedSettings = self._diffCallback(userData:getAllValues(), freshUserData)

        for modifiedKey, modifiedValue in pairs(modifiedSettings) do
            userData:setValue(modifiedKey, modifiedValue)
        end
    end
end

UserDataSynchroniser._writeSettings = function(self: UserDataSynchroniser)
    local writtenUserData = Cryo.Dictionary.join(self._userData:getAllValues(), {
        [Constants.META_UPDATE_SOURCE_KEY] = self._synchroniserId
    })

    plugin:SetSetting(self._pluginSettingKey, writtenUserData)
end

UserDataSynchroniser.new = function(
    userData: UserData,
    pluginSettingKey: string,
    diffCallback: (Values, Values) -> Values,
    initialWrite: boolean?
)
    local self = setmetatable({
        _userData = userData,
        _pluginSettingKey = pluginSettingKey,
        _diffCallback = diffCallback,

        _synchroniserId = HttpService:GenerateGUID(false),
        _valueChangedSubscription = Signal.createSignal():subscribe(function() end),
        _heartbeat = RunService.Heartbeat:Connect(function() end),
        _lastSyncTime = -1,
        _syncing = false,
    }, UserDataSynchroniser)

    -- these connections will be replaced
    self._valueChangedSubscription:unsubscribe()
    self._heartbeat:Disconnect()

    if (initialWrite) then
        self:_writeSettings()
    end

    self._valueChangedSubscription = userData.valueChanged:subscribe(function()
        self:_writeSettings()
    end)

    self._heartbeat = RunService.Heartbeat:Connect(function()
        if (self._syncing) then return end
        if ((os.clock() - self._lastSyncTime) < SYNC_FREQUENCY) then return end

        self._syncing = true
        self:_pullSettings()

        self._lastSyncTime = os.clock()
        self._syncing = false
    end)

    plugin.Unloading:Connect(function()
        self._valueChangedSubscription:unsubscribe()
        self._heartbeat:Disconnect()
    end)

    return self
end

---

return UserDataSynchroniser


--!strict
--[[
    Holds information for plugin widgets.
]]

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local Style = require(CommonModules.Style)

---

return {
    Settings = {
        Id = "CPCompanion_Settings",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, Style.Constants.EditorPageWidth, 200, Style.Constants.EditorPageWidth, 200),
    },

    RestoreDefaultsPrompt = {
        Id = "CPCompanion_RestoreDefaultsPrompt",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, true, 300, 120, 300, 120),
    },

    ColorProperties = {
        Id = "CPCompanion_ColorProperties",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, Style.Constants.EditorPageWidth, 400, Style.Constants.EditorPageWidth, 200),
    },

    ImportSettings = {
        Id = "CPCompanion_SettingsImport",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 240, 260, 240, 260),
    },

    ExportSettings = {
        Id = "CPCompanion_SettingsExport",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 200, 100, 200, 100),
    },

    ConfirmImportSettingsPrompt = {
        Id = "CPCompanion_SettingsImportConfirm",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 220, 200, 200, 200),
    }
}


--!strict
local fs = zune.fs;

local reflection = require("../src/reflection");

local api = reflection:GetApiDump();

local INDENT = "    ";

local HIDDEN = {};

local MemberValueTypeOverride = {
    ["Content"] = "any",
    ["Instance"] = "any",
    ["ContentId"] = "any",
    ["Map"] = "any",
    ["Variant"] = "any", -- Function

    ["ProtectedString"] = "string",
    ["Path2DControlPoint"] = HIDDEN,

    ["Dictionary"] = "{[string]: any}", -- Function
    ["null"] = "()", -- Function
    ["Array"] = "{any}", -- Function
    ["Tuple"] = "...any", -- Function

    ["string"] = "string",
    ["BinaryString"] = "buffer",
    ["int64"] = "buffer",
    ["int"] = "number",
    ["float"] = "number",
    ["double"] = "number",
    ["bool"] = "boolean",
    ["SharedString"] = HIDDEN,
    ["UniqueId"] = HIDDEN,
    ["SecurityCapabilities"] = HIDDEN,
};

local supported_datatypes = {};

for _, file in fs.entries("src/datatypes") do
    if (zune.mem.endsWith(file.name, ".luau")) then
        local name = file.name:sub(1, -6);
        supported_datatypes[name] = true;
    end
end

local function getSupportedType(valueType: reflection.TypeInfo): any?
    local optional = valueType.Name:find("?") ~= nil;
    local searchName = valueType.Name:gsub("?", "");
    local override = MemberValueTypeOverride[searchName];
    if (override) then
        return override;
    end
    if (valueType.Category == "Enum") then
        return `enum.Enum_{valueType.Name}{if (optional) then "?" else ""}`;
    elseif (valueType.Category == "Class") then
        return `{valueType.Name}{if (optional) then "?" else ""}`;
    elseif (valueType.Category == "DataType") then
        if (supported_datatypes[searchName]) then
            return `datatypes.{searchName}{if (optional) then "?" else ""}`;
        else
            print(`[type-gen] Unsupported datatype: {searchName}`);
            return;
        end
    else
        return `{valueType.Name}{if (optional) then "?" else ""}`;
    end
end

local function generateObjectMembersType(class: reflection.ClassInfo): string
    local root = class.Superclass == "<<<ROOT>>>"
    local lines = {
        `export type {class.Name} = {if (not root) then `{class.Superclass} & ` else ""}\{`
    };
    for _, member in class.Members do
        if (member.Tags and table.find(member.Tags, "Deprecated")) then
            continue;
        end
        if (member.MemberType == "Event") then
            -- Optional?
        elseif (member.MemberType == "Property") then
            local override = getSupportedType(member.ValueType);
            if (override == HIDDEN or not override) then
                continue;
            end
            local member_name = member.Name;
            if (member_name:find("[%s\"]")) then
                member_name = `["{member_name:gsub('"', "\\\"")}"]`;
            end
            table.insert(
                lines,
                `{INDENT}{member_name}: {override or member.ValueType.Name},`
            );
        elseif (member.MemberType == "Function") then
            local params = {};
            for _, param in member.Parameters do
                local override = getSupportedType(param.Type);
                if (override == HIDDEN or not override) then
                    override = "any";
                end
                if (override == "...any") then
                    table.insert(params, `...any`);
                    continue;
                end
                local name = param.Name;
                if (param.Name == "function") then
                    name = "callback";
                end
                table.insert(
                    params,
                    `{name}: {override or param.Type.Name}`
                );
            end
            local params_types = "";
            if (#params > 0) then
                params_types = `, {table.concat(params, ", ")}`;
            end
            local return_type = "()";
            if (#member.ReturnType == 0) then
                return_type = getSupportedType(member.ReturnType::any)::any;
                if (return_type::any == HIDDEN or not return_type) then
                    return_type = "any";
                end
            else
                return_type = "...any";
            end
            table.insert(
                lines,
                `{INDENT}{member.Name}: (self: {class.Name}{params_types}) -> {return_type},`
            );
        end
    end
    table.insert(lines, `}\n`);
    return table.concat(lines, "\n");
end

local function generateInstanceNewTypes()
    local lines = {
        `export type NewConstructor = `
    };
    for i, class in api.Classes do
        if (class.Tags and table.find(class.Tags, "NotCreatable")) then
            continue;
        end
        table.insert(lines, `{INDENT}& (("{class.Name}", parent: Instance?) -> {class.Name})`);
    end
    return table.concat(lines, "\n");
end

local lines = {
    `-- Luau code generated by types-gen.luau`,
    `--!strict`,
    `local datatypes = require("../datatypes")`,
    `local enum = require("./roblox_enums")\n`,
    `export type Enums = enum.Enums\n`,
};
local enum_lines = {
    `export type EnumItem = \{`,
    `{INDENT}Name: string,`,
    `{INDENT}Value: number,`,
    `{INDENT}EnumType: Enum,`,
    `{INDENT}IsA: (self: EnumItem, enumName: string) -> boolean,`,
    `}`,
    `export type Enum = \{`,
    `{INDENT}GetEnumItems: (self: Enum) -> \{ EnumItem \}`,
    `}`,
};
local end_enum_lines = {
    `export type Enums = \{`,
};
for i, enum in api.Enums do
    table.insert(enum_lines, `export type Enum_{enum.Name} = Enum & \{`);
    for i, item in enum.Items do
        table.insert(enum_lines, `{INDENT}{item.Name}: EnumItem,`);
    end
    table.insert(enum_lines, `}`);
    table.insert(end_enum_lines, `{INDENT}{enum.Name}: Enum_{enum.Name},`);
end
table.insert(end_enum_lines, `}`);
table.move(end_enum_lines, 1, #end_enum_lines, #enum_lines + 1, enum_lines);
table.insert(enum_lines, `return \{\};`);

for i, class in api.Classes do
    table.insert(lines, generateObjectMembersType(class));
end

table.insert(lines, generateInstanceNewTypes());
table.insert(lines, `return \{\};`);

if (fs.stat("./src/types").kind == "none") then
    fs.makeDir("./src/types");
end
fs.writeFile("./src/types/roblox_enums.luau", table.concat(enum_lines, "\n"));
fs.writeFile("./src/types/roblox.luau", table.concat(lines, "\n"));


--!strict
local datatypes = require("../datatypes");

local types = {}

type DataType = {
    new: (...any) -> any,
    load: (...any) -> any,
};

for type, datatype in datatypes do
    local dt = (datatype::any).DataType::DataType;
    if (dt and type ~= "Enums" and dt.load == nil) then
        dt.load = dt.new;
    end
    types[type::string] = dt;
end

return (types::any)::{
    Enums: any,
    [string]: DataType,
};


local Enum = require("../datatypes").Enums.DataType;

return {
    [Enum.Font.Legacy] = { family = "rbxasset://fonts/families/LegacyArial.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Arial] = { family = "rbxasset://fonts/families/Arial.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.ArialBold] = { family = "rbxasset://fonts/families/Arial.json", weight = Enum.FontWeight.Bold, style = Enum.FontStyle.Normal },
    [Enum.Font.SourceSans] = { family = "rbxasset://fonts/families/SourceSansPro.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.SourceSansBold] = { family = "rbxasset://fonts/families/SourceSansPro.json", weight = Enum.FontWeight.Bold, style = Enum.FontStyle.Normal },
    [Enum.Font.SourceSansLight] = { family = "rbxasset://fonts/families/SourceSansPro.json", weight = Enum.FontWeight.Light, style = Enum.FontStyle.Normal },
    [Enum.Font.SourceSansItalic] = { family = "rbxasset://fonts/families/SourceSansPro.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Italic },
    [Enum.Font.Bodoni] = { family = "rbxasset://fonts/families/AccanthisADFStd.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Garamond] = { family = "rbxasset://fonts/families/Guru.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Cartoon] = { family = "rbxasset://fonts/families/ComicNeueAngular.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Code] = { family = "rbxasset://fonts/families/Inconsolata.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Highway] = { family = "rbxasset://fonts/families/HighwayGothic.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.SciFi] = { family = "rbxasset://fonts/families/Zekton.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Arcade] = { family = "rbxasset://fonts/families/PressStart2P.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Fantasy] = { family = "rbxasset://fonts/families/Balthazar.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Antique] = { family = "rbxasset://fonts/families/RomanAntique.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.SourceSansSemibold] = { family = "rbxasset://fonts/families/SourceSansPro.json", weight = Enum.FontWeight.SemiBold, style = Enum.FontStyle.Normal },
    [Enum.Font.Gotham] = { family = "rbxasset://fonts/families/GothamSSm.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.GothamMedium] = { family = "rbxasset://fonts/families/GothamSSm.json", weight = Enum.FontWeight.Medium, style = Enum.FontStyle.Normal },
    [Enum.Font.GothamBold] = { family = "rbxasset://fonts/families/GothamSSm.json", weight = Enum.FontWeight.Bold, style = Enum.FontStyle.Normal },
    [Enum.Font.GothamBlack] = { family = "rbxasset://fonts/families/GothamSSm.json", weight = Enum.FontWeight.Heavy, style = Enum.FontStyle.Normal },
    [Enum.Font.AmaticSC] = { family = "rbxasset://fonts/families/AmaticSC.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Bangers] = { family = "rbxasset://fonts/families/Bangers.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Creepster] = { family = "rbxasset://fonts/families/Creepster.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.DenkOne] = { family = "rbxasset://fonts/families/DenkOne.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Fondamento] = { family = "rbxasset://fonts/families/Fondamento.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.FredokaOne] = { family = "rbxasset://fonts/families/FredokaOne.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.GrenzeGotisch] = { family = "rbxasset://fonts/families/GrenzeGotisch.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.IndieFlower] = { family = "rbxasset://fonts/families/IndieFlower.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.JosefinSans] = { family = "rbxasset://fonts/families/JosefinSans.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Jura] = { family = "rbxasset://fonts/families/Jura.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Kalam] = { family = "rbxasset://fonts/families/Kalam.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.LuckiestGuy] = { family = "rbxasset://fonts/families/LuckiestGuy.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Merriweather] = { family = "rbxasset://fonts/families/Merriweather.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Michroma] = { family = "rbxasset://fonts/families/Michroma.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Nunito] = { family = "rbxasset://fonts/families/Nunito.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Oswald] = { family = "rbxasset://fonts/families/Oswald.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.PatrickHand] = { family = "rbxasset://fonts/families/PatrickHand.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.PermanentMarker] = { family = "rbxasset://fonts/families/PermanentMarker.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Roboto] = { family = "rbxasset://fonts/families/Roboto.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.RobotoCondensed] = { family = "rbxasset://fonts/families/RobotoCondensed.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.RobotoMono] = { family = "rbxasset://fonts/families/RobotoMono.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Sarpanch] = { family = "rbxasset://fonts/families/Sarpanch.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.SpecialElite] = { family = "rbxasset://fonts/families/SpecialElite.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.TitilliumWeb] = { family = "rbxasset://fonts/families/TitilliumWeb.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.Ubuntu] = { family = "rbxasset://fonts/families/Ubuntu.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.BuilderSans] = { family = "rbxasset://fonts/families/BuilderSans.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.BuilderSansMedium] = { family = "rbxasset://fonts/families/BuilderSans.json", weight = Enum.FontWeight.Medium, style = Enum.FontStyle.Normal },
    [Enum.Font.BuilderSansBold] = { family = "rbxasset://fonts/families/BuilderSans.json", weight = Enum.FontWeight.Bold, style = Enum.FontStyle.Normal },
    [Enum.Font.BuilderSansExtraBold] = { family = "rbxasset://fonts/families/BuilderSans.json", weight = Enum.FontWeight.ExtraBold, style = Enum.FontStyle.Normal },
    [Enum.Font.Arimo] = { family = "rbxasset://fonts/families/Arimo.json", weight = Enum.FontWeight.Regular, style = Enum.FontStyle.Normal },
    [Enum.Font.ArimoBold] = { family = "rbxasset://fonts/families/Arimo.json", weight = Enum.FontWeight.Bold, style = Enum.FontStyle.Normal },
}


local Signal = require("../signal");

local function eventGenerator(name: string)
    return function(self: any)
        local event = self["@events"][name];
        if (not event) then
            event = Signal.new();
            self["@events"][name] = event;
        end
        return event;
    end
end

return {
    generate = eventGenerator;
}


--!strict

local polyfill = require("@polyfill");
local Sink = polyfill.sink;

local function decodeTags(value: string): {string}
    return value:split("\0");
end

local function encodeTags(value: {string}): string
    local count = #value;
    if (count == 0) then
        return "";
    end
    local size = count - 1;
    for _, tag in value do
        size += #tag;
    end
    local sink = Sink.create(size);
    local writer = sink:writer()
    for i, tag in value do
        writer:write(tag);
        if (i == count) then
            break;
        end
        writer:writeu8(0);
    end
    return sink:flush(false) :: string;
end

return {
    decodeTags = decodeTags,
    encodeTags = encodeTags,
}


--!strict
--!native
local polyfill = require("@polyfill");

local blake2 = polyfill.blake2;
local base64 = polyfill.base64;

local object = require("./object");

local dom = require("../dom");
local datatypes = require("../datatypes");

local reflection = require("../reflection");

local Enum = datatypes.Enums.DataType;

type Instance = any;
type DataModel = any;

local manager = {};

manager.datatypes = {
    Vector3 = datatypes.Vector3.DataType,
    Vector2 = datatypes.Vector2.DataType,
    CFrame = datatypes.CFrame.DataType,
    Color3 = datatypes.Color3.DataType,
    BrickColor = datatypes.BrickColor.DataType,
    UDim = datatypes.UDim.DataType,
    UDim2 = datatypes.UDim2.DataType,
    Ray = datatypes.Ray.DataType,
    Rect = datatypes.Rect.DataType,
    NumberRange = datatypes.NumberRange.DataType,
    NumberSequence = datatypes.NumberSequence.DataType,
    NumberSequenceKeypoint = datatypes.NumberSequenceKeypoint.DataType,
    ColorSequence = datatypes.ColorSequence.DataType,
    ColorSequenceKeypoint = datatypes.ColorSequenceKeypoint.DataType,
    PhysicalProperties = datatypes.PhysicalProperties.DataType,
    Axes = datatypes.Axes.DataType,
    Faces = datatypes.Faces.DataType,
    Enum = Enum,

    DateTime = datatypes.DateTime,
};

function manager.instanceFromDOM(instance: dom.DOMInstance): Instance?
    local className = instance.className;
    local class = reflection:GetClass(className);
    if (not class) then
        return nil
    end
    local obj = object.dom(instance);
    object.LoadDefaultProperties(obj, instance.className);
    return obj::any;
end

function manager.instancesFromDOM(doc: dom.DOM): {Instance}
    local instances = table.create(doc.info.instances);
    for _, instance in doc.instances do
        instances[instance.referent] = manager.instanceFromDOM(instance);
    end
    local valid = doc.info.instances;
    for _, instance in doc.instances do
        local object = instances[instance.referent];
        if (instance.parent) then
            local parent = instances[instance.parent];
            if (not parent) then
                print("[rbx-manager] Parent instance not found (instance dropped)");
                instances[instance.referent] = nil;
                continue;
            end
            rawset(object :: any, "@parent", parent);
            table.insert(rawget(parent :: any, "@children") :: any, object);
            valid -= 1;
        end
    end
    for _, instance in instances do
        for name, property in (instance :: any)["@instance"].properties do
            if (property.type == dom.roblox.PropertyTypes.Ref and property.value) then
                property.value = instances[property.value];
            elseif (property.type == dom.roblox.PropertyTypes.Content and property.value) then
                if (type(property.value.Object) == "number") then
                    property.value.Object = instances[property.value.Object];
                end
            elseif (property.type == dom.roblox.PropertyTypes.SharedString and property.value) then
                property.value = doc.sharedstrings[property.value];
            end
        end
    end
    local list: {Instance} = table.create(valid);
    for _, instance in instances do
        if (instance and rawget(instance :: any, "@parent") == nil) then
            table.insert(list, instance);
        end
    end
    return list;
end

function manager.modelFromDOM(doc: dom.DOM): {Instance}
    return manager.instancesFromDOM(doc);
end

function manager.placeFromDom(doc: dom.DOM): DataModel
    local datamodel = object.raw("DataModel");
    datamodel["@instance"].properties["Name"] = {
        value = "DataModel",
        type = dom.roblox.PropertyTypes.String,
    };
    local instances = manager.instancesFromDOM(doc);
    for _, instance in instances do
        rawset(instance::any, "@parent", datamodel);
        table.insert(datamodel["@children"], instance);
    end
    return datamodel::any;
end

function manager.domFromInstances(
    descendants: {Instance},
    descendants_map: {[Instance]: number},
    ignoreParent: Instance?
): dom.DOM
    local sharedstrings: {[string]: string} = {};
    local cached_sharedstrings: {[string]: string} = {};

    local instances: {dom.DOMInstance} = table.create(#descendants);
    local classes: {dom.DOMClass} = {}; do
        local map = {};
        local index = 1;
        for i, descendant in descendants do
            local instance = (descendant::any)["@instance"];
            local class_name = instance.className;
            local class: dom.DOMClass = map[class_name];
            if (not class) then
                local class_api = reflection:GetClass(class_name);
                if (not class_api) then
                    error(`Unknown class '{class_name}', cannot serialize`);
                end
                class = {
                    index = index;
                    name = class_name,
                    instances = {},
                    isService = table.find(class_api.Tags or {}, "Service") ~= nil,
                };
                index += 1;
                map[class_name] = class;
                table.insert(classes, class::any);
            end
            --[[Generate DOMInstance]] do
                local dom_instance = table.clone(instance);
                dom_instance.referent = i;
                dom_instance.properties = table.clone(dom_instance.properties);
                for name, property in instance.properties do
                    if (not property.type) then
                        continue;
                    end
                    local value = property.value;
                    if (property.type == dom.roblox.PropertyTypes.Ref and value) then
                        value = descendants_map[value];
                        if (not value) then
                            error(`Reference to non-existent instance at '{descendant:GetFullName()}' (from another DataModel?)`);
                        end
                    elseif (property.type == dom.roblox.PropertyTypes.Content and value) then
                        if (value.SourceType == Enum.ContentSourceType.Object) then
                            if (value.Object) then
                                value = descendants_map[value.Object];
                                if (not value) then
                                    error(`Content.Object Reference to non-existent instance at '{descendant:GetFullName()}' (from another DataModel?)`);
                                end
                            else
                                value = nil;
                            end
                        end
                    elseif (property.type == dom.roblox.PropertyTypes.SharedString) then
                        local data = property.value;
                        local hash_base64 = cached_sharedstrings[data];
                        if (not hash_base64) then
                            hash_base64 = blake2.b128(data, "base64");
                            cached_sharedstrings[data] = hash_base64;
                        end
                        sharedstrings[hash_base64] = data;
                        value = hash_base64;
                    else
                        continue;
                    end
                    dom_instance.properties[name] = {
                        value = value,
                        type = property.type,
                    };
                end
                local parent = rawget(descendant :: any, "@parent");
                if (parent and parent ~= ignoreParent) then
                    dom_instance.parent = descendants_map[parent];
                    assert(dom_instance.parent, "Parent instance not found (failed to reference)");
                else
                    dom_instance.parent = nil :: any;
                end
                instances[i] = dom_instance;
            end
            table.insert(class.instances, i);
        end
    end

    return {
        info = {
            version = 0,
            instances = #instances,
            classes = #classes,
            reserved = buffer.create(8),
        },
        instances = instances,
        classes = classes,
        metadata = {},
        sharedstrings = sharedstrings,
    };
end

function manager.domFromPlace(datamodel: DataModel): dom.DOM
    assert(
        object.is(datamodel) and datamodel:IsA("DataModel"),
        "argument #1 must be a DataModel instance"
    );
    local descendants_map = {};
    local descendants = datamodel:GetDescendants(); do
        local filtered = table.create(#descendants);
        local index = 1;
        for _, descendant in descendants do
            local properties = (rawget(descendant :: any, "@instance") :: any).properties;
            if (properties["Archivable"] == nil or properties["Archivable"].value == true) then
                table.insert(filtered, descendant);
                descendants_map[descendant] = index;
                index += 1;
            end
        end
        descendants = filtered;
    end
    return manager.domFromInstances(descendants, descendants_map, datamodel);
end

function manager.domFromModel(instances: {Instance}): dom.DOM
    assert(type(instances) == "table", "argument #1 must be a table of Instances");
    local descendants = {};
    local descendants_map = {};
    local index = 1;
    for _, instance in instances do
        assert(
            object.is(instance) and instance:IsA("Instance"),
            "value must be an Instance"
        );
        table.insert(descendants, instance);
        descendants_map[instance] = index;
        index += 1;
        for _, descendant in instance:GetDescendants() do
            local properties = (rawget(descendant :: any, "@instance") :: any).properties;
            if (properties["Archivable"] == nil or properties["Archivable"].value == true) then
                table.insert(descendants, descendant);
                descendants_map[descendant] = index;
                index += 1;
            end
        end
    end
    return manager.domFromInstances(descendants, descendants_map);
end

function manager.applyPatches()
    local patches = require("./patches");
    for class, property_patch in patches do
        local existing = object.presets[class];
        if (not existing) then
            local impl = {};
            impl.method = {};
            impl.property = {
                get = {};
                set = {};
            };
            existing = impl;
            object.presets[class] = existing;
        end
        if (not existing.property) then
            existing.property = {
                get = {};
                set = {};
            };
        end
        for property, patch in property_patch :: any do
            if (patch.get) then
                existing.property.get[property] = function(self)
                    return patch.get(self, property);
                end;
            end
            if (patch.set) then
                existing.property.set[property] = function(self, value)
                    return patch.set(self, property, value);
                end;
            end
            if (patch.loader) then
                existing.loader = existing.loader or {};
                existing.loader[property] = function(self, property_value)
                    return patch.loader(self, property, property_value);
                end;
            end
        end
    end
end

local instance = {};

local function instanceNew(className: string, parent: Instance?): Instance
    return object.new(className, parent);
end
local function instanceCreate(className: string): Instance
    return object.create(className);
end

instance.new = (instanceNew :: any) :: (className: string, parent: Instance?) -> Instance;
instance.create = (instanceCreate :: any) :: (className: string) -> Instance;

manager.Instance = instance;

manager.typeof = function(value: any)
    if (object.is(value)) then
        return "Instance";
    end
    return typeof(value);
end;

manager.type = function(value: any)
    if (object.is(value)) then
        return "userdata";
    end
    return type(value);
end;

manager.implementProperty = function(
    className: string,
    propertyName: string,
    get_callback: (instance: Instance) -> any,
    set_callback: (instance: Instance, value: any) -> ()
)
    assert(type(className) == "string", "className must be a string");
    assert(type(propertyName) == "string", "propertyName must be a string");
    assert(type(get_callback) == "function", "get_callback must be a function");
    assert(type(set_callback) == "function", "set_callback must be a function");
    local existing = object.presets[className];
    if (not existing) then
        local impl = {};
        impl.method = {};
        impl.property = {
            get = {};
            set = {};
        };
        existing = impl;
        object.presets[className] = existing;
    end
    if (not existing.property) then
        existing.property = {
            get = {};
            set = {};
        };
    end
    existing.property.get[propertyName] = get_callback;
    existing.property.set[propertyName] = set_callback;
end;

manager.implementMethod = function(
    className: string,
    methodName: string,
    callback: (instance: Instance, ...any) -> any
)
    assert(type(className) == "string", "className must be a string");
    assert(type(methodName) == "string", "methodName must be a string");
    assert(type(callback) == "function", "get_callback must be a function");
    local existing = object.presets[className];
    if (not existing) then
        local impl = {};
        impl.method = {};
        existing = impl;
        object.presets[className] = existing;
    end
    if (not existing.method) then
        existing.method = {};
    end
    existing.method[methodName] = callback;
end;

return manager;