-- A page showing a palette of gradients

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 Gradient = require(CommonIncludes.Color).Gradient
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 StandardScrollingFrame = require(StandardComponents.ScrollingFrame)
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIListLayout = require(StandardComponents.UIListLayout)
local StandardUIPadding = require(StandardComponents.UIPadding)

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

---

local KEY_CODE_DELTAS = {
    [Enum.KeyCode.Up] = -1,
    [Enum.KeyCode.Down] = 1,
}

local searchPrompt = Translator.FormatByKey("Searchbar_Prompt")
local numBuiltInGradients = #BuiltInGradients

---

--[[
    props
        beforeSetGradient: () -> nil

    store props
        theme: StudioTheme

        keypoints: array<GradientKeypoint>
        colorSpace: string
        hueAdjustment: string
        precision: number

        gradients: array<array<GradientKeypoint>>

        addPaletteColor: (array<GradientKeypoint>) -> nil
        removePaletteColor: (number) -> nil
        changePaletteColorName: (number, string) -> nil
        changePaletteColorPosition: (number, number) -> nil

        setGradient: (array<GradientKeypoint>, string, string, number) -> nil
]]

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

GradientPalette.init = function(self)
    self.listLength, self.updateListLength = Roact.createBinding(0)
    self.keyInputRepeaters = {}

    self.getCombinedPalette = function()
        local combinedPalette = {}
        local userGradients = self.props.gradients

        for i = 1, numBuiltInGradients do
            table.insert(combinedPalette, BuiltInGradients[i])
        end

        for i = 1, #userGradients do
            table.insert(combinedPalette, userGradients[i])
        end

        return combinedPalette
    end

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

            local nextSelected
            local searchTerm = self.state.searchTerm

            if (searchTerm) then
                local gradients = self.combinePaletteItems()
                local paletteColorsSlice = {}
                local paletteColorsSliceToWholeMap = {} --> slice index to palette index

                for i = 1, #gradients do
                    local color = gradients[i]
                    local include = true

                    if (searchTerm) then
                        local start = string.find(string.lower(color.name), searchTerm)

                        include = if start then true else false
                    end

                    if (include) then
                        paletteColorsSlice[i] = color
                    end
                end

                for paletteColorIndex in pairs(paletteColorsSlice) do
                    table.insert(paletteColorsSliceToWholeMap, paletteColorIndex)
                end

                table.sort(paletteColorsSliceToWholeMap)

                local sliceIndex = table.find(paletteColorsSliceToWholeMap, selected)
                if (not sliceIndex) then return end

                nextSelected = paletteColorsSliceToWholeMap[sliceIndex + delta]
                if (not nextSelected) then return end
            else
                nextSelected = selected + delta
            end

            if ((nextSelected < 1) or (nextSelected > (numBuiltInGradients + #self.props.gradients))) then return end

            self:setState({
                selected = nextSelected
            })
        end, 0.25, 0.1)

        self.keyInputRepeaters[keyCode] = repeater
    end

    self:setState({
        searchDisplayText = "",
    })
end

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

    self.keyInputRepeaters = nil
end

GradientPalette.render = function(self)
    local theme = self.props.theme
    local selected = self.state.selected

    local gradients = self.getCombinedPalette()
    local listElements = {}

    local searchTerm = self.state.searchTerm
    local paletteColorsSlice = {}
    local paletteColorsSliceArray = {}
    local paletteColorsSliceToWholeMap = {} --> slice index to palette index
    local paletteColorsWholeToSliceMap = {} --> palette index to slice index

    for i = 1, #gradients do
        local color = gradients[i]
        local include = true

        if (searchTerm) then
            local start = string.find(string.lower(color.name), searchTerm)

            include = if start then true else false
        end

        if (include) then
            paletteColorsSlice[i] = color
        end
    end

    if (searchTerm) then
        for paletteColorIndex in pairs(paletteColorsSlice) do
            table.insert(paletteColorsSliceToWholeMap, paletteColorIndex)
        end

        table.sort(paletteColorsSliceToWholeMap)

        for sliceColorIndex, paletteColorIndex in pairs(paletteColorsSliceToWholeMap) do
            paletteColorsWholeToSliceMap[paletteColorIndex] = sliceColorIndex
        end

        for i = 1, #paletteColorsSliceToWholeMap do
            paletteColorsSliceArray[i] = paletteColorsSlice[paletteColorsSliceToWholeMap[i]]
        end
    end

    for i, gradient in pairs(if searchTerm then paletteColorsSliceArray else paletteColorsSlice) do
        local wholeIndex = if searchTerm then paletteColorsSliceToWholeMap[i] else i
        local isReadOnly = (wholeIndex <= numBuiltInGradients)
        local isSelected = (selected == wholeIndex)
        local realIndex = wholeIndex - numBuiltInGradients

        local listItemHeight

        if (isSelected) then
            listItemHeight = (Style.Constants.StandardButtonHeight * (if isReadOnly then 1 else 2)) +
                (Style.Constants.MinorElementPadding * (if isReadOnly then 2 else 3))
        else
            listItemHeight = Style.Constants.StandardButtonHeight + (Style.Constants.MinorElementPadding * 2)
        end

        table.insert(listElements, Roact.createElement("TextButton", {
            BackgroundTransparency = 0,
            BorderSizePixel = 0,
            AutoButtonColor = false,
            Size = UDim2.new(1, 0, 0, listItemHeight),

            Text = "",
            TextTransparency = 1,

            BackgroundColor3 = if isSelected then
                theme:GetColor(Enum.StudioStyleGuideColor.TableItem, Enum.StudioStyleGuideModifier.Selected)
            else theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame),

            [Roact.Event.MouseEnter] = function(obj)
                if (isSelected) then return end

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

            [Roact.Event.MouseLeave] = function(obj)
                if (isSelected) then return end

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

            [Roact.Event.Activated] = function()
                if (isSelected) then return end

                self:setState({
                    selected = wholeIndex
                })
            end
        }, {
            UIPadding = Roact.createElement(StandardUIPadding, {
                paddings = {Style.Constants.MinorElementPadding}
            }),

            ColorIndicator = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, 0),
                Size = UDim2.new(0, Style.Constants.ColorSequencePreviewWidth, 0, Style.Constants.StandardButtonHeight),

                displayType = "colorSequence",

                color = Gradient.new(
                    Util.generateFullKeypointList(
                        gradient.keypoints,
                        gradient.colorSpace or "RGB",
                        gradient.hueAdjustment or "Shorter",
                        gradient.precision or 0
                    )
                ):colorSequence(),

                onActivated = function()
                    self.props.beforeSetGradient()

                    self.props.setGradient(
                        Util.table.deepCopy(gradient.keypoints),
                        gradient.colorSpace or "RGB",
                        gradient.hueAdjustment or "Shorter",
                        gradient.precision or 0
                    )

                    self:setState({
                        selected = wholeIndex
                    })
                end,
            }),

            ColorName = if isSelected then
                Roact.createElement(TextInput, {
                    AnchorPoint = Vector2.new(0, 0),
                    Position = UDim2.new(0, Style.Constants.ColorSequencePreviewWidth + Style.Constants.MinorElementPadding, 0, 0),
                    Size = UDim2.new(1, -(Style.Constants.ColorSequencePreviewWidth + Style.Constants.MinorElementPadding), 0, Style.Constants.StandardButtonHeight),

                    Text = gradient.name,
                    TextXAlignment = Enum.TextXAlignment.Left,

                    onSubmit = function(newText)
                        self.props.changePaletteColorName(realIndex, newText)
                    end,

                    disabled = isReadOnly,
                })
            else
                Roact.createElement(StandardTextLabel, {
                    AnchorPoint = Vector2.new(0, 0),
                    Position = UDim2.new(0, Style.Constants.ColorSequencePreviewWidth + Style.Constants.SpaciousElementPadding + 1, 0, 0),
                    Size = UDim2.new(1, -(Style.Constants.ColorSequencePreviewWidth + Style.Constants.SpaciousElementPadding + 1), 1, 0),
                    Text = gradient.name,

                    TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, if isSelected then Enum.StudioStyleGuideModifier.Selected else nil),
                }),

            ColorActions = if (isSelected and (not isReadOnly)) then
                Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0, 1),
                    Position = UDim2.new(0, Style.Constants.ColorSequencePreviewWidth + Style.Constants.MinorElementPadding, 1, 0),
                    Size = UDim2.new(0, (Style.Constants.StandardButtonHeight * 3) + (Style.Constants.MinorElementPadding * 2), 0, Style.Constants.StandardButtonHeight),

                    BackgroundTransparency = 1,
                    BorderSizePixel = 0,
                }, {
                    UIListLayout = Roact.createElement(StandardUIListLayout, {
                        Padding = UDim.new(0, Style.Constants.MinorElementPadding),

                        preset = 2,
                    }),

                    RemoveColorButton = if (isSelected and (not isReadOnly)) then
                        Roact.createElement(Button, {
                            LayoutOrder = 1,

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

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

                                self.props.removePaletteColor(realIndex)
                            end,
                        })
                    else nil,

                    MoveUpButton = if (isSelected and (not isReadOnly)) then
                        Roact.createElement(Button, {
                            LayoutOrder = 2,

                            displayType = "image",
                            image = Style.Images.MoveUpButtonIcon,
                            disabled = (selected == (numBuiltInGradients + 1)),

                            onActivated = function()
                                self:setState({
                                    selected = selected - 1,
                                })

                                self.props.changePaletteColorPosition(realIndex, -1)
                            end,
                        })
                    else nil,

                    MoveDownButton = if (isSelected and (not isReadOnly)) then
                        Roact.createElement(Button, {
                            LayoutOrder = 3,

                            displayType = "image",
                            image = Style.Images.MoveDownButtonIcon,
                            disabled = (selected == #gradients),

                            onActivated = function()
                                self:setState({
                                    selected = selected + 1,
                                })

                                self.props.changePaletteColorPosition(realIndex, 1)
                            end,
                        })
                    else nil,
                })
            else nil,
        }))
    end

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

        preset = 1,
    })

    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),
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        SearchBar = Roact.createElement(TextInput, {
            AnchorPoint = Vector2.new(0, 0),
            Position = UDim2.new(0, 0, 0, 0),
            Size = UDim2.new(1, -(Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding), 0, Style.Constants.StandardInputHeight),

            PlaceholderText = searchPrompt,
            Text = self.state.searchDisplayText,

            onTextChanged = function(newText)
                local newSearchTerm = string.lower(Util.escapeText(newText))

                local selectedColor = gradients[selected]
                local resetSelected

                if (selectedColor) then
                    local start = string.find(string.lower(selectedColor.name), newSearchTerm)

                    resetSelected = if (not start) then true else false
                else
                    resetSelected = false
                end

                self:setState({
                    selected = if resetSelected then Roact.None else nil,

                    searchDisplayText = newText,
                    searchTerm = if (newSearchTerm ~= "") then newSearchTerm else Roact.None
                })
            end,

            canSubmitEmptyString = true,
        }),

        AddColorButton = Roact.createElement(Button, {
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.new(1, 0, 0, 0),

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

            onActivated = function()
                self:setState({
                    selected = #gradients + 1,

                    searchDisplayText = "",
                    searchTerm = Roact.None,
                })

                self.props.addPaletteColor()
            end,
        }),

        Sequences = Roact.createElement(StandardScrollingFrame, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(1, -2, 1, -(Style.Constants.StandardInputHeight + Style.Constants.MinorElementPadding + 2)),

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

            [Roact.Event.InputBegan] = function(_, input)
                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,

            [Roact.Event.InputEnded] = function(_, input)
                if (input.UserInputType ~= Enum.UserInputType.Keyboard) then return end

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

                inputRepeater:stop()
            end,

            useMainBackgroundColor = true,
        }, listElements)
    })
end

---

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

        keypoints = state.gradientEditor.keypoints,
        colorSpace = state.gradientEditor.colorSpace,
        hueAdjustment = state.gradientEditor.hueAdjustment,
        precision = state.gradientEditor.precision,

        gradients = if (state.gradientEditor.palettes[1]) then
            state.gradientEditor.palettes[1].gradients
        else {},
    }
end, function(dispatch)
    return {
        setGradient = function(keypoints, colorSpace, hueAdjustment, precision)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_SetGradient,

                keypoints = keypoints,
                colorSpace = colorSpace,
                hueAdjustment = hueAdjustment,
                precision = precision,
            })
        end,

        addPaletteColor = function()
            dispatch({
                type = Enums.StoreActionType.GradientEditor_AddCurrentGradientToPalette,
                paletteIndex = 1,
            })
        end,

        removePaletteColor = function(index)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_RemovePaletteGradient,
                paletteIndex = 1,
                gradientIndex = index,
            })
        end,

        changePaletteColorName = function(index, newColorName)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_ChangePaletteGradientName,
                paletteIndex = 1,
                gradientIndex = index,
                newName = newColorName
            })
        end,

        changePaletteColorPosition = function(index, positionOffset)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_ChangePaletteGradientPosition,
                paletteIndex = 1,
                gradientIndex = index,
                offset = positionOffset,
            })
        end
    }
end)(GradientPalette)


-- A page for picking colors from gradients

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

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

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

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

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

---

--[[
    props
        LayoutOrder?

        gradientName: string
        gradient: ColorSequence

        valuePicked: (number) -> nil

    store props
        theme: StudioTheme
]]

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

GradientPicker.init = function(self)
    self.center, self.updateCenter = Roact.createBinding(Vector2.new(0, 0))
    self.size, self.updateSize = Roact.createBinding(0)

    self.pickValue = function(cursorPosition)
        local distanceFromCenter = cursorPosition - self.center:getValue()
        local value = math.clamp((distanceFromCenter.X / self.size:getValue()) + 0.5, 0, 1)

        self.props.valuePicked(value)
    end

    self:setState({
        tracking = false,
    })
end

GradientPicker.didMount = function(self)
    self.mousePositionChanged = ColorEditorInputSignals.MousePositionChanged.Event:subscribe(function(cursorPosition: Vector2)
        if (not self.state.tracking) then return end

        self.pickValue(cursorPosition)
    end)
end

GradientPicker.willUnmount = function(self)
    self.mousePositionChanged:unsubscribe()
end

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

    return Roact.createElement("Frame", {
        Size = UDim2.new(1, 0, 0, (
            Style.Constants.StandardTextSize +
            Style.Constants.StandardInputHeight +
            Style.Constants.MinorElementPadding
        )),

        LayoutOrder = self.props.LayoutOrder,
        BackgroundTransparency = 1,
        BorderSizePixel = 0
    }, {
        NameLabel = 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.gradientName,
        }),

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

            BackgroundColor3 = theme:GetColor(
                Enum.StudioStyleGuideColor.InputFieldBorder,
                if self.state.tracking then Enum.StudioStyleGuideModifier.Selected else nil
            ),
        }, {
            UICorner = Roact.createElement(StandardUICorner),

            Picker = Roact.createElement("Frame", {
                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,
                BorderSizePixel = 1,
                ClipsDescendants = true,

                BackgroundColor3 = Color3.new(1, 1, 1),
                BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder),

                [Roact.Event.InputBegan] = function(_, input)
                    if (self.state.inputFocused) then return end
                    if (input.UserInputType ~= Enum.UserInputType.MouseButton1) then return end

                    local inputPosition = input.Position

                    self:setState({
                        tracking = true,
                    })

                    self.pickValue(Vector2.new(inputPosition.X, inputPosition.Y))
                end,

                [Roact.Event.InputEnded] = function(_, input)
                    if (input.UserInputType ~= Enum.UserInputType.MouseButton1) then return end
                    if (not self.state.tracking) then return end

                    self:setState({
                        tracking = false,
                    })
                end,

                [Roact.Change.AbsolutePosition] = function(obj)
                    local sliderPosition = obj.AbsolutePosition
                    local sliderSize = obj.AbsoluteSize

                    self.updateCenter(sliderPosition + (sliderSize / 2))
                    self.updateSize(sliderSize.X)
                end,

                [Roact.Change.AbsoluteSize] = function(obj)
                    local sliderPosition = obj.AbsolutePosition
                    local sliderSize = obj.AbsoluteSize

                    self.updateCenter(sliderPosition + (sliderSize / 2))
                    self.updateSize(sliderSize.X)
                end,
            }, {
                UICorner = Roact.createElement(StandardUICorner),

                UIGradient = Roact.createElement("UIGradient", {
                    Color = self.props.gradient
                }),
            }),
        }),
    })
end

---

--[[
    store props

        gradients
        setColor: (Color) -> nil
]]

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

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

GradientPickers.render = function(self)
    local userGradients = self.props.gradients
    local pickerElements = {}

    local addGradient = function(gradientInfo)
        local gradient = Gradient.new(gradientInfo.keypoints)

        table.insert(pickerElements, Roact.createElement(GradientPicker, {
            LayoutOrder = #pickerElements + 1,

            gradientName = gradientInfo.name,
            gradient = gradient:colorSequence(nil, gradientInfo.colorSpace, gradientInfo.hueAdjustment),

            valuePicked = function(value)
                self.props.setColor(gradient:color(value, gradientInfo.colorSpace, gradientInfo.hueAdjustment))
            end,
        }))
    end

    for i = 1, #BuiltInGradients do
        addGradient(BuiltInGradients[i])
    end

    for i = 1, #userGradients do
        addGradient(userGradients[i])
    end

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

    pickerElements.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),
    }, pickerElements)
end

---

GradientPicker = ConnectTheme(GradientPicker)

return RoactRodux.connect(function(state)
    return {
        gradients = if (state.gradientEditor.palettes[1]) then
            state.gradientEditor.palettes[1].gradients
        else {},
    }
end, function(dispatch)
    return {
        setColor = function(color)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = color
            })
        end,
    }
end)(GradientPickers)


-- RGB slider page for monochrome colors

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 StandardUIListLayout = require(CommonComponents.StandardComponents.UIListLayout)

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

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

---

local EDITOR_KEY = Enums.EditorKey.GreyscaleSlider

local sliderLabel = Translator.FormatByKey("GreyscaleSlider_Brightness_Label")

local valueToTextFactory = function(size)
    return function(value)
        return math.floor(value * size)
    end
end

local textToValueFactory = function(size)
    return function(text)
        local n = tonumber(text)
        if (not n) then return end
        if ((n < 0) or (n > size)) then return end

        return (n / size)
    end
end

local percentValueToText, percentTextToValue = valueToTextFactory(100), textToValueFactory(100)

---

--[[
    store props
        theme: StudioTheme
        color: Color
        editor: string

        setColor: (Color) -> nil
]]

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

GreyscaleSliderPage.init = function(self, initProps)
    local color = initProps.color
    local brightness = (color.R + color.G + color.B) / 3

    self:setState({
        brightness = brightness
    })
end

GreyscaleSliderPage.getDerivedStateFromProps = function(props, state)
    if (props.editor == EDITOR_KEY) then return end

    if (state.captureFocus) then
        return {
            captureFocus = Roact.None,
        }
    end

    local color = props.color
    local brightness = (color.R + color.G + color.B) / 3
    if (brightness == state.brightness) then return end

    return {
        brightness = brightness
    }
end

GreyscaleSliderPage.render = function(self)
    local theme = self.props.theme
    local editor = self.props.editor

    local brightness = self.state.brightness

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

            preset = 1,
        }),

        Slider = Roact.createElement(Slider, {
            value = brightness,
            sliderLabel = sliderLabel,
            sliderGradient = ColorSequence.new(Color3.new(0, 0, 0), Color3.new(1, 1, 1)),

            markerColor = Color.gray(brightness):bestContrastingColor(
                Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)),
                Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)):invert()
            ):toColor3(),

            keypoints = {
                {value = 0, color = Color3.new()},
                {value = 0.25, color = Color3.new(0.25, 0.25, 0.25)},
                {value = 0.5, color = Color3.new(0.5, 0.5, 0.5)},
                {value = 0.75, color = Color3.new(0.75, 0.75, 0.75)},
                {value = 1, color = Color3.new(1, 1, 1)}
            },

            valueToText = percentValueToText,
            textToValue = percentTextToValue,

            valueChanged = function(value)
                self:setState({
                    captureFocus = if (editor ~= EDITOR_KEY) then true else nil,
                    brightness = value
                })

                self.props.setColor(Color.gray(value))
            end
        })
    })
end

---

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
        color = state.colorEditor.color,
        editor = state.colorEditor.authoritativeEditor,
    }
end, function(dispatch)
    return {
        setColor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = newColor,
                editor = EDITOR_KEY
            })
        end
    }
end)(GreyscaleSliderPage)


-- Palette naming dialog page

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 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 StandardUIListLayout = require(StandardComponents.UIListLayout)

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

---

local uiTranslations = Translator.GenerateTranslationTable({
    "NamePalette_Prompt",
    "PaletteNameOK_Message",
    "DefaultPaletteName",

    "Cancel_ButtonText",
    "OK_ButtonText",
})

---

--[[
    props
        paletteIndex: number?
        onPromptClosed: (boolean) -> nil

    store props
        theme: StudioTheme
        palettes: array<Palette>

        updatePalettePage: (number, number) -> nil
        addPalette: (string) -> nil
        changePaletteName: (number, string) -> nil
]]

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

RenamePalette.init = function(self, initProps)
    local palettes = initProps.palettes
    local paletteIndex = initProps.paletteIndex

    self:setState({
        newPaletteName = if paletteIndex then
            palettes[paletteIndex].name
        else uiTranslations["DefaultPaletteName"]
    })
end

RenamePalette.render = function(self)
    local theme = self.props.theme
    local palettes = self.props.palettes

    local paletteIndex = self.props.paletteIndex
    local selectedPalette = palettes[paletteIndex]

    local newPaletteName = self.state.newPaletteName
    local actualNewPaletteName = Util.palette.getNewItemName(palettes, newPaletteName, paletteIndex)

    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 = 1,
        BorderSizePixel = 0,
    }, {
        PromptLabel = 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 = if selectedPalette then
                Translator.FormatByKey("RenamePalette_Prompt", { selectedPalette.name })
            else uiTranslations["NamePalette_Prompt"],
        }),

        NameInput = Roact.createElement(TextInput, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, Style.Constants.StandardTextSize + Style.Constants.MinorElementPadding),
            Size = UDim2.new(1, 0, 0, Style.Constants.LargeButtonHeight),

            Text = newPaletteName,
            TextSize = Style.Constants.LargeTextSize,

            onSubmit = function(newText)
                self:setState({
                    newPaletteName = newText
                })
            end,
        }),

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

            Text = if (newPaletteName ~= actualNewPaletteName) then
                Translator.FormatByKey("PaletteRename_Message", { actualNewPaletteName })
            else uiTranslations["PaletteNameOK_Message"],
        }),

        Buttons = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, (Style.Constants.StandardTextSize * 2) + Style.Constants.LargeButtonHeight + (Style.Constants.MinorElementPadding * 3)),
            Size = UDim2.new(1, 0, 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(false)
                end
            }),

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

                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()
                    if (selectedPalette) then
                        self.props.changePaletteName(paletteIndex, actualNewPaletteName)
                    else
                        self.props.addPalette(newPaletteName)
                        self.props.updatePalettePage(2, #palettes + 1)
                    end

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

---

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
        palettes = state.colorEditor.palettes,
    }
end, function(dispatch)
    return {
        updatePalettePage = function(section, page)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    lastPalettePage = {section, page}
                }
            })
        end,

        addPalette = function(name)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_AddPalette,
                name = name
            })
        end,

        changePaletteName = function(paletteIndex, newName)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_ChangePaletteName,
                paletteIndex = paletteIndex,
                newName = newName,
            })
        end
    }
end)(RenamePalette)


--[[
    A container for multiple pages which can be selected with a dropdown,
    and also contains a button for showing page options
]]

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 Components = root.Components
local Selection = require(Components.Selection)

---

--[[
    props
        selectedPage: {number, number}

        pageSections: array<{
            name: string,

            items: array<{
                name: string,
                content: Element,
                layoutOrder: number?,
            }>
        }>

        options: array<{
            name: string
            onActivated: () -> nil
        }>

        onPageChanged: (number, number) -> nil
]]

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

Pages.init = function(self)
    self:setState({
        dropdownExpanded = false,
    })
end

Pages.render = function(self)
    local pageSections = self.props.pageSections

    local selectedPageIndices = self.props.selectedPage
    local selectedPageSectionNum, selectedPageNum = selectedPageIndices[1], selectedPageIndices[2]

    local selectedPageSection = pageSections[selectedPageSectionNum]
    if (not selectedPageSection) then return end

    local selectedPage = selectedPageSection.items[selectedPageNum]
    if (not selectedPage) then return end

    return Roact.createFragment({
        Selection = Roact.createElement(Selection, {
            selectedItem = selectedPageIndices,
            itemSections = pageSections,
            options = self.props.options,

            onExpandedStateToggle = function(expanded)
                self:setState({
                    dropdownExpanded = expanded
                })
            end,

            onItemChanged = self.props.onPageChanged
        }),

        Page = if (not self.state.dropdownExpanded) then
            Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 1),
                Position = UDim2.new(0.5, 0, 1, 0),
                Size = UDim2.new(1, 0, 1, -(Style.Constants.LargeButtonHeight + Style.Constants.MajorElementPadding)),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
            }, {
                Content = selectedPage.content
            })
        else nil
    })
end

return Pages


-- A color palette

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 StandardUIListLayout = require(CommonComponents.StandardComponents.UIListLayout)
local TextInput = require(CommonComponents.TextInput)

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

local Components = root.Components
local ButtonBar = require(Components.ButtonBar)
local PaletteColorGrid = require(Components.PaletteColorGrid)
local PaletteColorList = require(Components.PaletteColorList)

---

local searchPrompt = Translator.FormatByKey("Searchbar_Prompt")

---

--[[
    props
        palette: Palette?
            OR
        paletteIndex: number?

        readOnly: boolean?

    store props
        theme: StudioTheme
        paletteLayout: number

        updatePaletteLayout: (number) -> nil
        setColor: (Color) -> nil
        addCurrentColorToPalette: (number) -> nil
        removePaletteColor: (number, number) -> nil
        changePaletteColorName: (number, number, string) -> nil
        changePaletteColorPosition: (number, number, number) -> nil
]]

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

Palette.init = function(self)
    self:setState({
        searchDisplayText = "",

        lastSelectTime = os.clock(),
    })
end

Palette.didUpdate = function(self, prevProps)
    if (self.props.paletteIndex ~= prevProps.paletteIndex) then
        self:setState({
            selectedColorIndex = Roact.None,
        })
    end
end

Palette.render = function(self)
    local palette = self.props.palette
    local paletteIndex = self.props.paletteIndex
    local paletteLayout = self.props.paletteLayout
    local isReadOnly = self.props.readOnly or (not paletteIndex)

    local searchTerm = self.state.searchTerm
    local selectedColorIndex = self.state.selectedColorIndex
    local selectedColor = palette.colors[selectedColorIndex]

    local paletteColorsSlice = {}
    local paletteColorsSliceArray = {}
    local paletteColorsSliceToWholeMap = {} --> slice index to palette index
    local paletteColorsWholeToSliceMap = {} --> palette index to slice index

    for i = 1, #palette.colors do
        local color = palette.colors[i]
        local include = true

        if (searchTerm) then
            local start = string.find(string.lower(color.name), searchTerm)

            include = if start then true else false
        end

        if (include) then
            paletteColorsSlice[i] = color
        end
    end

    if (searchTerm) then
        for paletteColorIndex in pairs(paletteColorsSlice) do
            table.insert(paletteColorsSliceToWholeMap, paletteColorIndex)
        end

        table.sort(paletteColorsSliceToWholeMap)

        for sliceColorIndex, paletteColorIndex in pairs(paletteColorsSliceToWholeMap) do
            paletteColorsWholeToSliceMap[paletteColorIndex] = sliceColorIndex
        end

        for i = 1, #paletteColorsSliceToWholeMap do
            paletteColorsSliceArray[i] = paletteColorsSlice[paletteColorsSliceToWholeMap[i]]
        end
    end

    return Roact.createFragment({
        Tools = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, 0),
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardButtonHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            UIListLayout = Roact.createElement(StandardUIListLayout, {
                Padding = UDim.new(0, Style.Constants.MinorElementPadding),

                preset = 2,
            }),

            SearchBar = Roact.createElement(TextInput, {
                Size = UDim2.new(
                    1, -(
                        Style.Constants.StandardButtonHeight * 2 +
                        2 +
                        Style.Constants.MinorElementPadding +
                        (if isReadOnly then 0 else (Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding))
                    ),
                    1, 0
                ),

                LayoutOrder = 0,
                PlaceholderText = searchPrompt,
                Text = self.state.searchDisplayText,

                onTextChanged = function(newText)
                    local newSearchTerm = string.lower(Util.escapeText(newText))
                    local resetSelected

                    if (selectedColor) then
                        local start = string.find(string.lower(selectedColor.name), newSearchTerm)

                        resetSelected = if (not start) then true else false
                    else
                        resetSelected = false
                    end

                    self:setState({
                        selectedColorIndex = if resetSelected then Roact.None else nil,

                        searchDisplayText = newText,
                        searchTerm = if (newSearchTerm ~= "") then newSearchTerm else Roact.None
                    })
                end,

                canSubmitEmptyString = true,
            }),

            LayoutPicker = Roact.createElement(ButtonBar, {
                Size = UDim2.new(0, (Style.Constants.StandardButtonHeight * 2) + 2, 1, 0),
                LayoutOrder = 1,

                displayType = "image",
                selected = if (paletteLayout == "grid") then 1 else 2,

                buttons = {
                    {
                        name = "grid",
                        image = Style.Images.GridViewButtonIcon,
                    },

                    {
                        name = "list",
                        image = Style.Images.ListViewButtonIcon,
                    }
                },

                onButtonActivated = function(i)
                    self.props.updatePaletteLayout(if (i == 1) then "grid" else "list")
                end,
            }),

            AddColorButton = if (not isReadOnly) then
                Roact.createElement(Button, {
                    LayoutOrder = 2,

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

                    onActivated = function()
                        self:setState({
                            selectedColorIndex = #palette.colors + 1,

                            searchDisplayText = "",
                            searchTerm = Roact.None,
                        })

                        self.props.addCurrentColorToPalette(paletteIndex)
                    end
                })
            else nil,
        }),

        Colors = Roact.createElement(if (paletteLayout == "grid") then PaletteColorGrid else PaletteColorList, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding + 1),
            Size = UDim2.new(1, -2, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding + 2)),

            readOnly = isReadOnly,
            colors = if searchTerm then paletteColorsSliceArray else paletteColorsSlice,
            selected = if searchTerm then  paletteColorsWholeToSliceMap[self.state.selectedColorIndex] else self.state.selectedColorIndex,

            onColorSelected = function(i)
                self:setState({
                    selectedColorIndex = if searchTerm then paletteColorsSliceToWholeMap[i] else i
                })
            end,

            onColorSet = function(i)
                i = if searchTerm then paletteColorsSliceToWholeMap[i] else i

                self.props.setColor(Color.fromColor3(palette.colors[i].color))
            end,

            onColorRemoved = function()
                self:setState({
                    selectedColorIndex = Roact.None,
                })

                self.props.removePaletteColor(paletteIndex, selectedColorIndex)
            end,

            onColorNameChanged = function(newName)
                self.props.changePaletteColorName(paletteIndex, selectedColorIndex, newName)
            end,

            onColorMovedUp = function()
                self:setState({
                    selectedColorIndex = selectedColorIndex - 1
                })

                self.props.changePaletteColorPosition(paletteIndex, selectedColorIndex, -1)
            end,

            onColorMovedDown = function()
                self:setState({
                    selectedColorIndex = selectedColorIndex + 1
                })

                self.props.changePaletteColorPosition(paletteIndex, selectedColorIndex, 1)
            end,
        })
    })
end

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
        paletteLayout = state.sessionData.paletteLayout
    }
end, function(dispatch)
    return {
        updatePaletteLayout = function(newLayout)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    paletteLayout = newLayout
                }
            })
        end,

        setColor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = newColor
            })
        end,

        addCurrentColorToPalette = function(paletteIndex)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_AddCurrentColorToPalette,
                paletteIndex = paletteIndex
            })
        end,

        removePaletteColor = function(paletteIndex, colorIndex)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_RemovePaletteColor,
                paletteIndex = paletteIndex,
                colorIndex = colorIndex
            })
        end,

        changePaletteColorName = function(paletteIndex, colorIndex, newName)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_ChangePaletteColorName,
                paletteIndex = paletteIndex,
                colorIndex = colorIndex,
                newName = newName
            })
        end,

        changePaletteColorPosition = function(paletteIndex, colorIndex, offset)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_ChangePaletteColorPosition,
                paletteIndex = paletteIndex,
                colorIndex = colorIndex,
                offset = offset,
            })
        end
    }
end)(Palette)


-- A color palette where the colors are shown in a grid

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 Roact = require(CommonIncludes.RoactRodux.Roact)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local StandardUIListLayout = require(CommonComponents.StandardComponents.UIListLayout)
local TextInput = require(CommonComponents.TextInput)

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

---

local uiTranslations = Translator.GenerateTranslationTable({
    "SelectAColor_Prompt",
    "SetColor_ButtonText",
})

---

--[[
    props
        AnchorPoint?
        Position?
        Size?

        colors: array<{
            name: string?,
            color: Color3
        }>

        readOnly: boolean?
        selected: number?

        onColorSelected: (number) -> nil

        onColorSet: () -> nil
        onColorRemoved: () -> nil
        onColorNameChanged: (string) -> nil
        onColorMovedUp: () -> nil
        onColorMovedDown: () -> nil
]]

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

PaletteColorGrid.render = function(self)
    local isReadOnly = self.props.readOnly
    local colors = self.props.colors
    local selected = self.props.selected

    local colorGridList = {}

    for i = 1, #colors do
        colorGridList[i] = colors[i].color
    end

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Position = self.props.Position,
        Size = self.props.Size,
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        Grid = Roact.createElement(ColorGrids, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, 0),
            Size = UDim2.new(1, 0, 1, -(Style.Constants.StandardButtonHeight * 2) - (Style.Constants.MinorElementPadding * 1) - Style.Constants.MajorElementPadding),

            named = false,
            colorLists = {colorGridList},
            selected = self.props.selected,

            onColorSelected = function(i)
                if (selected == i) then
                    self.props.onColorSet(i)
                end

                self.props.onColorSelected(i)
            end,
        }),

        ColorInfo = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(1, 0, 0, (Style.Constants.StandardButtonHeight * 2) + Style.Constants.MinorElementPadding),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            NameInput = Roact.createElement(TextInput, {
                AnchorPoint = Vector2.new(0, 1),
                Position = UDim2.new(0, 0, 1, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardButtonHeight),

                Text = if colors[selected] then colors[selected].name else "",
                PlaceholderText = uiTranslations["SelectAColor_Prompt"],

                disabled = (not selected) or isReadOnly,
                onSubmit = self.props.onColorNameChanged,
            }),

            SetColorButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, 0),
                Size = Style.UDim2.DialogButtonSize,

                displayType = "text",
                text = uiTranslations["SetColor_ButtonText"],
                disabled = (not selected),

                onActivated = function()
                    self.props.onColorSet(selected)
                end,
            }),

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

                    preset = 2,
                }),

                RemoveColorButton = Roact.createElement(Button, {
                    LayoutOrder = 0,

                    displayType = "image",
                    image = Style.Images.DeleteButtonIcon,
                    disabled = ((not selected) or isReadOnly),

                    onActivated = self.props.onColorRemoved,
                }),

                MoveUpButton = Roact.createElement(Button, {
                    LayoutOrder = 1,

                    displayType = "image",
                    image = Style.Images.MoveLeftButtonIcon,
                    disabled = ((not selected) or isReadOnly or (selected == 1)),

                    onActivated = self.props.onColorMovedUp,
                }),

                MoveDownButton = Roact.createElement(Button, {
                    LayoutOrder = 2,

                    displayType = "image",
                    image = Style.Images.MoveRightButtonIcon,
                    disabled = ((not selected) or isReadOnly or (selected == #colors)),

                    onActivated = self.props.onColorMovedDown,
                }),
            }),
        })
    })
end

return PaletteColorGrid


-- A color palette where the colors are shown in a list

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

---

local KEY_CODE_DELTAS = {
    [Enum.KeyCode.Up] = -1,
    [Enum.KeyCode.Down] = 1,
}

--[[
    props
        AnchorPoint?
        Position?
        Size?

        colors: array<{
            name: string?,
            color: Color3
        }>

        readOnly: boolean?
        selected: number?

        onColorSelected: (number) -> nil
        onColorSet: (number) -> nil
        onColorRemoved: () -> nil
        onColorNameChanged: (string) -> nil
        onColorMovedUp: () -> nil
        onColorMovedDown: () -> nil

    store props
        theme: StudioTheme
]]

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

PaletteColorList.init = function(self)
    self.listLength, self.updateListLength = Roact.createBinding(0)
    self.keyInputRepeaters = {}

    for keyCode, delta 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 + delta
            if (not colors[nextSelected]) then return end

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

        self.keyInputRepeaters[keyCode] = repeater
    end
end

PaletteColorList.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

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

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

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

    local isReadOnly = self.props.readOnly
    local selected = self.props.selected

    local listElements = {}

    for i = 1, #colors do
        local color = colors[i]
        local isSelected = (selected == i)

        local listItemHeight

        if (isSelected) then
            listItemHeight = (Style.Constants.StandardButtonHeight * 2) + (Style.Constants.MinorElementPadding * 3)
        else
            listItemHeight = (Style.Constants.StandardButtonHeight * 1) + (Style.Constants.MinorElementPadding * 2)
        end

        table.insert(listElements, Roact.createElement("TextButton", {
            BackgroundTransparency = 0,
            BorderSizePixel = 0,
            AutoButtonColor = false,
            Size = UDim2.new(1, 0, 0, listItemHeight),

            Text = "",
            TextTransparency = 1,

            BackgroundColor3 = if isSelected then
                theme:GetColor(Enum.StudioStyleGuideColor.TableItem, Enum.StudioStyleGuideModifier.Selected)
            else theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame),

            [Roact.Event.MouseEnter] = function(obj)
                if (isSelected) then return end

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

            [Roact.Event.MouseLeave] = function(obj)
                if (isSelected) then return end

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

            [Roact.Event.Activated] = function()
                if (isSelected) then return end

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

            ColorIndicator = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0),
                Position = UDim2.new(0, 0, 0, 0),

                displayType = "color",
                color = color.color,

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

            ColorName = if (isSelected) then
                Roact.createElement(TextInput, {
                    AnchorPoint = Vector2.new(0, 0),
                    Position = UDim2.new(0, Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding, 0, 0),
                    Size = UDim2.new(1, -(Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding), 0, Style.Constants.StandardButtonHeight),

                    Text = color.name,
                    TextXAlignment = Enum.TextXAlignment.Left,

                    disabled = isReadOnly,
                    onSubmit = self.props.onColorNameChanged,
                })
            else
                Roact.createElement(StandardTextLabel, {
                    AnchorPoint = Vector2.new(0, 0),
                    Position = UDim2.new(0, Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding + 1, 0, 0),
                    Size = UDim2.new(1, -(Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding + 1), 1, 0),
                    Text = color.name,

                    TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, if isSelected then Enum.StudioStyleGuideModifier.Selected else nil),
                }),

            ColorActions = if (isSelected) then
                Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0, 1),
                    Position = UDim2.new(0, Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding, 1, 0),
                    Size = UDim2.new(1, -(Style.Constants.StandardButtonHeight + Style.Constants.MinorElementPadding), 0, Style.Constants.StandardButtonHeight),

                    BackgroundTransparency = 1,
                    BorderSizePixel = 0,
                }, {
                    UIListLayout = Roact.createElement(StandardUIListLayout, {
                        Padding = UDim.new(0, Style.Constants.MinorElementPadding),
                        FillDirection = Enum.FillDirection.Horizontal,
                        HorizontalAlignment = Enum.HorizontalAlignment.Left,
                        VerticalAlignment = Enum.VerticalAlignment.Center,
                    }),

                    RemoveColorButton = if (not isReadOnly) then
                        Roact.createElement(Button, {
                            LayoutOrder = 1,

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

                            onActivated = self.props.onColorRemoved,
                        })
                    else nil,

                    MoveUpButton = if (not isReadOnly) then
                        Roact.createElement(Button, {
                            LayoutOrder = 2,

                            displayType = "image",
                            image = Style.Images.MoveUpButtonIcon,
                            disabled = (selected == 1),

                            onActivated = self.props.onColorMovedUp,
                        })
                    else nil,

                    MoveDownButton = if (not isReadOnly) then
                        Roact.createElement(Button, {
                            LayoutOrder = 3,

                            displayType = "image",
                            image = Style.Images.MoveDownButtonIcon,
                            disabled = (selected == #colors),

                            onActivated = self.props.onColorMovedDown,
                        })
                    else nil,

                    SetColorButton = Roact.createElement(Button, {
                        LayoutOrder = 4,
                        Size = Style.UDim2.DialogButtonSize,

                        displayType = "text",
                        text = "Set Color",

                        onActivated = function()
                            self.props.onColorSet(i)
                        end,
                    })
                })
            else nil,
        }))
    end

    listElements.UIListLayout = Roact.createElement(StandardUIListLayout, {
        [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,
        BackgroundTransparency = 0,
        BorderSizePixel = 1,

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

---

return ConnectTheme(PaletteColorList)


-- A Pages container for all the built-in and user palettes
local root = script.Parent.Parent
local Common = root.Common

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

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

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

local BuiltInPalettes = Modules.BuiltInPalettes
local BrickColors = require(BuiltInPalettes.BrickColors)
local WebColors = require(BuiltInPalettes.WebColors)

local Components = root.Components
local ColorBrewerPalettes = require(Components.ColorBrewerPalettes)
local ExportPalette = require(Components.ExportPalette)
local ImportPalette = require(Components.ImportPalette)
local NamePalette = require(Components.NamePalette)
local Pages = require(Components.Pages)
local Palette = require(Components.Palette)
local PicularPalette = require(Components.PicularPalette)
local RemovePalette = require(Components.RemovePalette)

---

local uiTranslations = Translator.GenerateTranslationTable({
    "BuiltInPalette_Category",
    "UserPalette_Category",
    "CreatePalette_ButtonText",
    "ImportPalette_ButtonText",
    "ExportPalette_ButtonText",
    "DuplicatePalette_ButtonText",
    "RenamePalette_ButtonText",
    "DeletePalette_ButtonText",
})

local builtInPaletteElements = {
    {
        name = "BrickColors",

        content = Roact.createElement(Palette, {
            palette = Util.typeColorPalette(BrickColors, "Color3"),
            paletteIndex = -1,
            readOnly = true
        })
    },

    {
        name = "ColorBrewer",
        content = Roact.createElement(ColorBrewerPalettes)
    },

    {
        name = "Picular",
        content = Roact.createElement(PicularPalette),
    },

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

        content = Roact.createElement(Palette, {
            palette = Util.typeColorPalette(WebColors, "Color3"),
            paletteIndex = -2,
            readOnly = true
        })
    }
}

---

--[[
    store props
        theme: StudioTheme
        palettes: array<Palette>
        lastPalettePage: number

        updatePalettePage: (number, number) -> nil
        addPalette: (string) -> nil
        duplicatePalette: (number) -> nil
        removePalette: (number) -> nil
]]

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

PalettePages.init = function(self)
    self:setState({
        displayPage = "palettes",

        leftShiftDown = false,
        rightShiftDown = false,
    })
end

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

        if (input.KeyCode == Enum.KeyCode.LeftShift) then
            self:setState({
                leftShiftDown = true,
            })
        elseif (input.KeyCode ~= Enum.KeyCode.RightShift) then
            self:setState({
                rightShiftDown = true,
            })
        end
    end)

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

        if (input.KeyCode == Enum.KeyCode.LeftShift) then
            self:setState({
                leftShiftDown = false,
            })
        elseif (input.KeyCode ~= Enum.KeyCode.RightShift) then
            self:setState({
                rightShiftDown = false,
            })
        end
    end)
end

PalettePages.willUnmount = function(self)
    self.keyDown:unsubscribe()
    self.keyUp:unsubscribe()
end

PalettePages.render = function(self)
    local palettes = self.props.palettes
    local numBuiltInPalettes = #builtInPaletteElements
    local upstreamAvailable: boolean = self.props.upstreamAvailable

    local selectedPage = self.props.lastPalettePage
    local selectedPageSection, selectedPageNum = selectedPage[1], selectedPage[2]

    local displayPage = self.state.displayPage
    local displayPageElement
    local builtInPalettePages = {}
    local userPalettePages = {}

    for i = 1, numBuiltInPalettes do
        table.insert(builtInPalettePages, builtInPaletteElements[i])
    end

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

        table.insert(userPalettePages, {
            name = palette.name,

            content = Roact.createElement(Palette, {
                palette = palette,
                paletteIndex = i,
                readOnly = not upstreamAvailable,
            })
        })
    end

    -- decide which component to display
    if (displayPage == "namePalette") then
        displayPageElement = Roact.createElement(NamePalette, {
            onPromptClosed = function()
                self:setState({
                    displayPage = "palettes"
                })
            end,
        })
    elseif (displayPage == "renamePalette") then
        displayPageElement = Roact.createElement(NamePalette, {
            paletteIndex = self.state.paletteIndex,

            onPromptClosed = function()
                self:setState({
                    displayPage = "palettes",
                    paletteIndex = Roact.None,
                })
            end,
        })
    elseif (displayPage == "removePalette") then
        displayPageElement = Roact.createElement(RemovePalette, {
            paletteIndex = self.state.paletteIndex,

            onPromptClosed = function()
                self:setState({
                    displayPage = "palettes",
                    paletteIndex = Roact.None,
                })
            end
        })
    elseif (displayPage == "importPalette") then
        displayPageElement = Roact.createElement(ImportPalette, {
            onPromptClosed = function(success)
                if (success) then
                    self.props.updatePalettePage(2, #palettes + 1)
                end

                self:setState({
                    displayPage = "palettes"
                })
            end
        })
    elseif (displayPage == "exportPalette") then
        displayPageElement = Roact.createElement(ExportPalette, {
            paletteIndex = self.state.paletteIndex,

            onPromptClosed = function()
                self:setState({
                    displayPage = "palettes",
                    paletteIndex = Roact.None,
                })
            end
        })
    elseif (displayPage == "palettes") then
        local pageOptions = {}

        if (upstreamAvailable) then
            pageOptions = Cryo.List.join(pageOptions, {
                {
                    name = uiTranslations["CreatePalette_ButtonText"],

                    onActivated = function()
                        if (self.props.askNameBeforePaletteCreation) then
                            self:setState({
                                displayPage = "namePalette",
                                newPaletteName = Util.palette.getNewItemName(palettes, "New Palette")
                            })
                        else
                            self.props.addPalette()
                            self.props.updatePalettePage(2, #palettes + 1)
                        end
                    end
                },

                {
                    name = uiTranslations["ImportPalette_ButtonText"],

                    onActivated = function()
                        self:setState({
                            displayPage = "importPalette"
                        })
                    end
                },
            })

            if (selectedPageSection == 2) then
                pageOptions = Cryo.List.join(pageOptions, {
                    {
                        name = uiTranslations["ExportPalette_ButtonText"],

                        onActivated = function()
                            self:setState({
                                displayPage = "exportPalette",
                                paletteIndex = selectedPageNum,
                            })
                        end
                    },

                    {
                        name = uiTranslations["DuplicatePalette_ButtonText"],

                        onActivated = function()
                            self.props.duplicatePalette(selectedPageNum)
                            self.props.updatePalettePage(2, #palettes + 1)
                        end
                    },

                    {
                        name = uiTranslations["RenamePalette_ButtonText"],

                        onActivated = function()
                            self:setState({
                                displayPage = "renamePalette",
                                paletteIndex = selectedPageNum,
                            })
                        end
                    },

                    {
                        name = uiTranslations["DeletePalette_ButtonText"],

                        onActivated = function()
                            if (self.state.leftShiftDown or self.state.rightShiftDown) then
                                self.props.removePalette(selectedPageNum)
                                self.props.updatePalettePage(1, 1)
                            else
                                self:setState({
                                    displayPage = "removePalette",
                                    paletteIndex = selectedPageNum
                                })
                            end
                        end
                    },
                })
            end
        end

        displayPageElement = Roact.createElement(Pages, {
            selectedPage = selectedPage,
            showAllSections = true,
            onPageChanged = self.props.updatePalettePage,

            pageSections = {
                {
                    name = uiTranslations["BuiltInPalette_Category"],
                    items = builtInPalettePages,
                },

                {
                    name = uiTranslations["UserPalette_Category"],
                    items = userPalettePages,
                }
            },

            options = if (#pageOptions > 1) then pageOptions else nil,
        })
    end

    return displayPageElement
end

---

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

        palettes = state.colorEditor.palettes,
        upstreamAvailable = state.upstreamAvailable,
        lastPalettePage = state.sessionData.lastPalettePage,
        askNameBeforePaletteCreation = state.userData[CommonEnums.ColorPaneUserDataKey.AskNameBeforePaletteCreation]
    }
end, function(dispatch)
    return {
        updatePalettePage = function(section, page)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    lastPalettePage = {section, page}
                }
            })
        end,

        addPalette = function(name)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_AddPalette,
                name = name
            })
        end,

        duplicatePalette = function(paletteIndex)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_DuplicatePalette,
                paletteIndex = paletteIndex,
            })
        end,

        removePalette = function(paletteIndex)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_RemovePalette,
                paletteIndex = paletteIndex
            })
        end,
    }
end)(PalettePages)


-- A dialog for removing a palette

local TextService = game:GetService("TextService")

---

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 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 StandardUIPadding = require(StandardComponents.UIPadding)

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

---

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

---

--[[
    props
        paletteIndex: number,
        onPromptClosed: (boolean) -> nil

    store props
        theme: StudioTheme
        palettes: array<Palette>

        updatePalettePage: (number, number) -> nil
        removePalette: (number) -> nil
]]

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

RemovePalette.init = function(self)
    self.promptWidth, self.updatePromptWidth = Roact.createBinding(0)
end

RemovePalette.render = function(self)
    local theme = self.props.theme
    local palettes = self.props.palettes

    local paletteIndex = self.props.paletteIndex
    local promptText = Translator.FormatByKey("DeletePalette_Prompt", { palettes[paletteIndex].name })

    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 = 1,
        BorderSizePixel = 0,

        [Roact.Change.AbsoluteSize] = function(obj)
            self.updatePromptWidth(obj.AbsoluteSize.X)
        end
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {0, Style.Constants.MajorElementPadding}
        }),

        WarningText = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 0.5, -4),

            Size = self.promptWidth:map(function(promptWidth)
                local promptTextHeight = TextService:GetTextSize(
                    promptText,
                    Style.Constants.LargeTextSize,
                    Style.Fonts.Standard,
                    Vector2.new(promptWidth - (Style.Constants.MajorElementPadding * 2), math.huge)
                ).Y

                return UDim2.new(1, 0, 0, promptTextHeight)
            end),

            Text = promptText,
            TextSize = Style.Constants.LargeTextSize,
            TextXAlignment = Enum.TextXAlignment.Center,
            TextYAlignment = Enum.TextYAlignment.Bottom,
            TextWrapped = true,
        }),

        Buttons = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0.5, 4),
            Size = Style.UDim2.ButtonBarSize,
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            CancelButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(1, 0.5),
                Position = UDim2.new(0.5, -4, 0.5, 0),
                Size = Style.UDim2.DialogButtonSize,

                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(false)
                end
            }),

            ConfirmButton = Roact.createElement(Button, {
                AnchorPoint = Vector2.new(0, 0.5),
                Position = UDim2.new(0.5, 4, 0.5, 0),
                Size = Style.UDim2.DialogButtonSize,
                BackgroundTransparency = 0,

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

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

                onActivated = function()
                    self.props.updatePalettePage(1, 1)
                    self.props.removePalette(paletteIndex)
                    self.props.onPromptClosed(true)
                end
            }),
        }),
    })
end

---

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
        palettes = state.colorEditor.palettes,
    }
end, function(dispatch)
    return {
        updatePalettePage = function(section, page)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    lastPalettePage = {section, page}
                }
            })
        end,

        removePalette = function(paletteIndex)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_RemovePalette,
                paletteIndex = paletteIndex,
            })
        end,
    }
end)(RemovePalette)


-- A selection button and list

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 StandardUICorner = require(StandardComponents.UICorner)
local StandardUIPadding = require(StandardComponents.UIPadding)

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

---

--[[
    props
        selectedItem: {number, number}

        itemSections: array<{
            name: string,

            items: array<{
                name: string,
                layoutOrder: number?
            }>
        }>

        options: array<{
            name: string
            onActivated: () -> nil
        }>?

        onExpandedStateToggle: (boolean) -> nil
        onItemChanged: (number, number) -> nil

    store props
        theme: StudioTheme
]]

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

Selection.init = function(self)
    self:setState({
        dropdownOpen = false,
        optionsOpen = false,
    })

    self.updateSelection = function(delta)
        return function()
            if (self.state.dropdownOpen) then return end

            local itemSections = self.props.itemSections
            local selectedItemIndices = self.props.selectedItem
            if (not (itemSections and selectedItemIndices)) then return end

            local selectedItemSectionNum, selectedItemNum = selectedItemIndices[1], selectedItemIndices[2]

            local nextSection
            local nextItem = selectedItemNum + delta

            if (nextItem <= 0) then
                nextSection = selectedItemSectionNum - 1
                nextSection = if (nextSection == 0) then #itemSections else nextSection

                nextItem = #itemSections[nextSection].items
            elseif (nextItem > #itemSections[selectedItemSectionNum].items) then
                nextSection = selectedItemSectionNum + 1
                nextSection = if (nextSection > #itemSections) then 1 else nextSection

                nextItem = 1
            else
                nextSection = selectedItemSectionNum
            end

            if (nextSection ~= selectedItemSectionNum) then
                -- This does not account for the case where every section is empty,
                -- which should not happen in production.

                local section = itemSections[nextSection]

                while (#section.items <= 0) do
                    nextSection = nextSection + delta

                    if (nextSection <= 0) then
                        nextSection = #itemSections
                    elseif (nextSection > #itemSections) then
                        nextSection = 1
                    end

                    section = itemSections[nextSection]
                end

                if (delta == -1) then
                    nextItem = #section.items
                elseif (delta == 1) then
                    nextItem = 1
                end
            end

            self.props.onItemChanged(nextSection, nextItem)
        end
    end
end

Selection.didUpdate = function(self, _, prevState)
    local dropdownOpen = self.state.dropdownOpen
    local optionsOpen = self.state.optionsOpen

    if ((dropdownOpen ~= prevState.dropdownOpen) or (optionsOpen ~= prevState.optionsOpen)) then
        self.props.onExpandedStateToggle(dropdownOpen or optionsOpen)
    elseif (optionsOpen and (not self.props.options)) then
        self:setState({
            optionsOpen = false,
        })
    end
end

Selection.render = function(self)
    local theme = self.props.theme
    local itemSections = self.props.itemSections

    local selectedItemIndices = self.props.selectedItem
    local selectedItemSectionNum, selectedItemNum = selectedItemIndices[1], selectedItemIndices[2]

    local selectedItemSection = itemSections[selectedItemSectionNum]
    if (not selectedItemSection) then return end

    local selectedItem = selectedItemSection.items[selectedItemNum]
    if (not selectedItem) then return end

    local list
    local numItemsListItems = 0

    for i = 1, #itemSections do
        local items = itemSections[i].items

        for j = 1, #items do
            if (items[j] ~= selectedItem) then
                numItemsListItems = numItemsListItems + 1
            end
        end
    end

    if (self.state.dropdownOpen) then
        local itemListSections = {}

        for i = 1, #itemSections do
            local section = itemSections[i]
            local sectionItems = section.items
            local newIndex = #itemListSections + 1

            itemListSections[newIndex] = {
                name = section.name,
                items = {}
            }

            local itemListSection = itemListSections[newIndex]

            for j = 1, #sectionItems do
                local item = sectionItems[j]

                if (item ~= selectedItem) then
                    table.insert(itemListSection.items, {
                        name = item.name,
                        layoutOrder = item.layoutOrder,

                        onActivated = function()
                            self:setState({
                                dropdownOpen = false,
                            })

                            if (self.props.onItemChanged) then
                                self.props.onItemChanged(i, j)
                            end
                        end,
                    })
                end
            end
        end

        list = Roact.createElement(SimpleList, {
            AnchorPoint = Vector2.new(0, 0),
            Position = UDim2.new(0, 1, 1, 5),
            TextSize = Style.Constants.LargeTextSize,

            Size = UDim2.new(
                1, if self.props.options then (-Style.Constants.LargeButtonHeight - 6) else -2,
                0, Style.Constants.LargeButtonHeight * 5
            ),

            itemHeight = Style.Constants.LargeButtonHeight,
            itemPadding = Style.Constants.SpaciousElementPadding,
            sections = itemListSections,
        })
    elseif (self.state.optionsOpen and self.props.options) then
        local options = self.props.options
        local optionListItems = {}
        local numOptionsListItems = #options

        for i = 1, #options do
            local option = options[i]

            table.insert(optionListItems, {
                name = option.name,

                onActivated = function()
                    self:setState({
                        optionsOpen = false,
                        dropdownOpen = false,
                    })

                    option.onActivated()
                end,

                [Roact.Children] = {
                    UIPadding = Roact.createElement(StandardUIPadding, {
                        paddings = {0, 0, Style.Constants.SpaciousElementPadding, 0}
                    }),
                }
            })
        end

        list = Roact.createElement(SimpleList, {
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.new(1, -1, 1, 5),
            TextSize = Style.Constants.LargeTextSize,

            Size = UDim2.new(
                1, if self.props.options then (-Style.Constants.LargeButtonHeight - 6) else -2,
                0, Style.Constants.LargeButtonHeight * (if (numOptionsListItems <= 8) then numOptionsListItems else 8)
            ),

            itemHeight = Style.Constants.LargeButtonHeight,

            sections = {
                {
                    name = "",
                    items = optionListItems
                }
            },
        })
    end

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint or Vector2.new(0.5, 0),
        Position = self.props.Position or UDim2.new(0.5, 0, 0, 0),
        Size = UDim2.new(1, 0, 0, Style.Constants.LargeButtonHeight),
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        SelectionButton = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0, 0.5),
            Position = UDim2.new(0, 0, 0.5, 0),
            Size = UDim2.new(1, if self.props.options then (-Style.Constants.LargeButtonHeight - 4) else 0, 1, 0),
            BackgroundTransparency = 0,
            BorderSizePixel = 0,
            ClipsDescendants = true,

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

            Display = Roact.createElement("TextButton", {
                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,
                BorderSizePixel = 0,
                AutoButtonColor = false,

                Font = Style.Fonts.Standard,
                TextSize = Style.Constants.LargeTextSize,
                TextXAlignment = Enum.TextXAlignment.Left,
                TextYAlignment = Enum.TextYAlignment.Center,
                Text = selectedItem.name,

                BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Button),
                TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ButtonText),

                [Roact.Event.MouseWheelForward] = self.updateSelection(-1),
                [Roact.Event.MouseWheelBackward] = self.updateSelection(1),

                [Roact.Event.MouseEnter] = function(obj)
                    if (numItemsListItems < 1) then return end

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

                [Roact.Event.MouseLeave] = function(obj)
                    if (numItemsListItems < 1) then return end

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

                [Roact.Event.Activated] = function()
                    if (numItemsListItems < 1) then return end

                    self:setState(function(oldState)
                        return {
                            dropdownOpen = (not oldState.dropdownOpen),
                            optionsOpen = false,
                        }
                    end)
                end,
            }, {
                UICorner = Roact.createElement(StandardUICorner),

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

                Icon = if (numItemsListItems >= 1) then
                    Roact.createElement("ImageLabel", {
                        AnchorPoint = Vector2.new(1, 0.5),
                        Position = UDim2.new(1, -2, 0.5, 0),
                        Size = Style.UDim2.StandardButtonSize,
                        BackgroundTransparency = 1,
                        BorderSizePixel = 0,

                        Image = if self.state.dropdownOpen then Style.Images.CloseDropdownButtonIcon else Style.Images.OpenDropdownButtonIcon,
                        ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
                    })
                else nil
            }),
        }),

        OptionsButton = if self.props.options then
            Roact.createElement(Button, {
                AnchorPoint = Vector2.new(1, 0.5),
                Position = UDim2.new(1, 0, 0.5, 0),
                Size = UDim2.new(0, Style.Constants.LargeButtonHeight, 0, Style.Constants.LargeButtonHeight),

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

                onActivated = function()
                    self:setState(function(oldState)
                        return {
                            optionsOpen = (not oldState.optionsOpen),
                            dropdownOpen = false,
                        }
                    end)
                end
            })
        else nil,

        List = list,
    })
end

---

return ConnectTheme(Selection)