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

local root = script.Parent.Parent

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

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

---

--[[
    Checks if a value is a valid RGB tuple (a 3-element array of numbers).

    @param value The value to check
    @return If the value is a valid RGB tuple
    @return An error message, if the value was invalid
]]
local rgbTuple = t.strictInterface({
    [1] = t.number,
    [2] = t.number,
    [3] = t.number,
})

--[[
    Checks if a value is a valid color palette item.
    A valid value is a dictionary in the format:

    ```
    {
        name: string,
        color: rgbTuple
    }
    ```

    @param value The value to check
    @return If the value is a valid color palette item
    @return An error message, if the value was invalid
]]
local colorPaletteItem = t.strictInterface({
    name = t.string,
    color = rgbTuple,
})

--[[
    Checks if a value is a valid color palette.
    A valid value is a dictionary in the format:

    ```
    {
        name: string,
        colors: array<colorPaletteItem>
    }
    ```

    @param value The value to check
    @return If the value is a valid color palette
    @return An error message, if the value was invalid
]]
local colorPalette = t.strictInterface({
    name = t.string,
    colors = t.array(colorPaletteItem)
})

--[[
    Checks if a value is a valid gradient keypoint.
    A valid value is a dictionary in the format:

    ```
    {
        time: number,
        color: rgbTuple
    }
    ```

    where `time` is in the range [0, 1].

    @param value The value to check
    @return If the value is a valid gradient keypoint
    @return An error message, if the value was invalid
]]
local gradientKeypoint = t.strictInterface({
    time = t.numberConstrained(0, 1),
    color = rgbTuple,
})

--[[
    Checks if a value is a valid gradient keypoint list.
    A valid value is an array of gradient keypoints where:
    - the keypoints are ordered such that `keypoint[i].time >= keypoint[i+1].time`,
    - `keypoints[1].time = 0` (first keypoint's time is 0)
    - `keypoints[#keypoints].time = 1` (last keypoint's time is 1)

    @param value The value to check
    @return If the value is a valid gradient keypoint list
    @return An error message, if the value was invalid
]]
local gradientKeypoints = function(a): (boolean, string?)
    local success, failReason = t.array(gradientKeypoint)(a)

    if (not success) then
        return success, failReason
    end

    for i = 1, #a - 1 do
        local thisKeypoint = a[i]
        local nextKeypoint = a[i + 1]

        if ((i == 1) and (thisKeypoint.time ~= 0)) then
            return false, "first keypoint must have time 0"
        elseif ((i == (#a - 1)) and (nextKeypoint.time ~= 1)) then
            return false, "last keypoint must have time 1"
        elseif (thisKeypoint.time >= nextKeypoint.time) then
            return false, "keypoints must be ordered in time"
        end
    end

    return true
end

--[[
    Checks if a value is a valid gradient palette item.
    A valid value is a dictionary in the format:

    ```
    {
        name: string,
        colorSpace: MixableColorType (see Constants),
        precision: integer,
        hueAdjustment: HueAdjustment (see Constants),

        keypoints: gradientKeypoints
    }
    ```

    @param value The value to check
    @return If the value is a valid gradient palette item
    @return An error message, if the value was invalid
]]
local gradientPaletteItem = function(value: any): (boolean, string?)
    local success: boolean, failReason: string? = t.strictInterface({
        name = t.string,
        colorSpace = t.valueOf(Constants.VALID_GRADIENT_COLOR_SPACES),
        precision = t.integer,
        hueAdjustment = t.valueOf(Constants.VALID_HUE_ADJUSTMENTS),

        keypoints = gradientKeypoints,
    })(value)

    if (not success) then
        return false, failReason::string
    end

    local isPrecisionValid: boolean =
        Util.gradient.getUtilisedKeypoints(#value.keypoints, value.precision)
            <=
        Constants.MAX_COLORSEQUENCE_KEYPOINTS

    if (not isPrecisionValid) then
        return false, "invalid precision"
    else
        return true
    end
end

--[[
    Checks if a value is a valid gradient palette.
    A valid value is a dictionary in the format:

    ```
    {
        name: string,
        gradients: array<gradientPaletteItem>
    }
    ```

    @param value The value to check
    @return If the value is a valid gradient palette
    @return An error message, if the value was invalid
]]
local gradientPalette = t.strictInterface({
    name = t.string,
    gradients = t.array(gradientPaletteItem),
})

---

local ColorPaneUserDataValidators = {
    _colorPalette = colorPalette,
    _gradientPalette = gradientPalette,

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

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

    --[[
        Checks if a value is a valid snap value (a constrained number).

        @param value The value to check
        @return If the value is a valid snap value
        @return An error message, if the value was invalid
    ]]
    [Enums.ColorPaneUserDataKey.SnapValue] = t.numberConstrained(Constants.MIN_SNAP_VALUE, Constants.MAX_SNAP_VALUE),

    --[[
        Checks if a value is an array of color palettes.

        @param value The value to check
        @return If the value is an array of color palettes
        @return An error message, if the value was invalid
    ]]
    [Enums.ColorPaneUserDataKey.UserColorPalettes] = t.array(colorPalette),

    --[[
        Checks if a value is an array of gradient palettes.

        @param value The value to check
        @return If the value is an array of gradient palettes
        @return An error message, if the value was invalid
    ]]
    [Enums.ColorPaneUserDataKey.UserGradientPalettes] = t.array(gradientPalette),

    --[[
        Checks if a value is a valid legacy gradient palette.

        @param value The value to check
        @return If the value is valid
        @return An error message, if the value was invalid
    ]]
    _userGradients = t.array(t.strictInterface({
        name = t.string,
        colorSpace = t.string,
        precision = t.integer,
        hueAdjustment = t.string,

        keypoints = function(a): (boolean, string?)
            local success, failReason = t.array(t.strictInterface({
                Time = t.numberConstrained(0, 1),
                Color = rgbTuple,
            }))(a)

            if (not success) then
                return success, failReason
            end

            for i = 1, #a - 1 do
                local thisKeypoint = a[i]
                local nextKeypoint = a[i + 1]

                if ((i == 1) and (thisKeypoint.Time ~= 0)) then
                    return false, "first keypoint must have time 0"
                elseif ((i == (#a - 1)) and (nextKeypoint.Time ~= 1)) then
                    return false, "last keypoint must have time 1"
                elseif (thisKeypoint.Time >= nextKeypoint.Time) then
                    return false, "keypoints must be ordered in time"
                end
            end

            return true
        end
    })),
}

--[[
    Checks if a value is a valid user data table.

    @param value The value to check
    @return If the value is valid
    @return An error message, if the value was invalid

]]
ColorPaneUserDataValidators._userData = t.interface({
    AskNameBeforePaletteCreation = ColorPaneUserDataValidators.AskNameBeforePaletteCreation,
    SnapValue = ColorPaneUserDataValidators.SnapValue,
    UserColorPalettes = ColorPaneUserDataValidators.UserColorPalettes,
    UserGradientPalettes = ColorPaneUserDataValidators.UserGradientPalettes,
})

---

return ColorPaneUserDataValidators


--!strict
-- Provides constants used in the projects

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

---

local CONSTANTS = {
    --[[
        The list of valid gradient color spaces.
    ]]
    VALID_GRADIENT_COLOR_SPACES = { "RGB", "CMYK", "HSB", "HWB", "HSL", "Lab", "Luv", "LChab", "LChuv", "xyY", "XYZ" },

    --[[
        The list of valid hue adjustments.
    ]]
    VALID_HUE_ADJUSTMENTS = { "Shorter", "Longer", "Increasing", "Decreasing", "Specified" },

    --[[
        The lower bound of color temperatures, in Kelvin.
    ]]
    KELVIN_LOWER_RANGE = 1000,

    --[[
        The upper bound of color temperatures, in Kelvin.
    ]]
    KELVIN_UPPER_RANGE = 10000,

    --[[
        The minimum time value that gradient keypoints must be separated by.
    ]]
    MIN_SNAP_VALUE = 0.00001,

    --[[
        The maximum time value that gradient keypoints can be separated by.
    ]]
    MAX_SNAP_VALUE = 0.25,

    --[[
        The name of the ColorPane user interface folder.
    ]]
    COLORPANE_USERDATA_INTERFACE_NAME = "ColorPaneUserData_" .. StudioService:GetUserId(),
}

-- Calculate the maximum number of keypoints that can be in a ColorSequence
do
    local KEYPOINT_LIMIT: number = 102  -- set an artificial limit in case the limit imposed by the engine is removed

    local numKeypoints: number = 3
    local done: boolean = false

    while (not done) do
        local keypoints: {ColorSequenceKeypoint} = {}

        for i = 1, numKeypoints do
            table.insert(keypoints, ColorSequenceKeypoint.new((i - 1) / (numKeypoints - 1), Color3.new()))
        end

        -- pcall(ColorSequence.new, keypoints) raises a type-checking error
        local isColorSequenceOk: boolean = pcall(function()
            return ColorSequence.new(keypoints)
        end)

        if ((isColorSequenceOk) and (numKeypoints <= KEYPOINT_LIMIT)) then
            numKeypoints += 1
        else
            numKeypoints -= 1
            done = true
        end
    end

    --[[
        The maximum number of ColorSequenceKeypoints that can be in a ColorSequence.
    ]]
    CONSTANTS.MAX_COLORSEQUENCE_KEYPOINTS = numKeypoints
end

---

return CONSTANTS


--!strict
--[[
    Enums used by more than one project.
]]

return {
    ColorPaneUserDataKey = {
        SnapValue = "SnapValue",
        UserColorPalettes = "UserColorPalettes",
        UserGradientPalettes = "UserGradientPalettes",
        AskNameBeforePaletteCreation = "AskNameBeforePaletteCreation",
    },

    UserDataError = {
        InvalidKey = "InvalidKey",
        InvalidValue = "InvalidValue",
        ValidatorNotFound = "ValidatorNotFound",
        InvalidUserData = "InvalidUserData",
    },

    UpstreamUserDataProviderError = {
        Unavailable = "Unavailable",
    },
}


--!strict

local savedPlugin: Plugin? = nil

--[[
    Stores and/or provides a Plugin object.

    If called with a plugin, it will be stored for future use.
    If a plugin is already stored, the function will throw an error.

    If called without a plugin, it will return the currently-stored plugin.
    If a plugin is not stored, the function will throw an error.

    @param plugin The plugin to be stored
    @return The currently-stored plugin
]]
return function(plugin: Plugin?): Plugin
    if (not plugin) then
        assert(savedPlugin, "Plugin object is missing")
        return savedPlugin
    else
        if (savedPlugin) then
            error("Plugin already stored")
        end
    end

    savedPlugin = plugin
    return plugin
end


--!strict
-- Creates windows for prompts

local Common = script.Parent.Parent

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

local Modules = script.Parent
local PluginProvider = require(Modules.PluginProvider)
local Translator = require(Modules.Translator)
local Window = require(Modules.Window)

local CommonComponents = Common.Components
local PromptComponent = require(CommonComponents.Prompt)

---

local plugin: Plugin = PluginProvider()

local UI_TRANSLATIONS = Translator.GenerateTranslationTable({
    "Cancel_ButtonText",
    "Confirm_ButtonText",
})

--[[
    @param Title The title for the prompt window
    @param PromptText The text for the body of the prompt
    @param CancelText (Optional) The text for the cancel button
    @param ConfirmText (Optional) The text for the confirm button
]]
type PromptInfo = {
    Title: string,
    PromptText: string,
    CancelText: string?,
    ConfirmText: string?,
}

--[[
    Creates a one-time prompt to take or avoid an action.

    @param id The prompt window ID
    @param widgetInfo The prompt window info
    @param promptInfo The prompt information
    @return A signal that will fire when the prompt is closed
]]
return function(id: string, widgetInfo: DockWidgetPluginGuiInfo, promptInfo: PromptInfo, store)
    local window: Window.Window = Window.new(id, widgetInfo)
    local promptClosed: Signal.Signal<boolean>, firePromptClosed: Signal.FireSignal<boolean> = Signal.createSignal()

    local openedWithoutMounting: Signal.Subscription
    local closedWithoutUnmounting: Signal.Subscription
    local promptClosedCleanup: Signal.Subscription

    local cleanup = function()
        promptClosedCleanup:unsubscribe()
        openedWithoutMounting:unsubscribe()
        closedWithoutUnmounting:unsubscribe()
        window:destroy()
    end

    promptClosedCleanup = promptClosed:subscribe(cleanup)
    openedWithoutMounting = window.openedWithoutMounting:subscribe(cleanup)

    closedWithoutUnmounting = window.closedWithoutUnmounting:subscribe(function()
        firePromptClosed(false)
    end)

    plugin.Unloading:Connect(function()
        if (window:isMounted()) then
            firePromptClosed(false)
        else
            cleanup()
        end
    end)

    window:mount(
        promptInfo.Title,
        Roact.createElement(PromptComponent, {
            promptText = promptInfo.PromptText,
            cancelText = promptInfo.CancelText or UI_TRANSLATIONS["Cancel_ButtonText"],
            confirmText = promptInfo.ConfirmText or UI_TRANSLATIONS["Confirm_ButtonText"],
            onDone = firePromptClosed,
        }),
        store
    )

    return promptClosed
end


--!strict
-- GUI styling information and image IDs

---

local Style = {
    UDim2 = {},

    Fonts = {
        Standard = Enum.Font.SourceSans,
        Monospaced = Enum.Font.RobotoMono,
    },

    Constants = {
        -- Paddings
        MajorElementPadding = 16,
        MinorElementPadding = 4,
        SpaciousElementPadding = 8,
        PagePadding = 8,
        TextObjectPadding = 4,

        -- Element sizes
        MarkerSize = 8,

        StandardTextSize = 14,
        LargeTextSize = 18,

        DialogButtonWidth = 70,
        EditorPageWidth = 265,
        ColorWheelRingWidth = 20,
        ColorSequencePreviewWidth = 62,
        ScrollbarThickness = 16,

        -- Misc.
        StandardCornerRadius = 4,
    },

    Images = {
        -- Color wheel images
        HueWheel = "rbxassetid://5686040244",
        SBPlane = "rbxassetid://5685199701",

        -- Color harmony icons
        HarmonyMarker = "rbxassetid://6208151356",
        NoHarmonyButtonIcon = "rbxassetid://6237978084",
        ComplementaryHarmonyButtonIcon = "rbxassetid://6237986907",
        AnalogousHarmonyButtonIcon = "rbxassetid://6237983501",
        TriadicHarmonyButtonIcon = "rbxassetid://6237989125",
        SplitComplementaryButtonIcon = "rbxassetid://6237993440",
        SquareHarmonyButtonIcon = "rbxassetid://6237995215",
        TetradicHarmonyButtonIcon = "rbxassetid://6237996947",
        HexagonalHarmonyButtonIcon = "rbxassetid://6238003104",

        -- ColorBrewer palette types
        SequentialDataTypeButtonIcon = "rbxassetid://6313201384",
        DivergingDataTypeButtonIcon = "rbxassetid://6308594803",
        QualitativeDataTypeButtonIcon = "rbxassetid://6308596898",

        -- Palette controls
        GridViewButtonIcon = "rbxassetid://6541631629",
        ListViewButtonIcon = "rbxassetid://6313201384",

        -- Color Editor buttons
        ColorWheelEditorButtonIcon = "rbxassetid://6333198466",
        SlidersEditorButtonIcon = "rbxassetid://6333325727",
        PaletteEditorButtonIcon = "rbxassetid://6333569516",
        ColorToolsEditorButtonIcon = "rbxassetid://8988492544",
        RandomColorButtonIcon = "rbxassetid://15839330129",

        -- Gradient editor buttons
        ReverseGradientButtonIcon = "rbxassetid://6409046433",
        GradientInfoButtonIcon = "rbxassetid://6554131676",
        ShowCodeButtonIcon = "rbxassetid://8521488559",
        HideCodeButtonIcon  = "rbxassetid://8521969115",

        -- Dropdown buttons
        CloseDropdownButtonIcon = "rbxassetid://2064489060",
        OpenDropdownButtonIcon = "rbxassetid://367867055",

        -- Async status icons
        ResultOkIcon = "rbxassetid://1469818624",
        ResultNotOkIcon = "rbxassetid://367878870",
        ResultWaitingIcon = "rbxassetid://6973265105",

        -- Generic control buttons
        MoveUpButtonIcon = "rbxassetid://965323360",
        MoveDownButtonIcon = "rbxassetid://913309373",
        MoveLeftButtonIcon = "rbxassetid://330699522",
        MoveRightButtonIcon = "rbxassetid://330699633",

        AddButtonIcon = "rbxassetid://919844482",
        SubtractButtonIcon = "rbxassetid://6213137847",
        DeleteButtonIcon = "rbxassetid://919846965",

        -- Misc. UI
        PageOptionsButtonIcon = "rbxassetid://6308568235",
        ScrollbarImage = "rbxassetid://590077572",
        SearchHistoryButtonIcon = "rbxassetid://9126497911",
        ColorSequenceTypeIcon = "rbxassetid://1097454788",

        -- Companion toolbar buttons
        ColorEditorToolbarButtonIcon = "rbxassetid://7066707717",
        GradientEditorToolbarButtonIcon = "rbxassetid://7066742393",
        SettingsToolbarButtonIcon = "rbxassetid://6528624327",
        ColorPropertiesToolbarButtonIcon = "rbxassetid://6531028502",
    }
}

-- Derived values

Style.Constants.StandardButtonHeight = Style.Constants.StandardTextSize + (Style.Constants.MinorElementPadding * 2)
Style.Constants.LargeButtonHeight = Style.Constants.StandardButtonHeight + (Style.Constants.MinorElementPadding * 2)
Style.Constants.StandardInputHeight = Style.Constants.StandardButtonHeight

Style.UDim2.StandardButtonSize = UDim2.new(0, Style.Constants.StandardButtonHeight, 0, Style.Constants.StandardButtonHeight)
--Style.UDim2.LargeButtonSize = UDim2.new(0, Style.Constants.LargeButtonHeight, 0, Style.Constants.LargeButtonHeight)
Style.UDim2.DialogButtonSize = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight)
Style.UDim2.ButtonBarSize = UDim2.new(1, 0, 0, Style.Constants.StandardButtonHeight)
Style.UDim2.MarkerSize = UDim2.new(0, Style.Constants.MarkerSize, 0, Style.Constants.MarkerSize)
Style.UDim2.MinorElementPaddingSize = UDim2.new(0, Style.Constants.MinorElementPadding, 0, Style.Constants.MinorElementPadding)

---

return Style


--!strict

--[[
    Represents the set of values used in UserData.
]]
export type UserDataValues = {[string]: any}

--[[
    Represents a key-value pair.
]]
export type KeyValue = {
    Key: string,
    Value: any
}

--[[
    Represents a raw color palette.
]]
export type ColorPalette = {
    name: string,

    colors: {{
        name: string,
        color: {number}
    }},
}

--[[
    Represents a raw gradient palette.
]]
export type GradientPalette = {
    name: string,

    gradients: {{
        name: string,
        colorSpace: string,
        hueAdjustment: string,
        precision: number,

        keypoints: {{
            time: number,
            color: {number}
        }}
    }}
}

--[[
    An array of raw color palettes.
]]
export type ColorPalettes = {ColorPalette}

--[[
    An array of raw gradient palettes.
]]
export type GradientPalettes = {GradientPalette}

--[[
    User data values for ColorPane.
]]
export type ColorPaneUserData = {
    AskNameBeforePaletteCreation: boolean,
    SnapValue: number,

    UserColorPalettes: ColorPalettes,
    UserGradientPalettes: GradientPalettes,
}

---

return {}


--!strict
--[[
    A class wrapper for a list of key-value pairs.
]]

local root = script.Parent.Parent

local Includes = root.Includes
local Signal = require(Includes.Signal)
local t = require(Includes.t)

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

---

type Values = Types.UserDataValues

type UserDataImpl = {
    __index: UserDataImpl,

    --[[
        Creates a new UserData.

        @param keys The set of valid user data keys
        @param validators The set of user data value validators
        @param diffs The set of custom user data value comparators
        @param userData The initial set of user data values
        @return A new UserData
    ]]
    new: (
        Values,
        {[string]: (any) -> (boolean, string?)},
        {[string]: (any, any) -> boolean},
        Values
    ) -> UserData,

    --[[
        Retrieves a user data value.

        @param self The UserData to retrieve the value from
        @param key The name of the value to retrieve
        @return The user data value
    ]]
    getValue: (UserData, string) -> any,

    --[[
        Retrieves all user data values.

        @param self The UserData to retrieve data from
        @return The user data table
    ]]
    getAllValues: (UserData) -> Values,

    --[[
        Updates a user data value.

        @param self The UserData to be updated
        @param key The name of the value to update
        @param value The new value
        @return If the value was actually updated
    ]]
    setValue: (UserData, string, any) -> boolean,
}

export type UserData = typeof(setmetatable(
    {}::{
        _data: Values,
        _keys: Values,
        _validators: {[string]: (any) -> (boolean, string?)},
        _diffs: {[string]: (any, any) -> boolean},
        _fireValueChanged: Signal.FireSignal<Types.KeyValue>,

        --[[
            Fires when a user data value changes.
        ]]
        valueChanged: Signal.Signal<Types.KeyValue>,
    },

    {}::UserDataImpl
))

---

--[[
    Represents a bundle of user data values and operations
    on those values.
]]
local UserData: UserDataImpl = {}::UserDataImpl
UserData.__index = UserData

UserData.new = function(
    keys: Values,
    validators: {[string]: (any) -> (boolean, string?)},
    diffs: {[string]: (any, any) -> boolean},
    userData: Values
): UserData
    local userDataIsValidTable = t.keys(t.string)
    assert(userDataIsValidTable, Enums.UserDataError.InvalidUserData)

    for key, value in pairs(userData) do
        -- drop extraneous values from table
        if (not keys[key]) then
            warn("extraneous user data key " .. key .. " will be dropped")
            userData[key] = nil
            continue
        end

        local validator: (any) -> (boolean, string?) = validators[key]
        assert(validator, Enums.UserDataError.ValidatorNotFound .. " " .. key)
        assert(validator(value), Enums.UserDataError.InvalidUserData)
    end

    local valueChangedSignal: Signal.Signal<Types.KeyValue>, fireValueChanged: Signal.FireSignal<Types.KeyValue> = Signal.createSignal()

    local self = {
        _data = userData,
        _keys = keys,
        _validators = validators,
        _diffs = diffs,
        _fireValueChanged = fireValueChanged,

        valueChanged = valueChangedSignal,
    }

    return setmetatable(self, UserData)
end

UserData.getValue = function(self: UserData, key: string): any
    assert(self._keys[key], Enums.UserDataError.InvalidKey)

    local value = self._data[key]

    if (typeof(value) == "table") then
        return Util.table.deepCopy(value)
    else
        return value
    end
end

UserData.getAllValues = function(self: UserData): Values
    return Util.table.deepCopy(self._data)
end

UserData.setValue = function(self: UserData, key: string, value: any): boolean
    assert(self._keys[key], Enums.UserDataError.InvalidKey)

    -- validate value
    local validator = self._validators[key]
    assert(validator, Enums.UserDataError.ValidatorNotFound)

    local valueIsValid: boolean = validator(value)
    assert(valueIsValid, Enums.UserDataError.InvalidValue)

    -- compare values to see if they're actually different
    local originalValue: any = self._data[key]
    local isDifferentValue: boolean

    if (self._diffs[key]) then
        isDifferentValue = self._diffs[key](originalValue, value)
    else
        isDifferentValue = originalValue ~= value
    end

    if (not isDifferentValue) then
        return false
    end

    -- add a copy of tables to prevent unintended behaviour
    if (typeof(value) == "table") then
        self._data[key] = Util.table.deepCopy(value)
    else
        self._data[key] = value
    end

    self._fireValueChanged({
        Key = key,
        Value = value
    })

    return true
end

---

return UserData


--!strict

--[[
    Utility functions
]]
local Util = {}

--[[
    Table utilities
]]
Util.table = {}

--[[
    Creates a copy of a table with sub-tables also being copied.

    If a sub-table has a metatable, it will not be copied.
    If the root table has a metatable, an error will occur.

    @param t The table to deep-copy
    @return A deep-copy of `t`
]]
Util.table.deepCopy = function(t: {[any]: any}): {[any]: any}
    if (getmetatable(t::any) ~= nil) then
        error("Cannot copy table with metatable!")
    end

    local copy: {[any]: any} = {}

    for k: any, v: any in pairs(t) do
        if (type(v) ~= "table") then
            copy[k] = v
        else
            if (getmetatable(v) == nil) then
                copy[k] = Util.table.deepCopy(v)
            else
                copy[k] = v
            end
        end
    end

    return copy
end

--[[
    Gradient utilities
]]
Util.gradient = {}

--[[
    Returns the number of generated keypoints for a
    given number of keypoints and precision.

    @param keypoints The number of actual keypoints
    @param precision The number of generated keypoints between each actual keypoint
]]
Util.gradient.getUtilisedKeypoints = function(keypoints: number, precision: number): number
    return (precision * (keypoints - 1)) + keypoints
end

---

return Util


--!strict
-- Provides an interface for working with DockWidgetPluginGuis

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

---

local Common = script.Parent.Parent
local Includes = Common.Includes

local RoactRoduxFolder = Includes.RoactRodux
local Roact = require(RoactRoduxFolder.Roact)
local RoactRodux = require(RoactRoduxFolder.RoactRodux)
local Rodux = require(RoactRoduxFolder.Rodux)

local Signal = require(Includes.Signal)

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

---

type Element = typeof(Roact.createElement())
type Store = typeof(Rodux.Store.new())

type WindowImpl = {
    __index: WindowImpl,

    --[[
        Creates a new Window

        @param id The window ID
        @param widgetInfo The window info
        @return A new window
    ]]
    new: (id: string, widgetInfo: DockWidgetPluginGuiInfo) -> Window,

    --[[
        Unmounts the currently-mounted element on the window and closes it

        @param self The window to unmount from
        @param resetTitle (Optional) Reset the plugin gui title
    ]]
    unmount: (Window, boolean?) -> (),

    --[[
        Mounts an element to the window and opens it

        @param self The window to mount to
        @param title The title of the window
        @param element The Roact element to mount to the window
        @param store The Rodux store to provide to Roact
    ]]
    mount: (Window, string, Element, Store) -> (),

    --[[
        Enables mouse tracking for this window.
        No effect if mouse tracking is already enabled.

        @param self The window to track
    ]]
    enableMouseTracking: (Window) -> (),

    --[[
        Disables mouse tracking for this window.
        No effect if mouse tracking isn't already enabled.

        @param self The window stop stop tracking
    ]]
    disableMouseTracking: (Window) -> (),

    --[[
        Opens the window (makes it visible)

        @param self The window to open
    ]]
    open: (Window) -> (),

    --[[
        Closes the window (makes is invisible)

        @param self The window to close
    ]]
    close: (Window) -> (),

    --[[
        Destroys the window.

        No further operations should be done on the Window, as indicated by its `destroyed` field.

        @param self The window to destroy
    ]]
    destroy: (Window) -> (),

    --[[
        Returns if the window has a mounted element

        @param self The window to check
        @return If the window has a mounted element (`true`) or not (`false`)
    ]]
    isMounted: (Window) -> boolean,

    --[[
        Returns if the window is open (visible)

        @param self The window to check
        @return If the window is open (`true`) or not (`false`)
    ]]
    isOpen: (Window) -> boolean,
}

export type Window = typeof(setmetatable(
    {}::{
        _tree: any,
        _window: DockWidgetPluginGui,
        _mousePositionPoll: RBXScriptConnection,
        _lastMousePosition: Vector2,
        _fireMousePositionChanged: Signal.FireSignal<Vector2>,

        --[[
            Marks if the window has been destroyed.
        ]]
        destroyed: boolean,

        --[[
            A signal that fires when the window is opened without having a mounted element
        ]]
        openedWithoutMounting: Signal.Signal<nil>,

        --[[
            A signal that fires when the window is closed without unmounting its element
        ]]
        closedWithoutUnmounting: Signal.Signal<nil>,

        --[[
            A signal that fires when the the mouse position changes
            and mouse tracking has been enabled for the window
        ]]
        mousePositionChanged: Signal.Signal<Vector2>,
    },

    {}::WindowImpl
))

---

local plugin: Plugin = PluginProvider()

local Window: WindowImpl = {}::WindowImpl
Window.__index = Window

Window.new = function(id: string, widgetInfo: DockWidgetPluginGuiInfo): Window
    local window: DockWidgetPluginGui = plugin:CreateDockWidgetPluginGui(id, widgetInfo)
    local openedWithoutMounting: Signal.Signal<nil>, fireOpenedWithoutMounting: Signal.FireSignal<nil> = Signal.createSignal()
    local closedWithoutUnmounting: Signal.Signal<nil>, fireClosedWithoutUnmounting: Signal.FireSignal<nil> = Signal.createSignal()
    local mousePositionChanged: Signal.Signal<Vector2>, fireMousePositionChanged: Signal.FireSignal<Vector2> = Signal.createSignal()

    local self = {
        _tree = nil,
        _window = window,
        _mousePositionPoll = RunService.Heartbeat:Connect(function() end),
        _lastMousePosition = Vector2.new(math.huge, math.huge),
        _fireMousePositionChanged = fireMousePositionChanged,

        destroyed = false,
        openedWithoutMounting = openedWithoutMounting,
        closedWithoutUnmounting = closedWithoutUnmounting,
        mousePositionChanged = mousePositionChanged,
    }

    window:GetPropertyChangedSignal("Enabled"):Connect(function()
        local enabled: boolean = window.Enabled

        if (enabled and (not self._tree)) then
            fireOpenedWithoutMounting()
        elseif ((not enabled) and self._tree) then
            fireClosedWithoutUnmounting()
        end
    end)

    window.Name = id
    window.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
    self._mousePositionPoll:Disconnect()
    return setmetatable(self, Window)
end

Window.unmount = function(self: Window, resetTitle: boolean?)
    if ((self.destroyed) or (not self._tree)) then return end

    Roact.unmount(self._tree)
    self._tree = nil

    local window: DockWidgetPluginGui = self._window
    window.Enabled = false

    if (resetTitle) then
        window.Title = ""
    end
end

Window.mount = function(self: Window, title: string, component: Element, store: Store)
    if ((self.destroyed) or (self._tree)) then return end

    local window: DockWidgetPluginGui = self._window

    self._tree = Roact.mount(Roact.createElement(RoactRodux.StoreProvider, {
        store = store
    }, {
        App = component
    }), window)

    window.Title = title
    window.Enabled = true
end

Window.enableMouseTracking = function(self: Window)
    if ((self.destroyed) or (self._mousePositionPoll.Connected)) then return end

    self._mousePositionPoll = RunService.Heartbeat:Connect(function()
        local position: Vector2 = self._window:GetRelativeMousePosition()
        if (position == self._lastMousePosition) then return end

        self._lastMousePosition = position
        self._fireMousePositionChanged(position)
    end)
end

Window.disableMouseTracking = function(self: Window)
    if ((self.destroyed) or (not self._mousePositionPoll.Connected)) then return end

    self._mousePositionPoll:Disconnect()
end

Window.open = function(self: Window)
    if (self.destroyed) then return end
    self._window.Enabled = true
end

Window.close = function(self: Window)
    if (self.destroyed) then return end
    self._window.Enabled = false
end

Window.destroy = function(self: Window)
    if (self.destroyed) then return end

    if (self._tree) then
        self:unmount()
    end

    self._mousePositionPoll:Disconnect()
    self._window:Destroy()
    self.destroyed = true
end

Window.isMounted = function(self: Window): boolean
    if (self.destroyed) then
        return false
    else
        return if (self._tree) then true else false
    end
end

Window.isOpen = function(self: Window): boolean
    if (self.destroyed) then
        return false
    else
        return self._window.Enabled
    end
end

---

return Window


--!strict

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

---

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

local CommonModules = Common.Modules
require(CommonModules.PluginProvider)(plugin)
local Translator = require(CommonModules.Translator)
local Window = require(CommonModules.Window)

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

local Components = root.Components
local Settings = require(Components.Settings)
local ColorProperties = require(Components.ColorProperties)

local Modules = root.Modules
require(Modules.ColorPaneUserDataInterface)
local Enums = require(Modules.Enums)
local ManagedUserData = require(Modules.ManagedUserData)
local ColorPane = require(Modules.ColorPane)
local RobloxAPI = require(Modules.RobloxAPI)
local Store = require(Modules.Store)
local Toolbar = require(Modules.Toolbar)
local WidgetInfo = require(Modules.WidgetInfo)

---

local colorEditPromise: typeof(ColorPane.PromptForColor())?
local gradientEditPromise: typeof(ColorPane.PromptForGradient())?

local colorPropertiesWindow: Window.Window = Window.new(WidgetInfo.ColorProperties.Id, WidgetInfo.ColorProperties.Info)
local settingsWindow: Window.Window = Window.new(WidgetInfo.Settings.Id, WidgetInfo.Settings.Info)

---

Toolbar.ColorEditButton.ClickableWhenViewportHidden = true
Toolbar.GradientEditButton.ClickableWhenViewportHidden = true

if (ManagedUserData.Companion:getValue(Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData)) then
    local startupRequestFinished
    startupRequestFinished = RobloxAPI.DataRequestFinished:subscribe(function(success: boolean)
        startupRequestFinished:unsubscribe()

        if ((not success) and RunService:IsEdit()) then
            warn(Translator.FormatByKey("AutoLoadColorPropertiesFailure_Message"))
        end
    end)

    RobloxAPI.GetData()
end

if (colorPropertiesWindow:isOpen()) then
    colorPropertiesWindow:mount(Translator.FormatByKey("ColorProperties_WindowTitle"), Roact.createElement(ColorProperties), Store)
    Toolbar.ColorPropertiesButton:SetActive(true)
end

Toolbar.ColorEditButton.Click:Connect(function()
    if (colorEditPromise) then
        colorEditPromise:cancel()
        colorEditPromise = nil
        return
    end

    if (ColorPane.IsColorPromptAvailable()) then
        local editPromise = ColorPane.PromptForColor()

        editPromise:catch(function() end):finally(function()
            colorEditPromise = nil
            Toolbar.ColorEditButton:SetActive(false)
        end)

        colorEditPromise = editPromise
        Toolbar.ColorEditButton:SetActive(true)
    else
        Toolbar.ColorEditButton:SetActive(false)
    end
end)

Toolbar.GradientEditButton.Click:Connect(function()
    if (gradientEditPromise) then
        gradientEditPromise:cancel()
        gradientEditPromise = nil
        return
    end

    if (ColorPane.IsGradientPromptAvailable()) then
        local editPromise = ColorPane.PromptForGradient()

        editPromise:catch(function() end):finally(function()
            gradientEditPromise = nil
            Toolbar.GradientEditButton:SetActive(false)
        end)

        gradientEditPromise = editPromise
        Toolbar.GradientEditButton:SetActive(true)
    else
        Toolbar.GradientEditButton:SetActive(false)
    end
end)

Toolbar.ColorPropertiesButton.Click:Connect(function()
    if (colorPropertiesWindow:isMounted()) then
        colorPropertiesWindow:unmount()
        Toolbar.ColorPropertiesButton:SetActive(false)
    else
        colorPropertiesWindow:mount(Translator.FormatByKey("ColorProperties_WindowTitle"), Roact.createElement(ColorProperties), Store)
        Toolbar.ColorPropertiesButton:SetActive(true)
    end
end)

Toolbar.SettingsButton.Click:Connect(function()
    if (settingsWindow:isMounted()) then
        settingsWindow:unmount()
        Toolbar.SettingsButton:SetActive(false)
    else
        settingsWindow:mount(Translator.FormatByKey("Settings_WindowTitle"), Roact.createElement(Settings), Store)
        Toolbar.SettingsButton:SetActive(true)
    end
end)

colorPropertiesWindow.openedWithoutMounting:subscribe(function()
    colorPropertiesWindow:close()
end)

colorPropertiesWindow.closedWithoutUnmounting:subscribe(function()
    colorPropertiesWindow:unmount()
    Toolbar.ColorPropertiesButton:SetActive(false)
end)

settingsWindow.openedWithoutMounting:subscribe(function()
    settingsWindow:close()
end)

settingsWindow.closedWithoutUnmounting:subscribe(function()
    settingsWindow:unmount()
    Toolbar.SettingsButton:SetActive(false)
end)

plugin.Unloading:Connect(function()
    colorPropertiesWindow:destroy()
    settingsWindow:destroy()

    if (colorEditPromise) then
        colorEditPromise:cancel()
        colorEditPromise = nil
    end

    if (gradientEditPromise) then
        gradientEditPromise:cancel()
        gradientEditPromise = nil
    end
end)


local RunService = game:GetService("RunService")

---

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

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

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

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

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

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

local StandardComponents = CommonComponents.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIPadding = require(StandardComponents.UIPadding)

---

local companionUserData = ManagedUserData.Companion

local uiTranslations = Translator.GenerateTranslationTable({
    "NoAPIAlert_MainText",
    "NoAPIAlert_SecondaryText_EditMode",
    "NoAPIAlert_SecondaryText_NotEditMode",
    "Loading_Message",
    "Load_ButtonText",
    "AutoLoadColorProperties_SettingDescription",
    "CacheAPIData_SettingDescription",
})

---

--[[
    store props

        theme: StudioTheme
]]

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

NoAPIAlert.init = function(self)
    self.apiDataRequestStarted = RobloxAPI.DataRequestStarted:subscribe(function()
        self:setState({
            requestRunning = true,
        })
    end)

    self.apiDataRequestFinished = RobloxAPI.DataRequestFinished:subscribe(function()
        self:setState({
            requestRunning = false,
        })
    end)

    self.valueChanged = companionUserData.valueChanged:subscribe(function(value)
        local key = value.Key

        if (
            (key ~= Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData) and
            (key ~= Enums.CompanionUserDataKey.CacheColorPropertiesAPIData)
        ) then return end

        self:setState({
            [key] = value.Value
        })
    end)

    self:setState({
        requestRunning = RobloxAPI.IsRequestRunning(),

        [Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData] = companionUserData:getValue(Enums.CompanionUserDataKey.AutoLoadColorPropertiesAPIData),
        [Enums.CompanionUserDataKey.CacheColorPropertiesAPIData] = companionUserData:getValue(Enums.CompanionUserDataKey.CacheColorPropertiesAPIData)
    })
end

NoAPIAlert.willUnmount = function(self)
    self.apiDataRequestStarted:unsubscribe()
    self.apiDataRequestFinished:unsubscribe()
    self.valueChanged:unsubscribe()
end

NoAPIAlert.render = function(self)
    local theme = self.props.theme
    local requestRunning = self.state.requestRunning

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

        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        Notice = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0.5, 0),
            Size = UDim2.new(1, 0, 0.5, 0),
            Position = UDim2.new(0.5, 0, 0, 0),

            Text = uiTranslations["NoAPIAlert_MainText"] .. (RunService:IsEdit() and
                (" " .. uiTranslations["NoAPIAlert_SecondaryText_EditMode"]) or
                ("\n\n" .. uiTranslations["NoAPIAlert_SecondaryText_NotEditMode"])
            ),

            TextXAlignment = Enum.TextXAlignment.Center,
            TextYAlignment = Enum.TextYAlignment.Bottom,
            TextWrapped = true,
        }),

        LoadButton = Roact.createElement(Button, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0.5, Style.Constants.SpaciousElementPadding),
            Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),

            disabled = requestRunning,
            displayType = "text",
            text = uiTranslations[requestRunning and "Loading_Message" or "Load_ButtonText"],

            backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton),
            borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
            hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Hover),
            displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText),

            disabledBackgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Disabled),
            disabledDisplayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText, Enum.StudioStyleGuideModifier.Disabled),

            onActivated = function()
                if (requestRunning) then return end

                RobloxAPI.GetData()
            end
        }),

        AutoLoadRobloxAPICheckbox = Roact.createElement(Checkbox, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0.5, Style.Constants.StandardButtonHeight + (Style.Constants.SpaciousElementPadding * 2)),
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),

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

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

        CacheAPIDataCheckbox = Roact.createElement(Checkbox, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0.5, Style.Constants.StandardButtonHeight + (Style.Constants.SpaciousElementPadding * 3) + (Style.Constants.StandardTextSize * 2)),
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),

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

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

---

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

ColorProperties.init = function(self)
    local apiLoaded = RobloxAPI.IsAvailable()

    self.apiDataLoaded = (not apiLoaded) and
        RobloxAPI.DataRequestFinished:subscribe(function(didLoad)
            if (not didLoad) then return end

            self.apiDataLoaded:unsubscribe()
            self.apiDataLoaded = nil

            self:setState({
                apiLoaded = true,
            })
        end)
    or nil

    self:setState({
        apiLoaded = apiLoaded
    })
end

ColorProperties.willUnmount = function(self)
    if (self.apiDataLoaded) then
        self.apiDataLoaded:unsubscribe()
    end
end

ColorProperties.render = function(self)
    return Roact.createElement(self.state.apiLoaded and ColorPropertiesList or NoAPIAlert)
end

---

NoAPIAlert = ConnectTheme(NoAPIAlert)
return ColorProperties


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

---

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

local CommonComponents = Common.Components
local ConnectTheme = require(CommonComponents.ConnectTheme)

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

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

local Modules = root.Modules
local ColorPane = require(Modules.ColorPane)
local DocumentationPluginMenu = require(Modules.DocumentationPluginMenu)
local SelectionManager = require(Modules.SelectionManager)

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

---

local LIST_ITEM_LEFT_PADDING = 24
local COLOR_TYPE_ICON_SIZE = 12

local partClassIconData = StudioService:GetClassIcon("Part")

local uiTranslations = Translator.GenerateTranslationTable({
    "NoSelectionColorProperties_Message",
    "SelectionColorMultipleValues_Indicator",
})

local getCellTextSizes = function(propName, displayClassName, showClassName)
    local propNameTextSize = TextService:GetTextSize(
        propName,
        Style.Constants.StandardTextSize,
        Style.Fonts.Standard,
        Vector2.new(math.huge, math.huge)
    )

    local classNameTextSize = showClassName and
        TextService:GetTextSize(
            displayClassName,
            Style.Constants.StandardTextSize,
            Style.Fonts.Standard,
            Vector2.new(math.huge, math.huge)
        )
    or Vector2.new()

    return propNameTextSize, classNameTextSize
end

---

--[[
    props

        AnchorPoint?
        Size?
        Position?
        LayoutOrder?

        className: string
        propertyName: string
        custom: boolean?

        colorType: "BrickColor" | "Color3" | "ColorSequence"
        color: Binding<Color | Gradient>

        showClassName: boolean?
        selected: boolean?
        disabled: boolean?

        promptForEdit: () -> nil

    store props

        theme: StudioTheme
]]

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

ColorPropertyListItem.render = function(self)
    local theme = self.props.theme
    local color = self.props.color
    local colorType = self.props.colorType

    local className = self.props.className
    local propertyName = self.props.propertyName

    local isColorSequence = (colorType == "ColorSequence")
    local selected, disabled = self.props.selected, self.props.disabled
    local displayClassName = "(" .. className .. ")"
    local propertyNameTextSize, classNameTextSize = getCellTextSizes(propertyName, displayClassName, self.props.showClassName)

    local colorTypeImage
    local propertyLabelTextColor

    if (colorType == "BrickColor") then
        colorTypeImage = partClassIconData.Image
    elseif (colorType == "Color3") then
        colorTypeImage = Style.Images.HueWheel
    elseif (colorType == "ColorSequence") then
        colorTypeImage = Style.Images.ColorSequenceTypeIcon
    end

    if (selected) then
        propertyLabelTextColor = theme:GetColor(Enum.StudioStyleGuideColor.MainText, Enum.StudioStyleGuideModifier.Selected)
    else
        propertyLabelTextColor = theme:GetColor(
            Enum.StudioStyleGuideColor.MainText,
            disabled and Enum.StudioStyleGuideModifier.Disabled or nil
        )
    end

    return Roact.createElement("TextButton", {
        AnchorPoint = self.props.AnchorPoint,
        Size = self.props.Size,
        Position = self.props.Position,
        LayoutOrder = self.props.LayoutOrder,
        AutoButtonColor = false,
        BorderSizePixel = 1,
        BorderMode = Enum.BorderMode.Outline,

        Text = "",
        TextTransparency = 1,

        BackgroundColor3 = selected and
            theme:GetColor(Enum.StudioStyleGuideColor.TableItem, Enum.StudioStyleGuideModifier.Selected)
        or theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),

        BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),

        [Roact.Event.MouseEnter] = function(obj)
            if (selected or disabled) then return end

            obj.BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.TableItem, Enum.StudioStyleGuideModifier.Hover)
        end,

        [Roact.Event.MouseLeave] = function(obj)
            if (selected or disabled) then return end

            obj.BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
        end,

        [Roact.Event.MouseButton2Click] = function()
            if (self.props.custom) then return end

            local triggerConnection = DocumentationPluginMenu.Action.Triggered:Connect(function()
                DocumentationPluginMenu.ShowPropertyDocumentation(className, propertyName)
            end)

            DocumentationPluginMenu.Menu:ShowAsync()
            triggerConnection:Disconnect()
        end,

        [Roact.Event.Activated] = function()
            if (selected or disabled) then return end

            self.props.promptForEdit()
        end,
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {
                Style.Constants.MinorElementPadding,
                Style.Constants.MinorElementPadding,
                LIST_ITEM_LEFT_PADDING,
                Style.Constants.MinorElementPadding
            }
        }),

        PropertyNameLabel = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0, 0.5),
            Size = UDim2.new(0, propertyNameTextSize.X, 1, 0),
            Position = UDim2.new(0, 0, 0.5, 0),
            Text = self.props.propertyName,

            TextColor3 = propertyLabelTextColor,
        }),

        ClassNameLabel = self.props.showClassName and
            Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0.5),
                Size = UDim2.new(0, classNameTextSize.X, 1, 0),
                Position = UDim2.new(0, propertyNameTextSize.X + Style.Constants.MinorElementPadding, 0.5, 0),
                Text = displayClassName,

                TextColor3 = theme:GetColor(
                    Enum.StudioStyleGuideColor.MainText,
                    (not selected) and Enum.StudioStyleGuideModifier.Disabled or Enum.StudioStyleGuideModifier.Selected
                ),
            })
        or nil,

        ColorTypeIndicator = Roact.createElement("ImageLabel", {
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, -(Style.Constants.ColorSequencePreviewWidth + Style.Constants.MinorElementPadding), 0.5, 0),
            Size = UDim2.new(0, COLOR_TYPE_ICON_SIZE, 0, COLOR_TYPE_ICON_SIZE),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            Image = colorTypeImage,
            ImageRectOffset = (colorType == "BrickColor") and partClassIconData.ImageRectOffset or nil,
            ImageRectSize =  (colorType == "BrickColor") and partClassIconData.ImageRectSize or nil,

            ImageColor3 = disabled and Color3.new(1/2, 1/2, 1/2) or nil,
        }, {
            UICorner = (colorType ~= "BrickColor") and
                Roact.createElement(StandardUICorner, { circular = true, })
            or nil,
        }),

        ColorButtonContainer = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.new(0, Style.Constants.ColorSequencePreviewWidth, 1, 0),
            BackgroundTransparency = 0,
            BorderSizePixel = 0,

            BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border)
        }, {
            UICorner = Roact.createElement(StandardUICorner),

            ColorButton = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 0.5),
                Position = UDim2.new(0.5, 0, 0.5, 0),
                Size = UDim2.new(1, -2, 1, -2),
                BackgroundTransparency = 0,

                TextXAlignment = Enum.TextXAlignment.Center,
                TextYAlignment = Enum.TextYAlignment.Center,

                Text = color:map(function(value)
                    return value and "" or uiTranslations["SelectionColorMultipleValues_Indicator"]
                end),

                BackgroundColor3 = color:map(function(value)
                    local bkgColor

                    if (not isColorSequence) then
                        bkgColor = value and value:toColor3() or nil
                    else
                        bkgColor = Color3.new(1, 1, 1)
                    end

                    return bkgColor or theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
                end),
            }, {
                UICorner = Roact.createElement(StandardUICorner),

                UIGradient = isColorSequence and
                    Roact.createElement("UIGradient", {
                        Color = color:map(function(value)
                            return value and value:colorSequence(nil, "RGB") or ColorSequence.new(Color3.new(1, 1, 1))
                        end),
                    })
                or nil,
            })
        })
    })
end

---

--[[
    props

        AnchorPoint?
        Size?
        Position?
]]

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

ColorPropertiesList.init = function(self)
    self.commonPropertyValues, self.updateCommonPropertyValues = Roact.createBinding({})
    self.listLength, self.updateListLength = Roact.createBinding(0)

    self.selectionChanged = SelectionManager.SelectionChanged:subscribe(function()
        if (self.state.editColorPromise) then
            self.state.editColorPromise:cancel()
        end

        self:setState({
            propertyData = SelectionManager.GetSelectionColorPropertyData(),
        })

        self.updateCommonPropertyValues(SelectionManager.GetSelectionCommonColorPropertyValues())
    end)

    self.selectionColorsChanged = SelectionManager.SelectionColorsChanged:subscribe(function()
        self.updateCommonPropertyValues(SelectionManager.GetSelectionCommonColorPropertyValues())
    end)

    self:setState({
        propertyData = SelectionManager.GetSelectionColorPropertyData(),
    })

    self.updateCommonPropertyValues(SelectionManager.GetSelectionCommonColorPropertyValues())
end

ColorPropertiesList.didMount = function()
    SelectionManager.Connect()
end

ColorPropertiesList.willUnmount = function(self)
    self.unmounting = true

    if (self.state.editColorPromise) then
        self.state.editColorPromise:cancel()
    end

    self.selectionChanged:unsubscribe()
    self.selectionColorsChanged:unsubscribe()
    SelectionManager.Disconnect()
end

ColorPropertiesList.render = function(self)
    local listElements = {}
    local minCellWidth = 0

    local properties = self.state.propertyData.Properties
    local duplicateProperties = self.state.propertyData.Duplicated
    local sortedProperties = self.state.propertyData.Sorted

    for i = 1, #sortedProperties do
        local className, propertyName = sortedProperties[i][1], sortedProperties[i][2]
        local propNameTextSize, classNameTextSize = getCellTextSizes(propertyName, "(" .. className .. ")", duplicateProperties[propertyName])

        local cellWidth = LIST_ITEM_LEFT_PADDING +
            propNameTextSize.X +
            Style.Constants.MinorElementPadding +
            classNameTextSize.X +
            Style.Constants.MajorElementPadding +
            COLOR_TYPE_ICON_SIZE +
            Style.Constants.MinorElementPadding +
            Style.Constants.ColorSequencePreviewWidth +
            Style.Constants.MinorElementPadding

        minCellWidth = (cellWidth > minCellWidth) and cellWidth or minCellWidth
    end

    for i = 1, #sortedProperties do
        local className, propertyName = sortedProperties[i][1], sortedProperties[i][2]
        local propertyInfo = properties[className][propertyName]

        local compositeName = className .. "/" .. propertyName
        local promptTitle = className .. "." .. propertyName

        local valueType = propertyInfo.ValueType.Name
        local isColorSequence = (valueType == "ColorSequence")

        listElements[compositeName] = Roact.createElement(ColorPropertyListItem, {
            Size = UDim2.new(1, 0, 0, Style.Constants.LargeButtonHeight),
            LayoutOrder = i,

            className = className,
            propertyName = propertyName,
            custom = propertyInfo.Custom,
            colorType = ((className == "DataModelMesh") and (propertyName == "VertexColor")) and "Color3" or valueType,

            showClassName = duplicateProperties[propertyName],
            selected = (self.state.editingProperty == compositeName),
            disabled = self.state.editColorPromise and (self.state.editingProperty ~= compositeName),

            color = self.commonPropertyValues:map(function(values)
                return values[className] and values[className][propertyName] or nil
            end),

            promptForEdit = function()
                if (isColorSequence) then
                    if (not ColorPane.IsGradientPromptAvailable()) then return end
                else
                    if (not ColorPane.IsColorPromptAvailable()) then return end
                end

                if (self.state.editColorPromise) then
                    self.state.editColorPromise:cancel()
                end

                local rejected = false
                local commonPropertyValues = self.commonPropertyValues:getValue()

                local editValuePromptOptions = {
                    PromptTitle = promptTitle,
                }

                if (isColorSequence) then
                    editValuePromptOptions.GradientType = "Gradient"
                    editValuePromptOptions.InitialGradient = commonPropertyValues[className] and commonPropertyValues[className][propertyName] or nil

                    editValuePromptOptions.OnGradientChanged = function(intermediate)
                        SelectionManager.SetSelectionProperty(className, propertyName, intermediate)
                    end
                else
                    editValuePromptOptions.ColorType = "Color"
                    editValuePromptOptions.InitialColor = commonPropertyValues[className] and commonPropertyValues[className][propertyName] or nil

                    editValuePromptOptions.OnColorChanged = function(intermediate)
                        SelectionManager.SetSelectionProperty(className, propertyName, intermediate)
                    end
                end

                SelectionManager.BeginRecording()

                local editColorPromise = isColorSequence and
                    ColorPane.PromptForGradient(editValuePromptOptions)
                or
                    ColorPane.PromptForColor(editValuePromptOptions)

                editColorPromise:andThen(function(newColor)
                    SelectionManager.SetSelectionProperty(className, propertyName, newColor)
                    SelectionManager.StopRecording(Enum.FinishRecordingOperation.Commit)
                end, function(err)
                    rejected = true

                    if (err == ColorPane.PromptRejection.PromptCancelled) then
                        SelectionManager.StopRecording(Enum.FinishRecordingOperation.Cancel)
                    end
                end):finally(function()
                    if (not self.unmounting) then
                        self:setState({
                            editColorPromise = Roact.None,
                            editingProperty = Roact.None,
                        })
                    end
                end)

                -- prevent conflict if the Promise is immediately rejected
                if (not rejected) then
                    self:setState({
                        editColorPromise = editColorPromise,
                        editingProperty = compositeName,
                    })
                end
            end,
        })
    end

    listElements.UIListLayout = Roact.createElement(StandardUIListLayout, {
        [Roact.Change.AbsoluteContentSize] = function(obj)
            self.updateListLength(obj.AbsoluteContentSize.Y)
        end,

        preset = 1,
    })

    return (#sortedProperties > 0) and
        Roact.createElement(StandardScrollingFrame, {
            AnchorPoint = Vector2.new(0.5, 0.5),
            Size = UDim2.new(1, 0, 1, 0),
            Position = UDim2.new(0.5, 0, 0.5, 0),

            CanvasSize = self.listLength:map(function(length)
                return UDim2.new(0, minCellWidth, 0, length)
            end),
        }, listElements)
    or
        Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0.5, 0.5),
            Size = UDim2.new(1, 0, 1, 0),
            Position = UDim2.new(0.5, 0, 0.5, 0),
            BackgroundTransparency = 0,

            Text = uiTranslations["NoSelectionColorProperties_Message"],
            TextXAlignment = Enum.TextXAlignment.Center,
            TextYAlignment = Enum.TextYAlignment.Center,
            TextWrapped = true,
        }, {
            UIPadding = Roact.createElement(StandardUIPadding, {
                paddings = {Style.Constants.PagePadding}
            })
        })
end

---

ColorPropertyListItem = ConnectTheme(ColorPropertyListItem)
return ColorPropertiesList


-- Interface for importing settings

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

---

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

local CommonModules = Common.Modules
local ColorPaneUserDataDefaultValues = require(CommonModules.ColorPaneUserDataDefaultValues)
local ColorPaneUserDataValidators = require(CommonModules.ColorPaneUserDataValidators)
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)

local CommonIncludes = Common.Includes
local Cryo = require(CommonIncludes.Cryo)
local Promise = require(CommonIncludes.Promise)
local Roact = require(CommonIncludes.RoactRodux.Roact)
local t = require(CommonIncludes.t)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local ConnectTheme = require(CommonComponents.ConnectTheme)
local RadioButtonGroup = require(CommonComponents.RadioButtonGroup)

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 Modules = root.Modules
local CompanionUserDataDefaultValues = require(Modules.CompanionUserDataDefaultValues)
local CompanionUserDataValidators = require(Modules.CompanionUserDataValidators)
local Constants = require(Modules.Constants)

---

local importTypes = { "ModuleScript", "StringValue", "File" }

local importOptions = {
    "ModuleScript",
    "StringValue",
    Translator.FormatByKey("JSONFile_ImportType"),
}

local uiTranslations = Translator.GenerateTranslationTable({
    "JSONFile_ImportType",

    "UseSelection_ButtonText",
    "SelectAFile_ButtonText",
    "Import_ButtonText",
    "Cancel_ButtonText",

    "WaitingForImport_Message",
    "EmptySelection_Message",
    "MultipleSelections_Message",
})

local settingsTableValidator: (any) -> (boolean, string?) = t.interface({
    [Constants.COLORPANE_USERDATA_KEY] = t.interface({
        AskNameBeforePaletteCreation = t.optional(ColorPaneUserDataValidators.AskNameBeforePaletteCreation),
        SnapValue = t.optional(ColorPaneUserDataValidators.SnapValue),
        UserColorPalettes = t.optional(ColorPaneUserDataValidators.UserColorPalettes),
        UserGradientPalettes = t.optional(ColorPaneUserDataValidators.UserGradientPalettes),
    }),

    [Constants.COMPANION_USERDATA_KEY] = t.interface({
        AutoLoadColorPropertiesAPIData = t.optional(CompanionUserDataValidators.AutoLoadColorPropertiesAPIData),
        CacheColorPropertiesAPIData = t.optional(CompanionUserDataValidators.CacheColorPropertiesAPIData),
        RobloxApiDump = t.optional(CompanionUserDataValidators.RobloxApiDump),
    }),
})

local statusIcons = {
    ok = Style.Images.ResultOkIcon,
    notOk = Style.Images.ResultNotOkIcon,
    wait = Style.Images.ResultWaitingIcon,
}

local statusColorGenerators = {
    ok = function()
        return Color3.fromRGB(0, 170, 0)
    end,

    notOk = function(theme)
        return theme:GetColor(Enum.StudioStyleGuideColor.ErrorText)
    end,
}

---

--[[
    props

        onPromptClosed: ({}?) -> ()

    store props

        theme: StudioTheme
]]
local ImportSettings = Roact.PureComponent:extend("ImportSettings")

ImportSettings.init = function(self)
    self.statusIcon = Roact.createRef()
    self.listLength, self.updateListLength = Roact.createBinding(0)

    self.importSuccessHandler = function(message)
        return function(settings)
            if (settings) then
                self:setState({
                    settings = settings,

                    status = "ok",
                    statusMessage = message
                })
            else
                self:setState({
                    status = Roact.None,
                    statusMessage = Roact.None,
                })
            end
        end
    end

    self.importErrorHandler = function(error)
        if (Promise.Error.is(error)) then
            error = error.error
        end

        self:setState({
            status = "notOk",
            statusMessage = error,
        })
    end
end

ImportSettings.didMount = function(self)
    self.rotator = RunService.Heartbeat:Connect(function(step)
        local statusIcon = self.statusIcon:getValue()
        if (not statusIcon) then return end

        if (self.state.status == "wait") then
            statusIcon.Rotation = (statusIcon.Rotation + (step * 60)) % 360
        else
            statusIcon.Rotation = 0
        end
    end)
end

ImportSettings.willUnmount = function(self)
    self.rotator:Disconnect()
end

ImportSettings.render = function(self)
    local theme = self.props.theme

    local status = self.state.status
    local importType = self.state.importType
    local settings = self.state.settings
    local importPage

    if (importType == "ModuleScript") then
        importPage = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, (Style.Constants.StandardTextSize * 3) + Style.Constants.StandardButtonHeight + (Style.Constants.MinorElementPadding * 2)),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            LayoutOrder = 3,
        }, {
            Instructions = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),
                Position = UDim2.new(0, 0, 0, 0),
                Text = Translator.FormatByKey("SelectObject_Prompt", { "ModuleScript" }),
            }),

            ConfirmButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding),
                Size = UDim2.new(0, 80, 0, Style.Constants.StandardButtonHeight),

                displayType = "text",
                text = uiTranslations["UseSelection_ButtonText"],

                onActivated = function()
                    self:setState({
                        settings = Roact.None,
                    })

                    Promise.new(function(resolve, reject)
                        local selection = Selection:Get()

                        if (#selection > 1) then
                            reject(uiTranslations["MultipleSelections_Message"])
                            return
                        elseif (#selection < 1) then
                            reject(uiTranslations["EmptySelection_Message"])
                            return
                        end

                        local object = selection[1]

                        if (not object:IsA("ModuleScript")) then
                            reject(Translator.FormatByKey("InvalidSelection_Message", { "ModuleScript" }))
                            return
                        end

                        local newSettingsTable = require(selection[1])

                        if (type(newSettingsTable) == "string") then
                            newSettingsTable = HttpService:JSONDecode(newSettingsTable)
                        end

                        local isValid, message = settingsTableValidator(newSettingsTable)

                        if (isValid) then
                            resolve(newSettingsTable)
                        else
                            reject(Translator.FormatByKey("NonConformantSettingsTable_Message", { message }))
                        end
                    end):andThen(
                        self.importSuccessHandler(Translator.FormatByKey("ValidSettingsTableImport_Message", { "ModuleScript" })),
                        self.importErrorHandler
                    )
                end,
            }),

            StatusIcon = Roact.createElement("ImageLabel", {
                AnchorPoint = Vector2.new(0, 0),
                Size = Style.UDim2.StandardButtonSize,
                Position = UDim2.new(0, 80 + Style.Constants.MinorElementPadding, 0, Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,

                Image = if status then statusIcons[status] else "",

                ImageColor3 = if statusColorGenerators[status] then
                    statusColorGenerators[status](theme)
                else theme:GetColor(Enum.StudioStyleGuideColor.MainText),

                [Roact.Ref] = self.statusIcon,
            }),

            StatusText = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
                Position = UDim2.new(0, 0, 0, Style.Constants.StandardTextSize + Style.Constants.StandardButtonHeight + (Style.Constants.MinorElementPadding * 2)),

                Text = self.state.statusMessage or uiTranslations["WaitingForImport_Message"],
                TextXAlignment = Enum.TextXAlignment.Left,
                TextYAlignment = Enum.TextYAlignment.Top,
                TextWrapped = true,
            }),
        })
    elseif (importType == "StringValue") then
        importPage = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, (Style.Constants.StandardTextSize * 3) + Style.Constants.StandardButtonHeight + (Style.Constants.MinorElementPadding * 2)),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            LayoutOrder = 3,
        }, {
            Instructions = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),
                Position = UDim2.new(0, 0, 0, 0),
                Text = Translator.FormatByKey("SelectObject_Prompt", { "StringValue" }),
            }),

            ConfirmButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding),
                Size = UDim2.new(0, 80, 0, Style.Constants.StandardButtonHeight),

                displayType = "text",
                text = uiTranslations["UseSelection_ButtonText"],

                onActivated = function()
                    self:setState({
                        settings = Roact.None,
                    })

                    Promise.new(function(resolve, reject)
                        local selection = Selection:Get()

                        if (#selection > 1) then
                            reject(uiTranslations["MultipleSelections_Message"])
                            return
                        elseif (#selection < 1) then
                            reject(uiTranslations["EmptySelection_Message"])
                            return
                        end

                        local object = selection[1]

                        if (not object:IsA("StringValue")) then
                            reject(Translator.FormatByKey("InvalidSelection_Message", { "StringValue" }))
                            return
                        end

                        local newSettingsTable = HttpService:JSONDecode(object.Value)
                        local isValid, message = settingsTableValidator(newSettingsTable)

                        if (isValid) then
                            resolve(newSettingsTable)
                        else
                            reject(Translator.FormatByKey("NonConformantSettingsTable_Message", { message }))
                        end
                    end):andThen(
                        self.importSuccessHandler(Translator.FormatByKey("ValidSettingsTableImport_Message", { "StringValue" })),
                        self.importErrorHandler
                    )
                end,
            }),

            StatusIcon = Roact.createElement("ImageLabel", {
                AnchorPoint = Vector2.new(0, 0),
                Size = Style.UDim2.StandardButtonSize,
                Position = UDim2.new(0, 80 + Style.Constants.MinorElementPadding, 0, Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,

                Image = if status then statusIcons[status] else "",

                ImageColor3 = if statusColorGenerators[status] then
                    statusColorGenerators[status](theme)
                else theme:GetColor(Enum.StudioStyleGuideColor.MainText),

                [Roact.Ref] = self.statusIcon,
            }),

            StatusText = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
                Position = UDim2.new(0, 0, 0, Style.Constants.StandardTextSize + Style.Constants.StandardButtonHeight + (Style.Constants.MinorElementPadding * 2)),

                Text = self.state.statusMessage or uiTranslations["WaitingForImport_Message"],
                TextXAlignment = Enum.TextXAlignment.Left,
                TextYAlignment = Enum.TextYAlignment.Top,
                TextWrapped = true,
            }),
        })
    elseif (importType == "File") then
        importPage = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding + (Style.Constants.StandardTextSize * 2)),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            LayoutOrder = 3,
        }, {
            ImportButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, 0),
                Size = Style.UDim2.DialogButtonSize,

                disabled = (status == "wait"),
                displayType = "text",
                text = uiTranslations["SelectAFile_ButtonText"],

                onActivated = function()
                    self:setState({
                        settings = Roact.None,
                        status = "wait",
                    })

                    local file

                    Promise.new(function(resolve, reject)
                        file = StudioService:PromptImportFile({"json"})

                        if (not file) then
                            resolve()
                            return
                        end

                        local newSettingsTable = HttpService:JSONDecode(file:GetBinaryContents())
                        local isValid, message = settingsTableValidator(newSettingsTable)

                        if (isValid) then
                            resolve(newSettingsTable)
                        else
                            reject(Translator.FormatByKey("NonConformantSettingsTable_Message", { message }))
                        end
                    end):andThen(
                        self.importSuccessHandler(Translator.FormatByKey("ValidSettingsTableImport_Message", { "file" })),
                        self.importErrorHandler
                    ):finally(function()
                        if (file) then
                            file:Destroy()
                            file = nil
                        end
                    end)
                end,
            }),

            StatusIcon = Roact.createElement("ImageLabel", {
                AnchorPoint = Vector2.new(0, 0),
                Size = Style.UDim2.StandardButtonSize,
                Position = UDim2.new(0, Style.Constants.DialogButtonWidth + Style.Constants.MinorElementPadding, 0, 0),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,

                Image = if status then statusIcons[status] else "",

                ImageColor3 = if statusColorGenerators[status] then
                    statusColorGenerators[status](theme)
                else theme:GetColor(Enum.StudioStyleGuideColor.MainText),

                [Roact.Ref] = self.statusIcon,
            }),

            StatusText = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
                Position = UDim2.new(0, 0, 0, Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding),

                Text = self.state.statusMessage or uiTranslations["WaitingForImport_Message"],
                TextXAlignment = Enum.TextXAlignment.Left,
                TextYAlignment = Enum.TextYAlignment.Top,
                TextWrapped = true,
            }),
        })
    end

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

        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        Dialog = Roact.createElement(StandardScrollingFrame, {
            AnchorPoint = Vector2.new(0.5, 0),
            Size = UDim2.new(1, 0, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding)),
            Position = UDim2.new(0.5, 0, 0, 0),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            CanvasSize = self.listLength:map(function(length)
                return UDim2.new(0, 0, 0, length)
            end),
        }, {
            UIPadding = Roact.createElement(StandardUIPadding, {
                paddings = {0, 0, 0, Style.Constants.SpaciousElementPadding}
            }),

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

                [Roact.Change.AbsoluteContentSize] = function(obj)
                    self.updateListLength(obj.AbsoluteContentSize.Y)
                end,

                preset = 1,
            }),

            ImportType = Roact.createElement(RadioButtonGroup, {
                Size = UDim2.new(1, 0, 0, (Style.Constants.StandardButtonHeight * 3) + (Style.Constants.MinorElementPadding * 2)),
                LayoutOrder = 1,

                selected = table.find(importTypes, importType),
                options = importOptions,

                onSelected = function(i)
                    self:setState({
                        importType = importTypes[i],
                        settings = Roact.None,

                        status = Roact.None,
                        statusMessage = Roact.None,
                    })
                end,
            }),

            Separator1 = if importType then
                Roact.createElement("Frame", {
                    Size = UDim2.new(1, 0, 0, 1),
                    BackgroundTransparency = 0,
                    BorderSizePixel = 0,
                    LayoutOrder = 2,

                    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Separator)
                })
            else nil,

            ImportPage = if importType then importPage else nil,
        }),

        Buttons = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(1, 1),
            Position = UDim2.new(1, 0, 1, 0),
            Size = UDim2.new(0, Style.Constants.DialogButtonWidth * 2 + Style.Constants.SpaciousElementPadding, 0, Style.Constants.StandardButtonHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            UIListLayout = Roact.createElement(StandardUIListLayout, {
                Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),

                preset = 2,
            }),

            CancelButton = Roact.createElement(Button, {
                Size = Style.UDim2.DialogButtonSize,
                LayoutOrder = 0,

                displayType = "text",
                text = uiTranslations["Cancel_ButtonText"],

                backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton),
                borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton, Enum.StudioStyleGuideModifier.Hover),
                displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonText),

                onActivated = function()
                    self.props.onPromptClosed(nil)
                end
            }),

            ImportButton = Roact.createElement(Button, {
                Size = Style.UDim2.DialogButtonSize,
                LayoutOrder = 1,

                disabled = (not settings),
                displayType = "text",
                text = uiTranslations["Import_ButtonText"],

                backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton),
                borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Hover),
                displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText),

                onActivated = function()
                    local filledSettingsTable = {
                        [Constants.COLORPANE_USERDATA_KEY] = Cryo.Dictionary.join(ColorPaneUserDataDefaultValues, settings[Constants.COLORPANE_USERDATA_KEY]),
                        [Constants.COMPANION_USERDATA_KEY] = Cryo.Dictionary.join(CompanionUserDataDefaultValues, settings[Constants.COMPANION_USERDATA_KEY]),
                    }

                    self.props.onPromptClosed(filledSettingsTable)
                end
            }),
        })
    })
end

return ConnectTheme(ImportSettings)