--!strict
--[[
    The ColorPane API.

    Please refer to the documentation for the available API
    members.
]]

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

local CommonModules = Common.Modules
local CommonEnums = require(CommonModules.Enums)
local Constants = require(CommonModules.Constants)
local PluginProvider = require(CommonModules.PluginProvider)
local Translator = require(CommonModules.Translator)
local Window = require(CommonModules.Window)

local CommonIncludes = Common.Includes
local ColorLib = require(CommonIncludes.Color)
local Cryo = require(CommonIncludes.Cryo)
local Promise = require(CommonIncludes.Promise)
local Roact = require(CommonIncludes.RoactRodux.Roact)
local Signal = require(CommonIncludes.Signal)

local Modules = root.Modules
local APITypeValidators = require(Modules.APITypeValidators)
local EditorInputSignals = require(Modules.EditorInputSignals)
local ManagedUserData = require(Modules.ManagedUserData)
local Enums = require(Modules.Enums)
local Store = require(Modules.Store)
local Types = require(Modules.Types)
local Util = require(Modules.Util)
local WidgetInfo = require(Modules.WidgetInfo)

local Components = root.Components
local ColorEditorComponent = require(Components.ColorEditor)
local GradientEditorComponent = require(Components.GradientEditor)

local Color = ColorLib.Color
local Gradient = ColorLib.Gradient

---

type Promise = typeof(Promise.new())

type Color = ColorLib.Color
type Gradient = ColorLib.Gradient
type GradientKeypoint = ColorLib.GradientKeypoint
type MixableColorType = ColorLib.MixableColorType
type HueAdjustment = ColorLib.HueAdjustment

type ColorPromptInfoExpectingColor = {
    PromptTitle: string,
    InitialColor: (Color | Color3),
    ColorType: "Color",
    OnColorChanged: ((Color) -> ())?
}

type ColorPromptInfoExpectingColor3 = {
    PromptTitle: string,
    InitialColor: (Color | Color3),
    ColorType: "Color3",
    OnColorChanged: ((Color3) -> ())?
}

type ColorPromptInfo = ColorPromptInfoExpectingColor | ColorPromptInfoExpectingColor3

type GradientPromptInfoExpectingGradient = {
    PromptTitle: string,
    InitialGradient: (Gradient | ColorSequence),
    InitialColorSpace: MixableColorType,
    InitialHueAdjustment: HueAdjustment,
    InitialPrecision: number,
    GradientType: "Gradient",
    OnGradientChanged: ((Gradient) -> ())?
}

type GradientPromptInfoExpectingColorSequence = {
    PromptTitle: string,
    InitialGradient: (Gradient | ColorSequence),
    InitialColorSpace: MixableColorType,
    InitialHueAdjustment: HueAdjustment,
    InitialPrecision: number,
    GradientType: "ColorSequence",
    OnGradientChanged: ((ColorSequence) -> ())?
}

type GradientPromptInfo = GradientPromptInfoExpectingGradient | GradientPromptInfoExpectingColorSequence

type ColorSequencePromptInfoArgument = {
    PromptTitle: string?,
    InitialColor: ColorSequence?,
    OnColorChanged: ((ColorSequence) -> ())?
}

---

local UI_TRANSLATIONS = Translator.GenerateTranslationTable({
    "ColorEditor_DefaultWindowTitle",
    "GradientEditor_DefaultWindowTitle",
})

local DEFAULT_COLOR_PROMPT_INFO: ColorPromptInfo = {
    PromptTitle = UI_TRANSLATIONS["ColorEditor_DefaultWindowTitle"],
    ColorType = "Color3",
    InitialColor = Color3.new(1, 1, 1),
}

local DEFAULT_GRADIENT_PROMPT_INFO: GradientPromptInfo = {
    PromptTitle = UI_TRANSLATIONS["GradientEditor_DefaultWindowTitle"],
    GradientType = "ColorSequence",
    InitialGradient = ColorSequence.new(Color3.new(1, 1, 1)),
    InitialColorSpace = "RGB",
    InitialHueAdjustment = "Shorter",
    InitialPrecision = 0,
}

local plugin: Plugin = PluginProvider()

local colorEditorWindow: Window.Window = Window.new(WidgetInfo.ColorEditor.Id, WidgetInfo.ColorEditor.Info)
local gradientEditorWindow: Window.Window = Window.new(WidgetInfo.GradientEditor.Id, WidgetInfo.GradientEditor.Info)

local fireColorEditFinished: Signal.FireSignal<boolean> = function(_: boolean) end
local fireGradientEditFinished: Signal.FireSignal<boolean> = function(_: boolean) end

--[[
    This is the actual color prompting function.
]]
local _promptForColor = function(promptInfo: Types.ColorPromptInfoArgument?): Promise
    local fullPromptInfo: ColorPromptInfo = Cryo.Dictionary.join(DEFAULT_COLOR_PROMPT_INFO, promptInfo or {})
    local finishedSignal: Signal.Signal<boolean>, fireFinished: Signal.FireSignal<boolean> = Signal.createSignal()
    local initialColor: Color

    -- Colors are used internally, so we need to convert the user-provided color
    do
        local unconvertedInitialColor: Color | Color3 = fullPromptInfo.InitialColor

        if (typeof(unconvertedInitialColor) == "Color3") then
            initialColor = Color.fromColor3(unconvertedInitialColor)
        else
            initialColor = unconvertedInitialColor
        end
    end

    local editPromise = Promise.new(function(resolve, reject, onCancel)
        local subscription: Signal.Subscription

        subscription = finishedSignal:subscribe(function(confirmed: boolean)
            subscription:unsubscribe()

            if (confirmed) then
                local newColor: Color = Store:getState().colorEditor.color

                -- if the user provided an initial color, we need to check if the new color is the same
                if (promptInfo and (promptInfo.InitialColor ~= nil) and (newColor == initialColor)) then
                    reject(Enums.PromptRejection.SameAsInitial)
                    return
                end

                if (fullPromptInfo.ColorType == "Color3") then
                    (resolve::(Color3) -> ())(newColor:toColor3())
                else
                    (resolve::(Color) -> ())(newColor)
                end
            else
                reject(Enums.PromptRejection.PromptCancelled)
            end
        end)

        onCancel(function()
            subscription:unsubscribe()
        end)
    end)

    local colorEditorElement = Roact.createElement(ColorEditorComponent, {
        originalColor = initialColor,
        fireFinished = fireFinished,
    })

    -- set up prompt state
    Store:dispatch({
        type = Enums.StoreActionType.ColorEditor_SetColor,
        color = initialColor,
    })

    -- check for color changes
    local storeChanged = if (fullPromptInfo.OnColorChanged) then
        Store.changed:connect(function(newState, oldState)
            local oldColor: Color? = oldState.colorEditor.color
            local newColor: Color? = newState.colorEditor.color
            if (not (oldColor and newColor)) then return end
            if (newColor == oldColor) then return end

            if (fullPromptInfo.ColorType == "Color3") then
                (fullPromptInfo.OnColorChanged::(Color3) -> ())(newColor:toColor3())
            elseif (fullPromptInfo.ColorType == "Color") then
                (fullPromptInfo.OnColorChanged::(Color) -> ())(newColor)
            end
        end)
    else nil

    -- the catch is here to suppress rejection if the user closes the editor
    editPromise:catch(function() end):finally(function()
        if (storeChanged) then
            storeChanged:disconnect()
        end

        colorEditorWindow:unmount(true)

        Store:dispatch({
            type = Enums.StoreActionType.ColorEditor_SetColor,
            color = nil,
        })
    end)

    fireColorEditFinished = fireFinished
    colorEditorWindow:mount(fullPromptInfo.PromptTitle, colorEditorElement, Store)
    return editPromise
end

---

local ColorPane = {}

--[[
    Prompt rejection enum
]]
ColorPane.PromptRejection = Enums.PromptRejection

--[[
    Promise status enum
]]
ColorPane.PromiseStatus = Promise.Status

--[[
    Returns if a request to prompt for a color will succeed
    instead of immediately rejecting.
]]
ColorPane.IsColorPromptAvailable = function(): boolean
    return (not (colorEditorWindow:isMounted() or gradientEditorWindow:isMounted()))
end

--[[
    Returns if a request to prompt for a gradient will succeed
    instead of immediately rejecting.
]]
ColorPane.IsGradientPromptAvailable = function(): boolean
    return (not (colorEditorWindow:isMounted() or gradientEditorWindow:isMounted()))
end

--[[
    Prompts the user for a color. The prompt info table is as follows:

    ```
    {
        PromptTitle: string?,
        InitialColor: (Color | Color3)?,
        ColorType: ("Color" | "Color3"),
        OnColorChanged: (((Color) -> ()) | ((Color3) -> ()))?
    }
    ```

    `OnColorChanged` must not yield.

    The specified `ColorType` and the type parameter to `OnColorChanged` should match, i.e.
    - `ColorType` is `"Color3"`, and `OnColorChanged` accepts a `Color3`, or
    - `ColorType` is `"Color"`, and `OnColorChanged` accepts a `Color`

    but not
    - `ColorType` is `"Color3"`, and `OnColorChanged` accepts a `Color`, nor
    - `ColorType` is `"Color"`, and `OnColorChanged` accepts a `Color3`

    @param promptInfo The prompt info, see above
    @return A Promise that will resolve with a user-generated color, or reject with a rejection reason
]]
ColorPane.PromptForColor = function(promptInfo: Types.ColorPromptInfoArgument?): Promise
    if (gradientEditorWindow:isMounted()) then
        -- gradient editor needs exclusive access to the color editor
        return Promise.reject(Enums.PromptRejection.ReservationProblem)
    elseif (colorEditorWindow:isMounted()) then
        -- can't open the prompt if it's already open
        return Promise.reject(Enums.PromptRejection.PromptAlreadyOpen)
    end

    local isPromptInfoValid: boolean = APITypeValidators.ColorPromptInfoArgument(promptInfo)

    if (not isPromptInfoValid) then
        return Promise.reject(Enums.PromptRejection.InvalidPromptOptions)
    end

    return _promptForColor(promptInfo)
end

--[[
    Prompts the user for a gradient. The prompt info table is as follows:

    ```
    {
        PromptTitle: string?,
        InitialGradient: (Gradient | ColorSequence)?,
        InitialColorSpace: MixableColorType?,
        InitialHueAdjustment: HueAdjustment?,
        InitialPrecision: number?,
        GradientType: ("Gradient" | "ColorSequence")?,
        OnGradientChanged: (((Gradient) -> ()) | ((ColorSequence) -> ()))?
    }
    ```

    `OnGradientChanged` must not yield.

    The specified `GradientType` and the type parameter to `OnGradientChanged` should match, i.e.
    - `GradientType` is `"ColorSequence"`, and `OnGradientChanged` accepts a `ColorSequence`, or
    - `GradientType` is `"Gradient"`, and `OnGradientChanged` accepts a `Gradient`

    but not
    - `GradientType` is `"ColorSequence"`, and `OnGradientChanged` accepts a `Gradient`, nor
    - `GradientType` is `"Gradient"`, and `OnGradientChanged` accepts a `ColorSequence`.

    @param promptInfo The prompt info, see above
    @return A Promise that will resolve with a user-generated gradient, or reject with a rejection reason
]]
ColorPane.PromptForGradient = function(promptInfo: Types.GradientPromptInfoArgument?): Promise
    if (colorEditorWindow:isMounted()) then
        -- gradient editor needs exclusive access to the color editor
        return Promise.reject(Enums.PromptRejection.ReservationProblem)
    elseif (gradientEditorWindow:isMounted()) then
        -- can't open the prompt if it's already open
        return Promise.reject(Enums.PromptRejection.PromptAlreadyOpen)
    end

    local isPromptInfoValid: boolean = APITypeValidators.GradientPromptInfoArgument(promptInfo)

    if (not isPromptInfoValid) then
        return Promise.reject(Enums.PromptRejection.InvalidPromptOptions)
    end

    local fullPromptInfo: GradientPromptInfo = Cryo.Dictionary.join(DEFAULT_GRADIENT_PROMPT_INFO, promptInfo or {})
    local finishedSignal: Signal.Signal<boolean>, fireFinished: Signal.FireSignal<boolean> = Signal.createSignal()

    local initialGradient: Gradient
    local initialKeypoints: {GradientKeypoint}

    -- Gradients are used internally, so we need to convert the user-provided gradient
    do
        local unconvertedInitialGradient: Gradient | ColorSequence = fullPromptInfo.InitialGradient

        if (typeof(unconvertedInitialGradient) == "ColorSequence") then
            initialGradient = Gradient.fromColorSequence(unconvertedInitialGradient)
        else
            initialGradient = unconvertedInitialGradient
        end

        initialKeypoints = initialGradient.Keypoints
    end

    local isKeypointPrecisionCombinationValid: boolean =
        Util.getUtilisedKeypoints(#initialKeypoints, fullPromptInfo.InitialPrecision)
            <=
        Constants.MAX_COLORSEQUENCE_KEYPOINTS

    if (not isKeypointPrecisionCombinationValid) then
        return Promise.reject(Enums.PromptRejection.InvalidPromptOptions)
    end

    local editPromise = Promise.new(function(resolve, reject, onCancel)
        local subscription: Signal.Subscription

        subscription = finishedSignal:subscribe(function(confirmed: boolean)
            subscription:unsubscribe()

            if (confirmed) then
                local newKeypoints: {GradientKeypoint} = Store:getState().gradientEditor.keypoints
                local newGradient: Gradient = Gradient.new(newKeypoints)

                -- if the user provided an initial gradient, we need to check if the new gradient is the same
                if (promptInfo and (promptInfo.InitialGradient ~= nil) and (newGradient == initialGradient)) then
                    reject(Enums.PromptRejection.SameAsInitial)
                    return
                end

                if (fullPromptInfo.GradientType == "ColorSequence") then
                    (resolve::(ColorSequence) -> ())(newGradient:toColorSequence())
                else
                    (resolve::(Gradient) -> ())(newGradient)
                end
            else
                reject(Enums.PromptRejection.PromptCancelled)
            end
        end)

        onCancel(function()
            subscription:unsubscribe()
        end)
    end)

    local gradientEditorElement = Roact.createElement(GradientEditorComponent, {
        originalKeypoints = initialKeypoints,
        originalColorSpace = fullPromptInfo.InitialColorSpace,
        originalHueAdjustment = fullPromptInfo.InitialHueAdjustment,
        originalPrecision = fullPromptInfo.InitialPrecision,
        fireFinished = fireFinished,

        promptForColorEdit = _promptForColor,
    })

    -- set up prompt state
    Store:dispatch({
        type = Enums.StoreActionType.GradientEditor_SetGradient,

        keypoints = initialKeypoints,
        colorSpace = fullPromptInfo.InitialColorSpace,
        hueAdjustment = fullPromptInfo.InitialHueAdjustment,
        precision = fullPromptInfo.InitialPrecision,
    })

    -- check for color changes
    local storeChanged = if (fullPromptInfo.OnGradientChanged) then
        Store.changed:connect(function(newState, oldState)
            local oldKeypoints: {GradientKeypoint}? = oldState.gradientEditor.displayKeypoints
            local newKeypoints: {GradientKeypoint}? = newState.gradientEditor.displayKeypoints
            if (not (oldKeypoints and newKeypoints)) then return end

            local oldGradient: Gradient = Gradient.new(oldKeypoints)
            local newGradient: Gradient = Gradient.new(newKeypoints)
            if (oldGradient == newGradient) then return end

            if (fullPromptInfo.GradientType == "ColorSequence") then
                (fullPromptInfo.OnGradientChanged::(ColorSequence) -> ())(newGradient:toColorSequence())
            elseif (fullPromptInfo.GradientType == "Gradient") then
                (fullPromptInfo.OnGradientChanged::(Gradient) -> ())(newGradient)
            end
        end)
    else nil

    -- the catch is here to suppress rejection if the user closes the editor
    editPromise:catch(function() end):finally(function()
        if (storeChanged) then
            storeChanged:disconnect()
        end

        gradientEditorWindow:unmount(true)

        Store:dispatch({
            type = Enums.StoreActionType.GradientEditor_ResetState,
        })
    end)

    fireGradientEditFinished = fireFinished
    gradientEditorWindow:mount(fullPromptInfo.PromptTitle, gradientEditorElement, Store)
    return editPromise
end

--[[
    **DEPRECATED**

    Returns the API version.
]]
ColorPane.GetVersion = function(): (number, number, number)
    return 0, 5, 0
end

--[[
    **DEPRECATED**: You should subscribe to your plugin's Unloading event instead.

    Fires when the API is about to unload
]]
ColorPane.Unloading = plugin.Unloading

--[[
    **DEPRECATED**: Use `ColorPane.PromptRejection` instead

    Prompt rejection enum
]]
ColorPane.PromptError = Enums.PromptRejection

--[[
    **DEPRECATED**: Use `ColorPane.PromptForGradient` instead.

    Prompts the user for a ColorSequence. The prompt info table is as follows:

    ```
    {
        PromptTitle: string?,
        InitialColor: ColorSequence?,
        OnColorChanged: ((ColorSequence) -> ())?
    }
    ```

    @param promptInfo The prompt info, see above
    @return A Promise that will resolve with a user-generated ColorSequence, or reject with a rejection reason
]]
ColorPane.PromptForColorSequence = function(promptInfo: ColorSequencePromptInfoArgument?): Promise
    return ColorPane.PromptForGradient({
        PromptTitle = promptInfo and promptInfo.PromptTitle,
        InitialGradient = promptInfo and promptInfo.InitialColor,
        GradientType = "ColorSequence",
        OnGradientChanged = promptInfo and promptInfo.OnColorChanged,
    })
end

--[[
    **DEPRECATED**: Use `ColorPane.IsColorPromptAvailable` instead.
]]
ColorPane.IsColorEditorOpen = function(): boolean
    return colorEditorWindow:isMounted()
end

--[[
    **DEPRECATED**: Use `ColorPane.IsGradientPromptAvailable` instead.
]]
ColorPane.IsGradientEditorOpen = function(): boolean
    return gradientEditorWindow:isMounted()
end

--[[
    **DEPRECATED**: Use `ColorPane.IsGradientPromptAvailable` instead.
]]
ColorPane.IsColorSequenceEditorOpen = function(): boolean
    return gradientEditorWindow:isMounted()
end

---

-- hook editor input signals
colorEditorWindow:enableMouseTracking()
gradientEditorWindow:enableMouseTracking()

colorEditorWindow.mousePositionChanged:subscribe(EditorInputSignals.ColorEditor.MousePositionChanged.Fire)
gradientEditorWindow.mousePositionChanged:subscribe(EditorInputSignals.GradientEditor.MousePositionChanged.Fire)

Store.changed:connect(function(newState, oldState)
    -- update non-palette user data
    if (newState.userData ~= oldState.userData) then
        for key: string, value: any in pairs(newState.userData) do
            ManagedUserData.SetValue(key, value)
        end
    end

    -- update color palettes
    if (newState.colorEditor.palettes ~= oldState.colorEditor.palettes) then
        local newPalettes = Util.table.deepCopy(newState.colorEditor.palettes)

        for i = 1, #newPalettes do
            local palette = newPalettes[i]

            for j = 1, #palette.colors do
                local color = palette.colors[j]
                local colorValue = color.color

                color.color = {colorValue.R, colorValue.G, colorValue.B}
            end
        end

        ManagedUserData.SetValue(CommonEnums.ColorPaneUserDataKey.UserColorPalettes, newPalettes)
    end

    -- update gradient palettes
    if (newState.gradientEditor.palettes ~= oldState.gradientEditor.palettes) then
        local newPalettes = Util.table.deepCopy(newState.gradientEditor.palettes)

        for i = 1, #newPalettes do
            local palette = newPalettes[i]
            local gradients = palette.gradients

            for j = 1, #gradients do
                local gradient = gradients[j]
                local keypoints = gradient.keypoints

                for k = 1, #keypoints do
                    local keypoint = keypoints[k]

                    keypoints[k] = {
                        time = keypoint.Time,
                        color = {keypoint.Color:components()}
                    }
                end
            end
        end

        ManagedUserData.SetValue(CommonEnums.ColorPaneUserDataKey.UserGradientPalettes, newPalettes)
    end
end)

-- color editor must stay closed when unmounted
colorEditorWindow.openedWithoutMounting:subscribe(function()
    colorEditorWindow:close()
end)

-- gradient editor must stay closed when unmounted
gradientEditorWindow.openedWithoutMounting:subscribe(function()
    gradientEditorWindow:close()
end)

-- user closing the color editor is the same as cancelling the prompt
colorEditorWindow.closedWithoutUnmounting:subscribe(function()
    fireColorEditFinished(false)
end)

-- user closing the gradient editor is the same as cancelling the prompt
gradientEditorWindow.closedWithoutUnmounting:subscribe(function()
    fireGradientEditFinished(false)
end)

plugin.Unloading:Connect(function()
    fireColorEditFinished(false)
    fireGradientEditFinished(false)

    colorEditorWindow:destroy()
    gradientEditorWindow:destroy()
end)

-- make sure the user doesn't accidentally modify these
table.freeze(Enums.PromptRejection)
table.freeze(ColorPane)

return ColorPane


-- A container for multiple buttons, where one button is selected at a time

local Common = script.Parent.Parent.Common

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

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

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

local StandardComponents = CommonComponents.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUICorner = require(StandardComponents.UICorner)
local StandardUIListLayout = require(StandardComponents.UIListLayout)

---

--[[
    props
        AnchorPoint?
        Position?
        Size?
        LayoutOrder?

        title: string?
        vertical: boolean?
        selected: number?
        customLayout: boolean?

        displayType: "image" | "text"

        buttons: array<{
            name: string,
            text: string?,
            image: string?,

            disabled: boolean?,

            displayColor: Color3?,
            selctedDisplayColor: Color3?,
            disabledDisplayColor: Color3?
        }>

        onButtonActivated: (number) -> nil

    store props
        theme: StudioTheme
]]

local buttonTypes = {
    image = "ImageButton",
    text = "TextButton"
}

local getTextButtonProps = function(text, textColor)
    return {
        Font = Style.Fonts.Standard,
        TextSize = Style.Constants.StandardTextSize,
        TextXAlignment = Enum.TextXAlignment.Center,
        TextYAlignment = Enum.TextYAlignment.Center,
        Text = text,
        TextColor3 = textColor,
    }
end

local getImageButtonProps = function(image, imageColor)
    return {
        Image = image,
        ScaleType = Enum.ScaleType.Fit,
        ImageColor3 = imageColor,
    }
end

---

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

ButtonBar.render = function(self)
    local theme = self.props.theme
    local displayType = self.props.displayType
    local selected = self.props.selected

    local buttonElements = {}
    local numButtons = #self.props.buttons

    for i = 1, numButtons do
        local buttonInfo = self.props.buttons[i]

        local buttonProps: {[any]: any} = {
            Size = if self.props.vertical then
                UDim2.new(1, -2, 1 / numButtons, if ((i == 1) or (i == numButtons)) then -1 else 0)
            else UDim2.new(1 / numButtons, if ((i == 1) or (i == numButtons)) then -1 else 0, 1, -2),

            BackgroundTransparency = 0,
            BorderSizePixel = 0,
            LayoutOrder = if self.props.customLayout then buttonInfo.order else i,
            AutoButtonColor = false,

            BackgroundColor3 = theme:GetColor(
                Enum.StudioStyleGuideColor.Button,
                if (selected == i) then Enum.StudioStyleGuideModifier.Selected else nil
            ),

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

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

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

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

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

                self.props.onButtonActivated(i)
            end
        }

        local displayColor

        if (buttonInfo.disabled) then
            displayColor = buttonInfo.disabledDisplayColor or theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Disabled)
        else
            if (selected == i) then
                displayColor = buttonInfo.selectedDisplayColor or theme:GetColor(
                    Enum.StudioStyleGuideColor.ButtonText,
                    Enum.StudioStyleGuideModifier.Selected
                )
            else
                displayColor = buttonInfo.displayColor or theme:GetColor(Enum.StudioStyleGuideColor.ButtonText)
            end
        end

        if (displayType == "image") then
            buttonProps = Cryo.Dictionary.join(buttonProps, getImageButtonProps(buttonInfo.image, displayColor))
        elseif (displayType == "text") then
            buttonProps = Cryo.Dictionary.join(buttonProps, getTextButtonProps(buttonInfo.name, displayColor))
        end

        buttonElements[buttonInfo.name] = Roact.createElement(buttonTypes[displayType], buttonProps)
    end

    buttonElements.UICorner = Roact.createElement(StandardUICorner)
    buttonElements.UIListLayout = Roact.createElement(StandardUIListLayout, {
        FillDirection = if self.props.vertical then Enum.FillDirection.Vertical else Enum.FillDirection.Horizontal,
        HorizontalAlignment = Enum.HorizontalAlignment.Center,
        VerticalAlignment = Enum.VerticalAlignment.Center,
    })

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Size = self.props.Size,
        Position = self.props.Position,
        LayoutOrder = self.props.LayoutOrder,
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        TitleLabel = if (self.props.title and (not self.props.vertical)) then
            Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),

                Font = Style.Fonts.Standard,
                Text = self.props.title .. ": " .. self.props.buttons[selected].name,
            })
        else nil,

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

            Size = if self.props.vertical then
                UDim2.new(1, 0, 1, 0)
            else UDim2.new(1, 0, 1, if self.props.title then
                -(Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding)
            else 0),

            BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ButtonBorder)
        }, buttonElements)
    })
end

return ConnectTheme(ButtonBar)


-- Palette page for ColorBrewer palettes

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

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

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

local CommonComponents = Common.Components
local StandardTextLabel = require(CommonComponents.StandardComponents.TextLabel)

local Modules = root.Modules
local ColorBrewerPalette = require(Modules.BuiltInPalettes.ColorBrewer)
local Enums = require(Modules.Enums)

local Components = root.Components
local ButtonBar = require(Components.ButtonBar)
local ColorGrids = require(Components.ColorGrids)

---

local buttonBarHeight = Style.Constants.StandardButtonHeight + Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding

local uiTranslations = Translator.GenerateTranslationTable({
    "DataClass_SelectorText",
    "NumDataClass_SelectorText",
    "NoMatchingSchemes_Message",
})

local dataClasses = {
    {
        name = Translator.FormatByKey("Sequential_CBDataClass"),
        image = Style.Images.SequentialDataTypeButtonIcon,
        order = 1,
    },

    {
        name = Translator.FormatByKey("Diverging_CBDataClass"),
        image = Style.Images.DivergingDataTypeButtonIcon,
        order = 2,
    },

    {
        name = Translator.FormatByKey("Qualitative_CBDataClass"),
        image = Style.Images.QualitativeDataTypeButtonIcon,
        order = 3,
    }
}

local numDataClassesButtons = {}

---

for i = 1, 10 do
    local num = i + 2

    table.insert(numDataClassesButtons, {
        name = num,
        text = num,
    })
end

for _, scheme in pairs(ColorBrewerPalette) do
    for _, colors in pairs(scheme.colorSets) do
        for i = 1, #colors do
            local color = colors[i]

            colors[i] = Color3.fromRGB(color[1], color[2], color[3])
        end
    end
end

---

--[[
    store props
        dataClass: number
        numDataClasses: number

        setColor: (Color3) -> nil
        setDataClass: (number) -> nil
        setNumDataClasses: (number) -> nil
]]

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

ColorBrewerPalettes.render = function(self)
    local includedColorSchemes = {}
    local numIncludedColorSchemes = 0

    for schemeName, scheme in pairs(ColorBrewerPalette) do
        local colorSet = scheme.colorSets[self.props.numDataClasses]

        if ((scheme.type == dataClasses[self.props.dataClass].name) and colorSet) then
            includedColorSchemes[schemeName] = colorSet
            numIncludedColorSchemes = numIncludedColorSchemes + 1
        end
    end

    return Roact.createFragment({
        DataTypeSelector = Roact.createElement(ButtonBar, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, 0),
            Size = UDim2.new(1, 0, 0, buttonBarHeight),

            displayType = "image",
            title = uiTranslations["DataClass_SelectorText"],
            selected = self.props.dataClass,
            buttons = dataClasses,

            onButtonActivated = self.props.setDataClass
        }),

        NumDataClassesSelector = Roact.createElement(ButtonBar, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, buttonBarHeight + Style.Constants.MinorElementPadding),
            Size = UDim2.new(1, 0, 0, buttonBarHeight),

            displayType = "text",
            title = uiTranslations["NumDataClass_SelectorText"],
            selected = self.props.numDataClasses - 2,
            buttons = numDataClassesButtons,

            onButtonActivated = function(i)
                self.props.setNumDataClasses(i + 2)
            end
        }),

        InfoText = if (numIncludedColorSchemes == 0) then
            Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 1),
                Position = UDim2.new(0.5, 0, 1, 0),
                Size = UDim2.new(1, -2, 1, -((buttonBarHeight * 2) + Style.Constants.MajorElementPadding)),

                Text = uiTranslations["NoMatchingSchemes_Message"],
                TextXAlignment = Enum.TextXAlignment.Center,
                TextYAlignment = Enum.TextYAlignment.Top,
                TextWrapped = true,
            })
        else nil,

        Schemes = Roact.createElement(ColorGrids, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(1, -2, 1, -((buttonBarHeight * 2) + Style.Constants.MajorElementPadding)),

            named = true,
            colorLists = includedColorSchemes,

            onColorSelected = function(index, schemeName)
                self.props.setColor(Color.fromColor3(includedColorSchemes[schemeName][index]))
            end
        }),
    })
end

return RoactRodux.connect(function(state)
    local sessionData = state.sessionData

    return {
        dataClass = sessionData.cbDataClass,
        numDataClasses = sessionData.cbNumDataClasses,
    }
end, function(dispatch)
    return {
        setColor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = newColor
            })
        end,

        setDataClass = function(dataClass)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    cbDataClass = dataClass
                }
            })
        end,

        setNumDataClasses = function(numDataClasses)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    cbNumDataClasses = numDataClasses
                }
            })
        end,
    }
end)(ColorBrewerPalettes)


-- The entire color editor interface

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

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

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

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local TextInput = require(CommonComponents.TextInput)

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

local Modules = root.Modules
local ColorEditorInputSignals = require(Modules.EditorInputSignals).ColorEditor
local Enums = require(Modules.Enums)
local Util = require(Modules.Util)

local Components = root.Components
local ButtonBar = require(Components.ButtonBar)
local ColorToolPages = require(Components.ColorToolPages)
local ColorWheel = require(Components.ColorWheel)
local PalettePages = require(Components.PalettePages)
local SliderPages = require(Components.SliderPages)

---

local EDITOR_ICON_DISPLAY_COLOR = Color3.new(1, 1, 1)
local EDITOR_ICON_SELECTED_COLOR = Color3.new(1, 1, 1)
local EDITOR_ICON_DISABLED_COLOR = Color3.new(1/2, 1/2, 1/2)

local uiTranslations = Translator.GenerateTranslationTable({
    "Cancel_ButtonText",
    "OK_ButtonText"
})

local indicatorContainerSize = (Style.Constants.StandardButtonHeight * 2) + Style.Constants.MinorElementPadding

local getMaxPages = function(width)
    local maxPagesNoPadding = math.floor(width / Style.Constants.EditorPageWidth)
    local maxPaddingSpaces = math.floor((width % Style.Constants.EditorPageWidth) / Style.Constants.MajorElementPadding)

    return if (maxPaddingSpaces >= (maxPagesNoPadding - 1)) then maxPagesNoPadding else (maxPagesNoPadding - 1)
end

local editorTabs = {
    {
        name = "ColorWheel",
        image = Style.Images.ColorWheelEditorButtonIcon,

        displayColor = EDITOR_ICON_DISPLAY_COLOR,
        selectedDisplayColor = EDITOR_ICON_SELECTED_COLOR,
        disabledDisplayColor = EDITOR_ICON_DISABLED_COLOR,

        getElement = function()
            return Roact.createElement(ColorWheel, {
                ringWidth = Style.Constants.ColorWheelRingWidth,
            })
        end
    },

    {
        name = "Sliders",
        image = Style.Images.SlidersEditorButtonIcon,

        displayColor = EDITOR_ICON_DISPLAY_COLOR,
        selectedDisplayColor = EDITOR_ICON_SELECTED_COLOR,
        disabledDisplayColor = EDITOR_ICON_DISABLED_COLOR,

        getElement = function()
            return Roact.createElement(SliderPages)
        end
    },

    {
        name = "Palettes",
        image = Style.Images.PaletteEditorButtonIcon,

        displayColor = EDITOR_ICON_DISPLAY_COLOR,
        selectedDisplayColor = EDITOR_ICON_SELECTED_COLOR,
        disabledDisplayColor = EDITOR_ICON_DISABLED_COLOR,

        getElement = function()
            return Roact.createElement(PalettePages)
        end
    },

    {
        name = "ColorTools",
        image = Style.Images.ColorToolsEditorButtonIcon,

        getElement = function()
            return Roact.createElement(ColorToolPages)
        end
    }
}

---

--[[
    props
        originalColor: Color
        fireFinished: FireSignal<boolean>

    store props
        theme: StudioTheme
        editorPage: number
        color: Color
        quickPalette: array<Color>

        setColor: (Color) -> nil
        addQuickPaletteColor: (Color) -> nil
        setEditorPage: (number) -> nil
]]

local ColorEditor = Roact.Component:extend("ColorEditor")

ColorEditor.init = function(self)
    self:setState({
        editorWidth = 0,
    })
end

ColorEditor.shouldUpdate = function(self, nextProps, nextState)
    local propsDiff = Util.table.shallowCompare(self.props, nextProps)
    local stateDiff = Util.table.shallowCompare(self.state, nextState)
    if (#propsDiff >= 1) then return true end

    if (#stateDiff == 1) then
        if (stateDiff[1] == "editorWidth") then
            local oldMaxPages = math.clamp(getMaxPages(self.state.editorWidth), 1, #editorTabs)
            local newMaxPages = math.clamp(getMaxPages(nextState.editorWidth), 1, #editorTabs)

            return (oldMaxPages ~= newMaxPages)
        else
            return true
        end
    elseif (#stateDiff > 1) then
        return true
    end

    return false
end

ColorEditor.render = function(self)
    local theme = self.props.theme
    local color = self.props.color
    local originalColor = self.props.originalColor
    local selectedEditor = self.props.editorPage

    local editorPageElements = {}
    local quickPaletteElements = {}
    local editorTabButtons = {}

    local maxPages = math.clamp(getMaxPages(self.state.editorWidth), 1, #editorTabs)
    local numDisabledButtons

    if (maxPages == #editorTabs) then
        numDisabledButtons = #editorTabs
    else
        numDisabledButtons = maxPages - 1
    end

    if (selectedEditor <= numDisabledButtons) then
        selectedEditor = numDisabledButtons + 1
    end

    for i = 1, maxPages do
        local page

        if (maxPages ~= #editorTabs) then
            page = if (i ~= maxPages) then i else selectedEditor
        else
            page = i
        end

        local editorTab = editorTabs[page]

        editorPageElements[editorTab.name] = Roact.createElement("Frame", {
            Size = UDim2.new(1 / maxPages, -Style.Constants.MajorElementPadding * (maxPages - 1) / maxPages, 1, 0),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            LayoutOrder = i
        }, {
            Page = editorTab.getElement(),
        })
    end

    for i = 1, #editorTabs do
        local tab = editorTabs[i]

        table.insert(editorTabButtons, {
            name = tab.name,
            image = tab.image,
            disabled = (i <= numDisabledButtons),

            displayColor = tab.displayColor,
            selectedDisplayColor = tab.selectedDisplayColor,
            disabledDisplayColor = tab.disabledDisplayColor,
        })
    end

    for i = 1, #self.props.quickPalette do
        local quickPaletteColor = self.props.quickPalette[i]

        quickPaletteElements[i] = Roact.createElement(Button, {
            LayoutOrder = i,

            displayType = "color",
            color = quickPaletteColor:toColor3(),

            onActivated = function()
                self.props.setColor(quickPaletteColor)
            end
        })
    end

    editorPageElements.UIListLayout = Roact.createElement(StandardUIListLayout, {
        Padding = UDim.new(0, Style.Constants.MajorElementPadding),
        FillDirection = Enum.FillDirection.Horizontal,
        HorizontalAlignment = Enum.HorizontalAlignment.Center,
        VerticalAlignment = Enum.VerticalAlignment.Center,
    })

    quickPaletteElements.UIGridLayout = Roact.createElement("UIGridLayout", {
        CellPadding = Style.UDim2.MinorElementPaddingSize,
        CellSize = Style.UDim2.StandardButtonSize,
        FillDirection = Enum.FillDirection.Horizontal,
        HorizontalAlignment = Enum.HorizontalAlignment.Left,
        VerticalAlignment = Enum.VerticalAlignment.Top,
        StartCorner = Enum.StartCorner.TopLeft,
        SortOrder = Enum.SortOrder.LayoutOrder,
    })

    quickPaletteElements.AddColorButton = Roact.createElement(Button, {
        LayoutOrder = 0,

        displayType = "image",
        image = Style.Images.AddButtonIcon,

        onActivated = function()
            self.props.addQuickPaletteColor(color)
        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.ColorPickerFrame),

        [Roact.Event.InputBegan] = function(_, input: InputObject)
            ColorEditorInputSignals.InputBegan.Fire(input)
        end,

        [Roact.Event.InputEnded] = function(_, input: InputObject)
            ColorEditorInputSignals.InputEnded.Fire(input)
        end,
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        EditorPages = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0, 0),
            Position = UDim2.new(0, 0, 0, 0),
            Size = UDim2.new(1, -(Style.Constants.LargeButtonHeight + 2 + Style.Constants.MajorElementPadding), 1, -118),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            [Roact.Change.AbsoluteSize] = function(obj)
                local absoluteSize = obj.AbsoluteSize

                self:setState({
                    editorWidth = absoluteSize.X
                })
            end
        }, editorPageElements),

        EditorPagePicker = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.new(1, 0, 0, 0),
            Size = UDim2.new(0, Style.Constants.LargeButtonHeight + 2, 0, ((Style.Constants.LargeButtonHeight + Style.Constants.MinorElementPadding) * #editorTabs) + 2),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            ClipsDescendants = true,
        }, {
            Pickers = Roact.createElement(ButtonBar, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, 0),
                Size = UDim2.new(1, 0, 1, 0),

                displayType = "image",
                vertical = true,
                selected = selectedEditor,
                buttons = editorTabButtons,

                onButtonActivated = self.props.setEditorPage
            })
        }),

        Tools = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, -38),
            Size = UDim2.new(1, 0, 0, indicatorContainerSize),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            Separator = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, -Style.Constants.MajorElementPadding),
                Size = UDim2.new(1, 0, 0, 1),
                BackgroundTransparency = 0,
                BorderSizePixel = 0,

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

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

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

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

                    BackgroundColor3 = color:toColor3(),
                }),

                OriginalColorIndicator = Roact.createElement("TextButton", {
                    AnchorPoint = Vector2.new(0.5, 1),
                    Position = UDim2.new(0.5, 0, 1, -1),
                    Size = UDim2.new(1, -2, 0.5, 1),
                    BackgroundTransparency = 0,
                    BorderSizePixel = 0,
                    AutoButtonColor = false,

                    Text = "",
                    TextTransparency = 1,

                    BackgroundColor3 = originalColor:toColor3(),

                    [Roact.Event.Activated] = function()
                        self.props.setColor(originalColor)
                    end
                })
            }),

            QuickPalette = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(1, 0.5),
                Position = UDim2.new(1, 0, 0.5, 0),
                Size = UDim2.new(1, -indicatorContainerSize - Style.Constants.MinorElementPadding, 1, 0),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
                ClipsDescendants = true,
            }, quickPaletteElements)
        }),

        Hex = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0, 1),
            Position = UDim2.new(0, 0, 1, 0),
            Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            Label = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0.5),
                Position = UDim2.new(0, 0, 0.5, 0),
                Size = UDim2.new(0, 6, 1, 0),

                Text = "#",
                TextXAlignment = Enum.TextXAlignment.Center,
                TextYAlignment = Enum.TextYAlignment.Center,
            }),

            Input = Roact.createElement(TextInput, {
                AnchorPoint = Vector2.new(1, 0.5),
                Position = UDim2.new(1, 0, 0.5, 0),
                Size = UDim2.new(1, -6 - Style.Constants.MinorElementPadding, 0, Style.Constants.StandardInputHeight),

                Text = string.upper(color:toHex()),
                TextXAlignment = Enum.TextXAlignment.Center,

                isTextAValidValue = function(text)
                    return (pcall(Color.fromHex, text))
                end,

                onSubmit = function(text)
                    self.props.setColor(Color.fromHex(text))
                end,

                selectTextOnFocus = true,
            }),
        }),

        RandomColorButton = Roact.createElement(Button, {
            AnchorPoint = Vector2.new(0, 1),
            Position = UDim2.new(0, Style.Constants.DialogButtonWidth + Style.Constants.MinorElementPadding, 1, 0),
            Size = UDim2.new(0, Style.Constants.StandardButtonHeight, 0, Style.Constants.StandardButtonHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            displayType = "image",
            image = Style.Images.RandomColorButtonIcon,

            onActivated = function()
                self.props.setColor(Color.random())
            end,
        }),

        Actions = 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 = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                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.fireFinished(false)
                end
            }),

            ConfirmButton = Roact.createElement(Button, {
                Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                LayoutOrder = 1,

                displayType = "text",
                text = uiTranslations["OK_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()
                    self.props.fireFinished(true)
                end
            }),
        })
    })
end

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,

        editorPage = state.sessionData.editorPage,
        color = state.colorEditor.color,
        quickPalette = state.colorEditor.quickPalette,
    }
end, function(dispatch)
    return {
        setColor = function(color)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = color
            })
        end,

        addQuickPaletteColor = function(color)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_AddQuickPaletteColor,
                color = color,
            })
        end,

        setEditorPage = function(page)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    editorPage = page
                }
            })
        end,
    }
end)(ColorEditor)


-- A grid of colors

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

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

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

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

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 ColorEditorInputSignals = require(Modules.EditorInputSignals).ColorEditor
local RepeatingCallback = require(Modules.RepeatingCallback)

---

local KEY_CODE_DELTAS = {
    [Enum.KeyCode.Up] = function(cellsPerRow) return -cellsPerRow end,
    [Enum.KeyCode.Down] = function(cellsPerRow) return cellsPerRow end,
    [Enum.KeyCode.Left] = function() return -1 end,
    [Enum.KeyCode.Right] = function() return 1 end,
}

--[[
    props
        AnchorPoint?
        Position?

        colors: array<Color3>
        title: string?
        selected: number?

        onColorSelected: (number) -> nil

    store props
        theme: StudioTheme
]]

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

ColorGrid.init = function(self)
    self.gridLength, self.updateGridLength = Roact.createBinding(0)
    self.cellsPerRow, self.updateCellsPerRow = Roact.createBinding(0)
    self.keyInputRepeaters = {}

    for keyCode, deltaCallback in pairs(KEY_CODE_DELTAS) do
        local repeater = RepeatingCallback.new(function()
            local colors = self.props.colors
            local selected = self.props.selected
            if (not selected) then return end

            local nextSelected = selected + deltaCallback(self.cellsPerRow:getValue())
            if (not colors[nextSelected]) then return end

            self.props.onColorSelected(nextSelected)
        end, 0.25, 0.1)

        self.keyInputRepeaters[keyCode] = repeater
    end
end

ColorGrid.didMount = function(self)
    self.keyDown = ColorEditorInputSignals.InputBegan.Event:subscribe(function(input: InputObject)
        if (input.UserInputType ~= Enum.UserInputType.Keyboard) then return end

        local inputRepeater = self.keyInputRepeaters[input.KeyCode]
        if (not inputRepeater) then return end

        for _, repeater in pairs(self.keyInputRepeaters) do
            repeater:stop()
        end

        inputRepeater:start()
    end)

    self.keyUp = ColorEditorInputSignals.InputEnded.Event:subscribe(function(input: InputObject)
        if (input.UserInputType ~= Enum.UserInputType.Keyboard) then return end

        local inputRepeater = self.keyInputRepeaters[input.KeyCode]
        if (not inputRepeater) then return end

        inputRepeater:stop()
    end)
end

ColorGrid.willUnmount = function(self)
    for _, repeater in pairs(self.keyInputRepeaters) do
        repeater:destroy()
    end

    self.keyInputRepeaters = nil
    self.keyDown:unsubscribe()
    self.keyUp:unsubscribe()
end

ColorGrid.render = function(self)
    local theme = self.props.theme
    local colors = self.props.colors

    local colorElements = {}

    for i = 1, #colors do
        local color = colors[i]

        colorElements[i] = Roact.createElement(Button, {
            LayoutOrder = i,

            displayType = "color",
            color = color,

            borderColor = if (self.props.selected == i) then
                theme:GetColor(
                    Enum.StudioStyleGuideColor.InputFieldBorder,
                    Enum.StudioStyleGuideModifier.Selected
                )
            else nil,

            onActivated = function()
                self.props.onColorSelected(i)
            end
        })
    end

    colorElements.UIGridLayout = Roact.createElement("UIGridLayout", {
        FillDirection = Enum.FillDirection.Horizontal,
        HorizontalAlignment = Enum.HorizontalAlignment.Left,
        SortOrder = Enum.SortOrder.LayoutOrder,
        VerticalAlignment = Enum.VerticalAlignment.Top,
        StartCorner = Enum.StartCorner.TopLeft,

        CellSize = Style.UDim2.StandardButtonSize,
        CellPadding = Style.UDim2.MinorElementPaddingSize,

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

        [Roact.Change.AbsoluteCellCount] = function(obj)
            self.updateCellsPerRow(obj.AbsoluteCellCount.X)
        end,
    })

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Position = self.props.Position,

        Size = self.gridLength:map(function(gridLength)
            return UDim2.new(1, 0, 0, gridLength + (
                if self.props.title then
                    (Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding)
                else 0
            ))
        end),

        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        GridLabel = if self.props.title then
            Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),
                Text = self.props.title,
            })
        else nil,

        Grid = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(1, 0, 1, if self.props.title then -(Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding) else 0),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, colorElements)
    })
end

---

--[[
    props

        AnchorPoint?
        Position?
        Size?

        colorLists: dictionary<string | number, array<Color3>>
        named: boolean?
        selected: number? (only works properly if there is only one color list)

        onColorSelected = (number, string | number) -> nil
]]

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

ColorGrids.init = function(self)
    self.listLength, self.updateListLength = Roact.createBinding(0)
end

ColorGrids.render = function(self)
    local listElements = {}

    for gridName, gridColors in pairs(self.props.colorLists) do
        listElements[gridName] = Roact.createElement(ColorGrid, {
            colors = gridColors,
            title = if self.props.named then gridName else nil,
            selected = self.props.selected,

            onColorSelected = function(i)
                self.props.onColorSelected(i, gridName)
            end,
        })
    end

    listElements.UIPadding = Roact.createElement(StandardUIPadding, {
        paddings = {Style.Constants.MinorElementPadding}
    })

    listElements.UIListLayout = Roact.createElement(StandardUIListLayout, {
        Padding = UDim.new(0, Style.Constants.MinorElementPadding),
        SortOrder = Enum.SortOrder.Name,

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

        preset = 1,
    })

    return Roact.createElement(StandardScrollingFrame, {
        AnchorPoint = self.props.AnchorPoint,
        Position = self.props.Position,
        Size = self.props.Size,

        CanvasSize = self.listLength:map(function(listLength)
            return UDim2.new(0, 0, 0, listLength + (Style.Constants.MinorElementPadding * 2))
        end)
    }, listElements)
end

---

ColorGrid = ConnectTheme(ColorGrid)

return ColorGrids


-- A page containing information in different formats about a color

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

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

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

local CommonComponents = Common.Components
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 Modules = root.Modules
local Enums = require(Modules.Enums)
local WebColorsPalette = require(Modules.BuiltInPalettes.WebColors)
local Util = require(Modules.Util)

---

local webColors = Util.typeColorPalette(WebColorsPalette, "Color").colors

local parseComponents = function(componentString, numComponents)
    local pattern = string.rep("(.+), ?", numComponents - 1) .. "(.+)"
    local components = {string.match(componentString, pattern)}

    for i = 1, #components do
        if (not tonumber(components[i])) then
            components[i] = nil
        end
    end

    return table.unpack(components)
end

local infoComponents = {
    {
        name = Translator.FormatByKey("RGB_ColorType"),

        getComponentString = function(color)
            local r, g, b = color:toRGB()

            return string.format("%d, %d, %d", r, g, b)
        end,

        getColor = function(componentString)
            local r, g, b = parseComponents(componentString, 3)
            r, g, b = tonumber(r), tonumber(g), tonumber(b)
            if (not (r and g and b)) then return end

            return Color.fromRGB(r, g, b)
        end
    },

    {
        name = Translator.FormatByKey("CMYK_ColorType"),

        getComponentString = function(color)
            local c, m, y, k = color:toCMYK()
            c, m, y, k = math.floor(c * 100), math.floor(m * 100), math.floor(y * 100), math.floor(k * 100)

            return string.format("%d, %d, %d, %d", c, m, y, k)
        end,

        getColor = function(componentString)
            local c, m, y, k = parseComponents(componentString, 4)
            c, m, y, k = tonumber(c), tonumber(m), tonumber(y), tonumber(k)
            if (not (c and m and y and k)) then return end

            return Color.fromCMYK(c / 100, m / 100, y / 100, k / 100)
        end
    },

    {
        name = Translator.FormatByKey("HSB_ColorType"),

        getComponentString = function(color)
            local h, s, b = color:toHSB()
            h = if (h ~= h) then 0 else h
            s, b = math.floor(s * 100), math.floor(b * 100)

            return string.format("%d, %d, %d", h, s, b)
        end,

        getColor = function(componentString)
            local h, s, b = parseComponents(componentString, 3)
            h, s, b = tonumber(h), tonumber(s), tonumber(b)
            if (not (h and s and b)) then return end

            return Color.fromHSB(h, s / 100, b / 100)
        end
    },

    {
        name = Translator.FormatByKey("HSL_ColorType"),

        getComponentString = function(color)
            local h, s, l = color:toHSL()
            h = if (h ~= h) then 0 else h
            s, l = math.floor(s * 100), math.floor(l * 100)

            return string.format("%d, %d, %d", h, s, l)
        end,

        getColor = function(componentString)
            local h, s, l = parseComponents(componentString, 3)
            h, s, l = tonumber(h), tonumber(s), tonumber(l)
            if (not (h and s and l)) then return end

            return Color.fromHSL(h, s / 100, l / 100)
        end
    },

    {
        name = Translator.FormatByKey("HWB_ColorType"),

        getComponentString = function(color)
            local h, w, b = color:toHWB()
            h = if (h ~= h) then 0 else h
            w, b = math.floor(w * 100), math.floor(b * 100)

            return string.format("%d, %d, %d", h, w, b)
        end,

        getColor = function(componentString)
            local h, w, b = parseComponents(componentString, 3)
            h, w, b = tonumber(h), tonumber(w), tonumber(b)
            if (not (h and w and b)) then return end

            return Color.fromHWB(h, w / 100, b / 100)
        end,
    },

    {
        name = Translator.FormatByKey("Lab_ColorType"),

        getComponentString = function(color)
            local l, a, b = color:toLab()
            l, a, b = l * 100, a * 100, b * 100

            return string.format("%.03f, %.03f, %.03f", l, a, b)
        end,

        getColor = function(componentString)
            local l, a, b = parseComponents(componentString, 3)
            l, a, b = tonumber(l), tonumber(a), tonumber(b)
            if (not (l and a and b)) then return end

            return Color.fromLab(l / 100, a / 100, b / 100)
        end
    },

    {
        name = Translator.FormatByKey("Luv_ColorType"),

        getComponentString = function(color)
            local l, u, v = color:toLuv()
            l, u, v = l * 100, u * 100, v * 100

            return string.format("%.03f, %.03f, %.03f", l, u, v)
        end,

        getColor = function(componentString)
            local l, u, v = parseComponents(componentString, 3)
            l, u, v = tonumber(l), tonumber(u), tonumber(v)
            if (not (l and u and v)) then return end

            return Color.fromLuv(l / 100, u / 100, v / 100)
        end
    },

    {
        name = Translator.FormatByKey("LChab_ColorType"),

        getComponentString = function(color)
            local l, c, h = color:toLChab()
            h = if (h ~= h) then 0 else h
            l, c = l * 100, c * 100

            return string.format("%.03f, %.03f, %.03f", l, c, h)
        end,

        getColor = function(componentString)
            local l, c, h = parseComponents(componentString, 3)
            l, c, h = tonumber(l), tonumber(c), tonumber(h)
            if (not (l and c and h)) then return end

            return Color.fromLChab(l / 100, c / 100, h)
        end
    },

    {
        name = Translator.FormatByKey("LChuv_ColorType"),

        getComponentString = function(color)
            local l, c, h = color:toLChuv()
            h = if (h ~= h) then 0 else h
            l, c = l * 100, c * 100

            return string.format("%.03f, %.03f, %.03f", l, c, h)
        end,

        getColor = function(componentString)
            local l, c, h = parseComponents(componentString, 3)
            l, c, h = tonumber(l), tonumber(c), tonumber(h)
            if (not (l and c and h)) then return end

            return Color.fromLChuv(l / 100, c / 100, h)
        end
    },

    {
        name = Translator.FormatByKey("xyY_ColorType"),

        getComponentString = function(color)
            local x, y, Y = color:to("xyY")
            x, y, Y = x * 100, y * 100, Y * 100

            return string.format("%.03f, %.03f, %.03f", x, y, Y)
        end,

        getColor = function(componentString)
            local x, y, Y = parseComponents(componentString, 3)
            x, y, Y = tonumber(x), tonumber(y), tonumber(Y)
            if (not (x and y and Y)) then return end

            return Color.from("xyY", x / 100, y / 100, Y / 100)
        end,
    },

    {
        name = Translator.FormatByKey("XYZ_ColorType"),

        getComponentString = function(color)
            local x, y, z = color:toXYZ()
            x, y, z = x * 100, y * 100, z * 100

            return string.format("%.03f, %.03f, %.03f", x, y, z)
        end,

        getColor = function(componentString)
            local x, y, z = parseComponents(componentString, 3)
            x, y, z = tonumber(x), tonumber(y), tonumber(z)
            if (not (x and y and z)) then return end

            return Color.fromXYZ(x / 100, y / 100, z / 100)
        end,
    },

    {
        name = Translator.FormatByKey("Hex_ColorLabel"),
        selectTextOnFocus = true,

        getComponentString = function(color)
            return string.upper(color:toHex())
        end,

        getColor = Color.fromHex
    },

    {
        name = Translator.FormatByKey("Web_ColorLabel"),

        getComponentString = function(color)
            for i = 1, #webColors do
                local webColor = webColors[i]

                if (webColor.color:toHex() == color:toHex()) then
                    return table.concat(webColor.keywords, ", ")
                end
            end

            return "n/a"
        end,

        getColor = function(componentString)
            componentString = string.lower(componentString)

            for i = 1, #webColors do
                local webColor = webColors[i]

                if (table.find(webColor.keywords, componentString)) then
                    return webColor.color
                end
            end

            return nil
        end
    }
}

---

--[[
    store props
        color: Color
        setColor: (Color) -> nil
]]

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

ColorInfo.init = function(self)
    self.listLength, self.updateListLength = Roact.createBinding(0)
end

ColorInfo.render = function(self)
    local color = self.props.color
    local colorInfoElements = {}

    for i = 1, #infoComponents do
        local component = infoComponents[i]
        local name = component.name

        colorInfoElements[name] = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardButtonHeight),
            LayoutOrder = i,
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            ComponentLabel = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0.5),
                Position = UDim2.new(0, 0, 0.5, 0),
                Size = UDim2.new(0, 40, 1, 0),
                Text = name,
            }),

            ComponentInput = Roact.createElement(TextInput, {
                AnchorPoint = Vector2.new(1, 0.5),
                Position = UDim2.new(1, 0, 0.5, 0),
                Size = UDim2.new(1, -(40 + Style.Constants.SpaciousElementPadding), 1, 0),
                Text = component.getComponentString(color),

                isTextAValidValue = function(text)
                    return (if component.getColor(text) then true else false)
                end,

                onSubmit = function(newText)
                    self.props.setColor(component.getColor(newText))
                end,

                selectTextOnFocus = component.selectTextOnFocus,
            })
        })
    end

    colorInfoElements.UIPadding = Roact.createElement(StandardUIPadding, {
        paddings = {0, 0, 0, Style.Constants.SpaciousElementPadding}
    })

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

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

        preset = 1,
    })

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

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

return RoactRodux.connect(function(state)
    return {
        color = state.colorEditor.color,
    }
end, function(dispatch)
    return {
        setColor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = newColor
            })
        end
    }
end)(ColorInfo)


-- A page for sorting colors based on their ΔE* to another color

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

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

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

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

local StandardComponents = CommonComponents.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIListLayout = require(StandardComponents.UIListLayout)

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

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

---

local uiTranslations = Translator.GenerateTranslationTable({
    "CompareTo_Label",
    "EmptySortAnchor_Indicator",
    "Sort_ButtonText",
    "Clear_ButtonText",
})

---

--[[
    store props
        currentColor: Color
        sortAnchor: Color
        colors: array<Color>

        setColor: (Color) -> nil
        setSortAnchor: (Color) -> nil
        setSortColors: (array<Color>) -> nil
]]

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

ColorSorter.render = function(self)
    local currentColor = self.props.currentColor
    local sortAnchor = self.props.sortAnchor
    local colors = self.props.colors

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

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

                Text = uiTranslations["CompareTo_Label"],
            }),

            Color = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 1),
                Position = UDim2.new(0, 0, 1, 0),
                Size = UDim2.new(0, 60, 0, Style.Constants.StandardButtonHeight),

                displayType = if sortAnchor then "color" else "text",
                text = if (not sortAnchor) then uiTranslations["EmptySortAnchor_Indicator"] else nil,
                color = if sortAnchor then sortAnchor:toColor3() else nil,

                onActivated = function()
                    self.props.setSortAnchor(currentColor)
                end,
            })
        }),

        ListActions = Roact.createElement("Frame",{
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            Size = UDim2.new(0, (
                Style.Constants.StandardButtonHeight +
                (40 * 2) +
                (Style.Constants.MinorElementPadding * 2)
            ), 0, Style.Constants.StandardButtonHeight),
        }, {
            UIListLayout = Roact.createElement(StandardUIListLayout, {
                Padding = UDim.new(0, Style.Constants.MinorElementPadding),

                preset = 2,
            }),

            ClearListButton = Roact.createElement(Button, {
                Size = UDim2.new(0, 40, 0, Style.Constants.StandardButtonHeight),
                LayoutOrder = 2,

                displayType = "text",
                text = uiTranslations["Clear_ButtonText"],
                disabled = (#colors <= 0),

                onActivated = function()
                    self.props.setSortColors({})
                end,
            }),

            SortListButton = Roact.createElement(Button, {
                Size = UDim2.new(0, 40, 0, Style.Constants.StandardButtonHeight),
                LayoutOrder = 1,

                displayType = "text",
                text = uiTranslations["Sort_ButtonText"],
                disabled = (#colors <= 1),

                onActivated = function()
                    local newColorList = Util.table.deepCopy(colors)

                    table.sort(newColorList, function(a, b)
                        return Color.fromColor3(a):deltaE(sortAnchor) < Color.fromColor3(b):deltaE(sortAnchor)
                    end)

                    self.props.setSortColors(newColorList)
                end,
            }),

            AddColorButton = Roact.createElement(Button, {
                Size = Style.UDim2.StandardButtonSize,
                LayoutOrder = 3,

                displayType = "image",
                image = Style.Images.AddButtonIcon,

                onActivated = function()
                    local newColorList = Util.table.deepCopy(colors)
                    table.insert(newColorList, currentColor:toColor3())

                    self.props.setSortColors(newColorList)
                end,
            })
        }),

        Colors = Roact.createElement(ColorGrids, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, -1),
            Size = UDim2.new(1, -2, 1, -(
                Style.Constants.StandardTextSize +
                Style.Constants.StandardButtonHeight +
                Style.Constants.MinorElementPadding +
                Style.Constants.SpaciousElementPadding -
                2
            )),

            colorLists = {colors},
            named = false,

            onColorSelected = function(i)
                self.props.setColor(Color.fromColor3(colors[i]))
            end,
        })
    })
end

---

return RoactRodux.connect(function(state)
    local sessionData = state.sessionData

    return {
        currentColor = state.colorEditor.color,
        sortAnchor = sessionData.colorSorterAnchor,
        colors = sessionData.colorSorterColors,
    }
end, function(dispatch)
    return {
        setColor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = newColor
            })
        end,

        setSortAnchor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,

                slice = {
                    colorSorterAnchor = newColor
                }
            })
        end,

        setSortColors = function(newColors)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,

                slice = {
                    colorSorterColors = newColors
                }
            })
        end,
    }
end)(ColorSorter)


--[[
    A Pages container for the Color Information, Color Sorter,
    Color Variations, and Gradient Pickers pages
]]

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

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

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

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

local Components = root.Components
local ColorInfo = require(Components.ColorInfo)
local ColorSorter = require(Components.ColorSorter)
local ColorVariations = require(Components.ColorVariations)
local GradientPickers = require(Components.GradientPickers)
local Pages = require(Components.Pages)

---

local uiTranslations = Translator.GenerateTranslationTable({
    "ColorInformation_Page",
    "ColorSorter_Page",
    "ColorVariations_Page",
    "GradientPickers_Page"
})

---

--[[
    store props
        lastToolPage: number
        updateToolPage: (number) -> nil
]]

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

ColorToolPages.render = function(self)
    return Roact.createElement(Pages, {
        selectedPage = self.props.lastToolPage,
        onPageChanged = self.props.updateToolPage,

        pageSections = {
            {
                name = "",

                items = {
                    {
                        name = uiTranslations["ColorInformation_Page"],
                        content = Roact.createElement(ColorInfo)
                    },

                    {
                        name = uiTranslations["ColorSorter_Page"],
                        content = Roact.createElement(ColorSorter)
                    },

                    {
                        name = uiTranslations["ColorVariations_Page"],
                        content = Roact.createElement(ColorVariations)
                    },

                    {
                        name = uiTranslations["GradientPickers_Page"],
                        content = Roact.createElement(GradientPickers)
                    },
                }
            }
        }
    })
end

---

return RoactRodux.connect(function(state)
    return {
        lastToolPage = state.sessionData.lastToolPage,
    }
end, function(dispatch)
    return {
        updateToolPage = function(section, page)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    lastToolPage = {section, page}
                }
            })
        end,
    }
end)(ColorToolPages)


-- A page showing variations of a color

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

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

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

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local StandardTextLabel = require(CommonComponents.StandardComponents.TextLabel)

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

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

---

local MIN_STEPS = 4
local MAX_STEPS = 14

local uiTranslations = Translator.GenerateTranslationTable({
    "Hues_Label",
    "Shades_Label",
    "Tints_Label",
    "Tones_Label",
    "ColorSteps_Label"
})

---

--[[
    store props
        color: Color
        variationSteps: number

        setColor: (Color) -> nil
        updateVariationSteps: (number) -> nil
]]

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

ColorVariations.render = function(self)
    local variationSteps = self.props.variationSteps
    local color = self.props.color

    local modifiedColors = {
        [uiTranslations["Hues_Label"]] = {},
        [uiTranslations["Shades_Label"]] = {},
        [uiTranslations["Tints_Label"]] = {},
        [uiTranslations["Tones_Label"]] = {},
    }

    for i = 1, variationSteps do
        modifiedColors[uiTranslations["Shades_Label"]][i] = color:darken(i/2):toColor3()
        modifiedColors[uiTranslations["Tints_Label"]][i] = color:brighten(i/2):toColor3()
        modifiedColors[uiTranslations["Tones_Label"]][i] = color:desaturate(i/2):toColor3()

        local h, s, b = color:toHSB()
        h = if (h ~= h) then 0 else h
        h = (h + (360 / (MAX_STEPS + 1) * i)) % 360

        modifiedColors[uiTranslations["Hues_Label"]][i] = Color.fromHSB(h, s, b):toColor3()
    end

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

            Size = UDim2.new(
                0, (Style.Constants.StandardButtonHeight * 2) + (Style.Constants.MinorElementPadding * 3) + 20 + 60,
                0, Style.Constants.StandardButtonHeight
            ),
        }, {
            IncrementButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(1, 0.5),
                Position = UDim2.new(1, 0, 0.5, 0),

                displayType = "image",
                image = Style.Images.AddButtonIcon,
                disabled = (variationSteps >= MAX_STEPS),

                onActivated = function()
                    self.props.updateVariationSteps(variationSteps + 1)
                end,
            }),

            DecrementButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0.5),
                Position = UDim2.new(0, 60 + Style.Constants.MinorElementPadding, 0.5, 0),

                displayType = "image",
                image = Style.Images.SubtractButtonIcon,
                disabled = (variationSteps <= MIN_STEPS),

                onActivated = function()
                    self.props.updateVariationSteps(variationSteps - 1)
                end,
            }),

            StepsLabel = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0, 0.5),
                Size = UDim2.new(0, 60, 1, 0),
                Position = UDim2.new(0, 0, 0.5, 0),
                Text = uiTranslations["ColorSteps_Label"],

            }),

            StepsCountLabel = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(1, 0.5),
                Size = UDim2.new(0, 20, 1, 0),
                Position = UDim2.new(1, -(Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding), 0.5, 0),

                Text = variationSteps,
                TextXAlignment = Enum.TextXAlignment.Center,
                TextYAlignment = Enum.TextYAlignment.Center,
            })
        }),

        Colors = Roact.createElement(ColorGrids, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(1, -2, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding + 2)),

            named = true,
            colorLists = modifiedColors,

            onColorSelected = function(index, schemeName)
                self.props.setColor(Color.fromColor3(modifiedColors[schemeName][index]))
            end
        })
    })
end

---

return RoactRodux.connect(function(state)
    return {
        color = state.colorEditor.color,
        variationSteps = state.sessionData.variationSteps,
    }
end, function(dispatch)
    return {
        setColor = function(color)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = color
            })
        end,

        updateVariationSteps = function(steps)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    variationSteps = steps
                }
            })
        end,
    }
end)(ColorVariations)