/**
 * Defines the theme for the app.
 */

package theorycrafter.ui

import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.unit.*
import compose.input.KeyShortcut
import compose.utils.EasyTooltipPlacement
import compose.utils.TooltipDefaults
import compose.utils.TooltipHostProperties
import compose.widgets.AutoSuggestMenuItemProvider
import compose.widgets.DropdownMenuItem
import compose.widgets.LocalAutoSuggestMenuItemProvider
import eve.data.DamageType
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.hostOs
import theorycrafter.ColorThemeStyle
import theorycrafter.TheorycrafterContext
import theorycrafter.ui.widgets.*
import theorycrafter.utils.ProvideSystemTheme
import theorycrafter.utils.darker
import theorycrafter.utils.isSystemThemeDark
import theorycrafter.utils.lighter
import compose.utils.tooltip as easyTooltip


/**
 * The default width of the main window, in dp.
 */
const val DefaultMainWindowWidth: Float = 1480f


/**
 * The default height of the main window, in dp.
 */
const val DefaultMainWindowHeight = DefaultMainWindowWidth/1.618


/**
 * The UI scale factor.
 */
val ApplicationUiScale: Float
    get() = TheorycrafterContext.settings.uiScaleFactorPct/100f


/**
 * Defines the app's themes.
 */
object TheorycrafterTheme {


    /**
     * The text styles.
     */
    val textStyles = TextStyles()


    /**
     * The spacings between elements, paddings etc.
     */
    val spacing = Spacing()


    /**
     * Sizes of various elements.
     */
    val sizes = Sizes()


    /**
     * Colors of various elements.
     */
    val colors = Colors()


    /**
     * The style of icons we use.
     */
    val iconStyle = Icons.Outlined


    /**
     * The auto-mirrored style of icons we use.
     */
    val autoMirroredIconStyle = Icons.AutoMirrored.Outlined


    /**
     * The shape with which we clip eve type renders.
     */
    val eveTypeRenderingClip = RoundedCornerShape(4.dp)


    /**
     * The shape of dialogs.
     */
    val dialogShape: Shape = RoundedCornerShape(6.dp)


    /**
     * Defines the spacings between elements, paddings etc.
     */
    class Spacing {

        // Generic sizes
        val xxxsmall = 2.dp
        val xxsmall = 4.dp
        val xsmall = 6.dp
        val small = 8.dp
        val medium = 10.dp
        val large = 12.dp
        val largeish = 14.dp
        val larger = 16.dp
        val xlarge = 20.dp
        val xxlarge = 24.dp
        val xxxlarge = 32.dp


        /**
         * The horizontal distance between the edge of a panel that has a visual edge, and the content.
         */
        val horizontalEdgeMargin = xlarge


        /**
         * The vertical distance between the edge of a panel that has a visual edge, and the content.
         */
        val verticalEdgeMargin = large


        /**
         * The combination of [horizontalEdgeMargin] and [verticalEdgeMargin] into a [PaddingValues] object.
         */
        val edgeMargins = PaddingValues(
            horizontal = horizontalEdgeMargin,
            vertical = verticalEdgeMargin
        )


        /**
         * The content padding of tooltips.
         */
        val tooltipContentPadding = PaddingValues(horizontal = large, vertical = xsmall)


        /**
         * The content padding of buttons.
         */
        val buttonContentPadding = PaddingValues(horizontal = large, vertical = xsmall)


        /**
         * The padding for the actions button icon.
         */
        val actionsButtonIconPadding = xxsmall


    }


    /**
     * Defines the sizes of various elements.
     */
    class Sizes {


        /**
         * The maximum width of tooltips.
         */
        val maxTooltipWidth = 600.dp  // Maximum is effect tooltip for siege module


        /**
         * The default/initial size of the main window.
         */
        val mainWindowDefaultSize = DpSize(
            width = DefaultMainWindowWidth.dp,
            height = DefaultMainWindowHeight.dp
        ) * ApplicationUiScale


        /**
         * The width of the "Saved Fits" side panel.
         */
        val savedFitsPanelWidth = 300.dp


        /**
         * The size of a small eve type icon.
         */
        val eveTypeIconSmall = 22.dp


        /**
         * The size of a medium eve type icon.
         */
        val eveTypeIconMedium = 28.dp


        /**
         * The size of a large eve type icon.
         */
        val eveTypeIconLarge = 32.dp


        /**
         * The size of a full-size eve type icon.
         */
        @Suppress("unused")
        val eveTypeIconFull = 64.dp


        /**
         * The default size of a fit window.
         */
        val fitWindowDefaultSize = DpSize(
            width = DefaultMainWindowWidth.dp - savedFitsPanelWidth,
            height = DefaultMainWindowHeight.dp
        ) * ApplicationUiScale


        /**
         * The default size of the settings window.
         */
        val settingsWindowDefaultSize = DpSize(
            width = 1010.dp,
            height = 600.dp
        ) * ApplicationUiScale


        /**
         * The default size of the market tree window.
         */
        val marketTreeWindowDefaultSize = DpSize(
            width = 1100.dp,
            height = 830.dp
        )


        /**
         * The default size of the tournament window.
         */
        val tournamentWindowDefaultSize = DpSize(
            width = 1000.dp,
            height = 800.dp
        )


        /**
         * The default size of the doctrines window.
         */
        val doctrinesWindowDefaultSize = tournamentWindowDefaultSize


        /**
        * The maximum width of a snack bar.
         */
        val maxSnackbarWidth = 540.dp


        /**
         * The height of the items in the fit list.
         */
        val fitListItemHeight = 40.dp


        /**
        * The width of the "Fit Stats" side panel.
         */
        val fitStatsPanelWidth = 400.dp


        /**
         * The width of the caption (label) column (e.g. "CPU", "Power", "Shield") in the fit stats tables.
         */
        val fitStatsCaptionColumnWidth = 60.dp


        /**
         * The width of the colored rectangles that specify the percentage of some resource utilization in the fit
         * stats.
         */
        val fitStatsPercentageViewWidth = 60.dp


        /**
         * The width of the resource use text in fit editor slot autosuggest.
         * This is not used for the first resource type in the row; that one gets as much available space as it needs.
         */
        val fitEditorSuggestedItemsResourceUseWidth = 70.dp


        /**
         * The width of the price text in fit editor slot suggested items dropdown.
         */
        val fitEditorSuggestedItemsPriceWidth = 70.dp


        /**
         * The widths of the columns in the fit editor.
         */
        val fitEditorColumnWidths = listOf(
            24.dp,           // state icon
            24.dp,           // type icon
            Dp.Unspecified,  // name
            60.dp,           // power
            60.dp,           // cpu
            90.dp,           // range
            120.dp,          // effect
            90.dp,           // price
        )


        /**
         * The height of a slot row in the fit editor.
         */
        val fitEditorSlotRowHeight = 22.dp


        /**
         * The size of item state icons.
         */
        val itemStateIconSize = 18.dp


        /**
        * The maximum height of the auto-suggest dropdown in the item selector.
         */
        val itemSelectorSuggestionDropdownMaxHeight = 300.dp


        /**
         * The width of the subsystem effect description in the subsystem auto-suggest.
         */
        val fitEditorSubsystemEffectDescriptionInAutoSuggestWidth = 120.dp


        /**
         * The height of the status bar.
         */
        val statusBarHeight = 44.dp


        /**
        * The width of the "About" dialog.
         */
        val aboutDialogWidth = 480.dp


        /**
         * The size of a menu item icon.
         */
        val menuItemIconSize = 16.dp


        /**
         * The small variant of width of key shortcut descriptors in menu items.
         * This can typically be used when the key shortcut has one modifier.
         */
        val menuItemKeyShortcutWidthSmall = if (hostOs == OS.MacOS) 25.dp else 60.dp


        /**
         * The medium variant of width of key shortcut descriptors in menu items.
         * This can typically be used when the key shortcut has two modifiers.
         */
        val menuItemKeyShortcutWidthMedium = if (hostOs == OS.MacOS) 35.dp else 80.dp


        /**
         * The default size of a mutation editing dialog.
         */
        val mutationDialogSize = DpSize(440.dp, 720.dp)


        /**
         * The width of the textfields for mutated attribute values.
         */
        val mutatedAttributeValueTextFieldWidth = 165.dp


        /**
         * The width of the list of settings panes in the settings window.
         */
        val settingsPaneListWidth = 230.dp


        /**
         * The width of the list of skill sets.
         */
        val skillSetsListWidth = 220.dp


        /**
         * The width of the list of skill groups.
         */
        val skillGroupsListWidth = 220.dp


        /**
         * The minimum width of the list of skills.
         */
        val skillsListMinWidth = 200.dp


        /**
         * The width of the list of skill levels.
         */
        val skillLevelsListWidth = 120.dp


        /**
         * The size of the icon that marks the default skill set.
         */
        val defaultSkillSetIcon = 16.dp


        /**
         * The size of the arrows in [Spinner].
         */
        val spinnerArrowIcon = 22.dp


        /**
        * The width of the compositions list panel.
         */
        val compositionListWidth = 300.dp


        /**
         * The height of the items in the composition list.
         */
        val compositionListItemHeight = 40.dp


        /**
         * The width of the market group tree itself.
         * This must be enough to show:
         * `Ammunition & Charges -> Missiles -> Heavy Assault Missiles -> Advanced Long Range Heavy Assault Missiles`.
         */
        val marketTreeWidth = 500.dp


        /**
         * The height of the market group items in the market tree.
         */
        val marketTreeGroupItemHeight = 32.dp


        /**
         * The height of the eve items in the market tree.
         */
        val marketTreeItemHeight = 32.dp


        /**
         * The size of eve item icons in the market tree items list.
         */
        val marketTreeItemIconSize = eveTypeIconSmall


        /**
         * The size of the collapse/expand icon in the market tree.
         */
        val marketTreeCollapseExpandIconSize = 20.dp


        /**
         * The offset to the right of each consecutive level in the market tree.
         */
        val marketTreePerLevelOffset = 26.dp


        /**
         * The height of rows in the "Attributes" section of an item info panel.
         */
        val itemAttributesRowHeight = 24.dp


        /**
         * The width of the mini market tree in the fit editor.
         */
        val fitEditorMiniMarketPopupTreeWidth = 600.dp


        /**
         * The size of the collapse/expand icon in the mini market tree.
         */
        val miniMarketTreeArrowIconSize = marketTreeCollapseExpandIconSize


    }


    /**
     * Defines the text styles.
     */
    class TextStyles {


        /**
         * The default style of text.
         */
        val default = TextStyle.Default.copy(
            fontSize = 14.sp,
            letterSpacing = 0.25.sp
        )


        /**
         * The style for a huge title (used for the "App loading status").
         */
        val hugeTitle = default.copy(
            fontSize = 36.sp,
            lineBreak = LineBreak.Heading,
        )


        /**
         * The style for a large heading/title.
         */
        val largeHeading = default.copy(
            fontSize = 16.sp,
            lineBreak = LineBreak.Heading,
            letterSpacing = 0.5.sp
        )


        /**
         * The style for a medium heading/title.
         */
        val mediumHeading = default.copy(
            fontSize = 14.sp,
            fontWeight = FontWeight.Bold,
            lineBreak = LineBreak.Heading,
            letterSpacing = 0.4.sp
        )


        /**
         * The style for a small heading/title.
         */
        val smallHeading = default.copy(
            fontSize = 12.sp,
            fontWeight = FontWeight.Bold,
            lineBreak = LineBreak.Heading,
            letterSpacing = 0.25.sp
        )


        /**
         * The style for a caption/label.
         */
        val caption = default.copy(
            fontSize = 12.sp,
            letterSpacing = 0.4.sp
        )


        /**
         * The style for menu items.
         */
        val menuItem = default.copy(
            fontSize = 12.sp,
            letterSpacing = 0.4.sp
        )


        /**
         * The style for menu item headings.
         */
        @Composable
        fun menuItemHeading() = menuItem.copy(
            fontSize = 11.sp,
            color = colors.menuItemSecondary()
        )


        /**
         * The style for text that explains or details something.
         */
        @Composable
        fun detailedText() = default.copy(
            fontSize = 11.sp,
            letterSpacing = 0.sp,
            color = LocalContentColor.current.copy(alpha = 0.6f)
        )


        /**
         * The style for a footnote.
         */
        val footnote = default.copy(
            fontSize = 12.sp,
            letterSpacing = 0.sp,
        )


        /**
         * The style for regular tooltip text.
         */
        val tooltipRegular = default.copy(
            fontWeight = FontWeight.Light,
            lineBreak = LineBreak.Paragraph,
            letterSpacing = 0.sp
        )


        /**
         * The style for headings/titles in a tooltip.
         */
        val tooltipHeading = tooltipRegular.copy(
            fontWeight = FontWeight.SemiBold,
            lineBreak = LineBreak.Heading
        )


        /**
         * The style for the text in the [PercentageView].
         */
        val percentageView = default.copy(
            fontSize = 12.sp,
            letterSpacing = 0.sp,
        )


        /**
         * The style of the section titles in the fit editor.
         */
        val fitEditorSectionTitle = default.copy(fontWeight = FontWeight.SemiBold)


        /**
         * The style of the extra info text in the section titles of the fit editor.
         */
        val fitEditorSectionTitleExtraInfo = default.copy(fontWeight = FontWeight.ExtraLight).toSpanStyle()


        /**
         * The style of the text in the fit editor autosuggest footer.
         */
        val fitEditorAutosuggestFooter
            get() = caption


        /**
         * The style of the text in the secondary slots of the fit editor (e.g. charges).
         */
        val fitEditorSecondarySlot = default.copy(
            fontWeight = FontWeight.Light,
            fontSize = default.fontSize * 0.92
        )


        /**
         * The style of the text in the tertiary slots of the fit editor (e.g. charges of remote fit modules).
         */
        val fitEditorTertiarySlot = default.copy(
            fontWeight = FontWeight.ExtraLight,
            fontSize = default.fontSize * 0.88
        )


        /**
         * The style of the numbers in the booster side effects widget.
         */
        val sideEffectWidget = default.copy(fontSize = 9.sp)


        /**
         * The style of the counts in the composition utility summary.
         */
        val compositionUtilitySummaryCounts = default.copy(
            fontWeight = FontWeight.Medium,
            fontSize = default.fontSize * 0.9
        )


        /**
         * The style of the details in the composition utility summary.
         */
        val compositionUtilitySummaryDetails = compositionUtilitySummaryCounts.copy(
            fontWeight = FontWeight.ExtraLight
        )


        /**
         * The style for the fit tags in the fit list.
         */
        @Composable
        fun fitListTag() = default.copy(
            fontSize = 12.sp,
            letterSpacing = 0.4.sp,
            color = LocalContentColor.current.copy(alpha = 0.7f)
        )

    }


    /**
     * The Theorycrafter base theme colors.
     */
    class BaseColors(
        val isLight: Boolean,
        val background: Color,
        val content: Color,
        val primary: Color,
        val secondary: Color,
        val errorBackground: Color,
        val errorContent: Color,
        val warningContent: Color,
        val successContent: Color,
    ) {

        fun asMaterialColors() =
            if (isLight) {
                lightColors(
                    primary = primary,
                    secondary = secondary,
                    background = background,
                    surface = background,
                    onPrimary = content,
                    onSecondary = content,
                    onSurface = content,
                    error = errorContent,
                )
            }
            else {
                darkColors(
                    primary = primary,
                    secondary = secondary,
                    background = background,
                    surface = background,
                    onPrimary = content,
                    onSecondary = content,
                    onSurface = content,
                    error = errorContent,
                )
            }

    }


    /**
     * The colors for a dark theme.
     */
    private val darkColors = BaseColors(
        isLight = false,
        background = Color(44, 44, 44),
        content = Color(224, 224, 224),
        primary = Color(100, 140, 190),
        secondary = Color(80, 90, 100),
        errorBackground = Color(146, 32, 48, alpha = 153),  // Need transparency so it doesn't obscure the selection
        errorContent = Color(255, 96, 102),
        warningContent = Color(187, 176, 80),
        successContent = Color(0, 160, 40)
    )


    /**
     * The colors for a light theme.
     */
    private val lightColors = BaseColors(
        isLight = true,
        background = Color(235, 235, 235),
        content = Color.Black,
        primary = Color(120, 140, 240),
        secondary = Color(200, 190, 180),
        errorBackground = Color(180, 53, 74, alpha = 153),  // Need transparency so it doesn't obscure the selection
        errorContent = Color(170, 70, 80),
        warningContent = Color(180, 150, 10),
        successContent = Color(0, 160, 40)
    )


    /**
     * Defines the various colors used in the app.
     */
    class Colors {


        /**
         * The base theme colors.
         */
        @Composable
        fun base(): BaseColors = when (TheorycrafterContext.settings.colorThemeStyle) {
            ColorThemeStyle.Light -> lightColors
            ColorThemeStyle.Dark -> darkColors
            ColorThemeStyle.System -> if (isSystemThemeDark()) darkColors else lightColors
        }


        /**
         * Returns one of given colors depending on whether the current theme is light/dark.
         */
        @Composable
        private inline fun themeStyleColor(
            light: BaseColors.() -> Color,
            dark: BaseColors.() -> Color
        ): Color {
            with(base()) {
                return if (isLight) light() else dark()
            }
        }


        /**
         * Returns one of given colors depending on whether the current theme is light/dark.
         */
        @Composable
        private fun themeStyleColor(
            light: Color,
            dark: Color
        ): Color {
            with(base()) {
                return if (isLight) light else dark
            }
        }


        /**
         * Returns the primary color, or the disabled primary color, based on [enabled].
         */
        @Composable
        fun primary(enabled: Boolean = true) = if (enabled)
            base().primary
        else
            base().content.copy(alpha = ContentAlpha.disabled)


        /**
        * Returns [BaseColors.errorContent] if [valid] is `false`, [Color.Unspecified] otherwise.
         */
        @Composable
        fun invalidContent(valid: Boolean) = if (valid) Color.Unspecified else base().errorContent


        /**
        * The text selection (background) color.
         */
        @Composable
        fun textSelection(): TextSelectionColors {
            return TextSelectionColors(
                handleColor = Color.Transparent,
                backgroundColor = themeStyleColor(
                    light = { secondary.darker(0.3f).copy(alpha = 0.5f) },
                    dark = { secondary.lighter(0.35f).copy(alpha = 0.6f) }
                )
            )
        }


        /**
         * The background color to use for selected items.
         */
        @Composable
        fun selectedItemBackground(): Color {
            return base().secondary
        }


        /**
         * The background color for rows at odd indices, for lists use alternating background colors for even and odd
         * rows. The even background should be transparent.
         */
        @Composable
        fun oddRowBackground(): Color {
            return base().secondary.copy(alpha = 0.2f)
        }


        /**
         * The background color for a row with the given index, for lists that use alternating background colors for
         * even and odd rows.
         */
        @Composable
        fun alternatingRowBackground(index: Int): Color {
            return if (index.mod(2) == 1)
                oddRowBackground()
            else
                Color.Transparent
        }


        /**
         * The scrim color for dialogs.
         */
        @Composable
        fun innerDialogScrim(): Color = Color.Black.copy(alpha = 0.3f)


        /**
        * The secondary (less pronounced) content color in menu items. This is used for key shortcuts and headings.
         */
        @Composable
        fun menuItemSecondary(): Color = themeStyleColor(
            light = { content.lighter(factor = 0.45f) },
            dark = { content.darker(factor = 0.25f) }
        )


        /**
         * The color for menu separators.
         */
        @Composable
        fun menuItemSeparator(): Color = themeStyleColor(
            light = { content.lighter(factor = 0.8f) },
            dark = { content.darker(factor = 0.55f) }
        )


        /**
         * The background color for tooltips.
         */
        @Composable
        fun tooltipBackground(): Color = themeStyleColor(
            light = { Color(240, 230, 200) },
            dark = { background }
        )


        /**
         * The background color for areas that are interactive, e.g. the background of a text field.
         */
        @Composable
        fun interactiveBackground(): Color = MaterialTheme.colors.onSurface.copy(
            alpha = TextFieldDefaults.BackgroundOpacity
        )


        /**
        * The textfield colors.
         */
        @Composable
        fun textFieldColors(
            textColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
            disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
            backgroundColor: Color = interactiveBackground(),
            // Set the cursor color to the text color
            cursorColor: Color = textColor,
            errorCursorColor: Color = MaterialTheme.colors.error,
            focusedIndicatorColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
            unfocusedIndicatorColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.UnfocusedIndicatorLineOpacity),
            disabledIndicatorColor: Color = unfocusedIndicatorColor.copy(alpha = ContentAlpha.disabled),
            errorIndicatorColor: Color = MaterialTheme.colors.error,
            leadingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity),
            disabledLeadingIconColor: Color = leadingIconColor.copy(alpha = ContentAlpha.disabled),
            errorLeadingIconColor: Color = leadingIconColor,
            trailingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity),
            disabledTrailingIconColor: Color = trailingIconColor.copy(alpha = ContentAlpha.disabled),
            errorTrailingIconColor: Color = MaterialTheme.colors.error,
            focusedLabelColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
            unfocusedLabelColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
            disabledLabelColor: Color = unfocusedLabelColor.copy(ContentAlpha.disabled),
            errorLabelColor: Color = MaterialTheme.colors.error,
            placeholderColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
            disabledPlaceholderColor: Color = placeholderColor.copy(ContentAlpha.disabled)
        ): TextFieldColors = TextFieldDefaults.textFieldColors(
            textColor = textColor,
            disabledTextColor = disabledTextColor,
            backgroundColor = backgroundColor,
            cursorColor = cursorColor,
            errorCursorColor = errorCursorColor,
            focusedIndicatorColor = focusedIndicatorColor,
            unfocusedIndicatorColor = unfocusedIndicatorColor,
            disabledIndicatorColor = disabledIndicatorColor,
            errorIndicatorColor = errorIndicatorColor,
            leadingIconColor = leadingIconColor,
            disabledLeadingIconColor = disabledLeadingIconColor,
            errorLeadingIconColor = errorLeadingIconColor,
            trailingIconColor = trailingIconColor,
            disabledTrailingIconColor = disabledTrailingIconColor,
            errorTrailingIconColor = errorTrailingIconColor,
            focusedLabelColor = focusedLabelColor,
            unfocusedLabelColor = unfocusedLabelColor,
            disabledLabelColor = disabledLabelColor,
            errorLabelColor = errorLabelColor,
            placeholderColor = placeholderColor,
            disabledPlaceholderColor = disabledPlaceholderColor,
        )


        /**
         * The outlined textfield colors.
         */
        @Composable
        fun outlinedTextFieldColors(
            textColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
            disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
            backgroundColor: Color = Color.Transparent,
            // Set the cursor color to the text color
            cursorColor: Color = textColor,
            errorCursorColor: Color = MaterialTheme.colors.error,
            focusedBorderColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
            unfocusedBorderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
            disabledBorderColor: Color = unfocusedBorderColor.copy(alpha = ContentAlpha.disabled),
            errorBorderColor: Color = MaterialTheme.colors.error,
            leadingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity),
            disabledLeadingIconColor: Color = leadingIconColor.copy(alpha = ContentAlpha.disabled),
            errorLeadingIconColor: Color = leadingIconColor,
            trailingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity),
            disabledTrailingIconColor: Color = trailingIconColor.copy(alpha = ContentAlpha.disabled),
            errorTrailingIconColor: Color = MaterialTheme.colors.error,
            focusedLabelColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
            unfocusedLabelColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
            disabledLabelColor: Color = unfocusedLabelColor.copy(ContentAlpha.disabled),
            errorLabelColor: Color = MaterialTheme.colors.error,
            placeholderColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
            disabledPlaceholderColor: Color = placeholderColor.copy(ContentAlpha.disabled)
        ): TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(
            textColor = textColor,
            disabledTextColor = disabledTextColor,
            backgroundColor = backgroundColor,
            cursorColor = cursorColor,
            errorCursorColor = errorCursorColor,
            focusedBorderColor = focusedBorderColor,
            unfocusedBorderColor = unfocusedBorderColor,
            disabledBorderColor = disabledBorderColor,
            errorBorderColor = errorBorderColor,
            leadingIconColor = leadingIconColor,
            disabledLeadingIconColor = disabledLeadingIconColor,
            errorLeadingIconColor = errorLeadingIconColor,
            trailingIconColor = trailingIconColor,
            disabledTrailingIconColor = disabledTrailingIconColor,
            errorTrailingIconColor = errorTrailingIconColor,
            focusedLabelColor = focusedLabelColor,
            unfocusedLabelColor = unfocusedLabelColor,
            disabledLabelColor = disabledLabelColor,
            errorLabelColor = errorLabelColor,
            placeholderColor = placeholderColor,
            disabledPlaceholderColor = disabledPlaceholderColor,
        )


        /**
         * The colors for a checkbox.
         */
        @Composable
        fun checkboxColors() = with(base()) {
            val disabledColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
            CheckboxDefaults.colors(
                checkedColor = primary,
                checkmarkColor = content,
                disabledColor = disabledColor,
                // For some reason, default disabledIndeterminateColor differs from disabled color
                disabledIndeterminateColor = disabledColor
            )
        }


        /**
         * The colors for a radio button.
         */
        @Composable
        fun radioButtonColors() = with(base()) {
            RadioButtonDefaults.colors(
                selectedColor = primary,
            )
        }


        /**
         * The colors for a switch widget.
         */
        @Composable
        fun switchColors() = SwitchDefaults.colors(
            checkedThumbColor = base().primary,
        )


        /**
         * The color of the tooltip border.
         */
        @Composable
        fun tooltipBorder() = Color.Gray


        /**
         * The background color for the "keys" in the fit editor key shortcuts tooltip.
         */
        @Composable
        fun fitEditorKeyShortcutsTooltipKeysBackground() = themeStyleColor(
            light = Color.Black.copy(alpha = 0.1f),
            dark = Color.Black.copy(alpha = 0.3f)
        )


        /**
         * The colors of the [ThreePanelScaffold].
         */
        class ThreePanelScaffoldColors(
            val sideBackground: Color,
            val middleBackground: Color,
            val statusBarBackground: Color,
        )


        /**
         * The actual colors of [ThreePanelScaffold].
         */
        @Composable
        fun threePanelScaffold(): ThreePanelScaffoldColors {
            val colors = base()
            return if (colors.isLight) {
                ThreePanelScaffoldColors(
                    sideBackground = colors.background.darker(0.09f),
                    middleBackground = colors.background,
                    statusBarBackground = colors.background.darker(0.2f),
                )
            }
            else {
                ThreePanelScaffoldColors(
                    sideBackground = colors.background.lighter(0.07f),
                    middleBackground = colors.background,
                    statusBarBackground = colors.background.lighter(0.1f),
                )
            }
        }


        /**
         * The background color of a small header.
         */
        @Composable
        fun smallHeaderBackground(): Color = themeStyleColor(
            light = { background.darker(0.2f) },
            dark = { background.lighter(0.2f) }
        )


        /**
         * The background color of a small footer.
         */
        @Composable
        fun smallFooterBackground(): Color = smallHeaderBackground()


        /**
         * The text color for percentage indicators.
         */
        private fun percentageIndicatorTextColor(baseColors: BaseColors) =
            if (baseColors.isLight) baseColors.content else Color.White


        /**
         * The text shadow color for percentage indicators.
         */
        private fun percentageIndicatorTextShadowColor(baseColors: BaseColors) =
            if (baseColors.isLight) baseColors.background else Color.Black


        /**
         * Returns the colors for percentage indicators, given the base and background colors.
         */
        private fun percentageViewColors(
            baseColors: BaseColors,
            filled: Color,
            unfilled: Color
        ) = PercentageViewColors(
            textColor = percentageIndicatorTextColor(baseColors),
            textShadowColor = percentageIndicatorTextShadowColor(baseColors),
            filled = if (baseColors.isLight) filled.lighter(0.15f) else filled,
            unfilled = if (baseColors.isLight) unfilled.lighter(0.2f) else unfilled
        )


        /**
         * The colors for CPU indicators.
         */
        @Composable
        fun cpuIndicatorColors() = percentageViewColors(
            baseColors = base(),
            filled = Color(170, 70, 180),
            unfilled =  Color(90, 60, 100)
        )


        /**
         * The "filled" color for power indicators.
         */
        @Composable
        fun powerIndicatorColors() = percentageViewColors(
            baseColors = base(),
            filled = Color(80, 140, 70),
            unfilled = Color(70, 90, 65)
        )


        /**
         * The colors of the resistance indicators, by damage type.
         */
        @Composable
        fun resistanceIndicatorColors(damageType: DamageType): PercentageViewColors {
            val baseColors = base()
            return remember(damageType, baseColors) {
                when (damageType) {
                    DamageType.EM -> percentageViewColors(
                        baseColors = baseColors,
                        filled = Color(80, 150, 210),
                        unfilled = Color(70, 90, 120)
                    )
                    DamageType.THERMAL -> percentageViewColors(
                        baseColors = baseColors,
                        filled = Color(210, 100, 80),
                        unfilled = Color(120, 70, 70)
                    )
                    DamageType.KINETIC -> percentageViewColors(
                        baseColors = baseColors,
                        filled = Color(150, 150, 150),
                        unfilled = Color(100, 100, 100)
                    )
                    DamageType.EXPLOSIVE -> percentageViewColors(
                        baseColors = baseColors,
                        filled = Color(200, 150, 70),
                        unfilled = Color(90, 90, 60)
                    )
                }
            }
        }


        /**
         * The color of a "good" mutation value.
         */
        val mutationColorGood = Color(0, 160, 30)


        /**
         * The color of a "bad" mutation value.
         */
        val mutationColorBad = Color(190, 20, 50)


        /**
         * The color of the indicator of the "current" item in a list.
         */
        val currentItemIndicator = Color.Gray


        /**
         * The background of a module (and perhaps other items in the future) slot when it's being dragged to rearrange.
         */
        @Composable
        fun draggedSlotBackground(): Color = selectedItemBackground().copy(alpha = 0.75f)


        /**
         * The background color for a selected tab in a tabbed box.
         */
        @Composable
        fun tabbedBoxSelectedBackground() = themeStyleColor(
            light = { background.darker(0.09f) },
            dark = { background.lighter(0.06f) }
        )


        /**
         * The color for the cheapest price.
         */
        @Composable
        fun cheap() = themeStyleColor(
            light = Color(40, 50, 150),
            dark = Color(120, 130, 200)
        )


        /**
         * The color for the most expensive price.
         */
        @Composable
        fun expensive() = themeStyleColor(
            light = Color.Red,
            dark = Color(255, 100, 60)
        )


    }


    /**
     * The configuration/theming of tooltips.
     */
    val TooltipHostProperties = TooltipHostProperties(
        decorator = { content ->
            Surface(
                elevation = 6.dp,
                shape = RoundedCornerShape(4.dp),
                color = colors.tooltipBackground(),
                border = BorderStroke(width = Dp.Hairline, color = colors.tooltipBorder()),
            ) {
                ProvideTextStyle(textStyles.tooltipRegular) {
                    Box(
                        Modifier
                            .padding(spacing.tooltipContentPadding)
                            .widthIn(max = sizes.maxTooltipWidth),
                    ) {
                        content()
                    }
                }
            }
        }
    )


    /**
     * The default tooltip show delay, in milliseconds.
     */
    const val DefaultTooltipDelay = 300


}


/**
 * A simple text tooltip.
 */
fun Modifier.tooltip(
    text: String?,
    delayMillis: Int = TheorycrafterTheme.DefaultTooltipDelay,
    placement: EasyTooltipPlacement = TooltipDefaults.Placement,
    modifier: Modifier = Modifier
) = easyTooltip(
    text = text,
    delayMillis = delayMillis,
    placement = placement,
    modifier = modifier
)


/**
 * A simple text tooltip.
 */
fun Modifier.tooltip(
    text: @Composable (() -> String)?,
    delayMillis: Int = TheorycrafterTheme.DefaultTooltipDelay,
    placement: EasyTooltipPlacement = TooltipDefaults.Placement,
    modifier: Modifier = Modifier
) = easyTooltip(
    text = text,
    delayMillis = delayMillis,
    placement = placement,
    modifier = modifier
)


/**
 * A tooltip with an [AnnotatedString] content.
 */
fun Modifier.tooltip(
    annotatedText: @Composable (() -> AnnotatedString)?,
    inlineContent: Map<String, InlineTextContent> = mapOf(),
    delayMillis: Int = TheorycrafterTheme.DefaultTooltipDelay,
    placement: EasyTooltipPlacement = TooltipDefaults.Placement,
    modifier: Modifier = Modifier
) = easyTooltip(
    annotatedText = annotatedText,
    inlineContent = inlineContent,
    delayMillis = delayMillis,
    placement = placement,
    modifier = modifier
)


/**
 * A tooltip for buttons showing text and a key shortcut.
 */
fun Modifier.tooltip(
    text: String,
    keyShortcut: KeyShortcut?,
    delayMillis: Int = TheorycrafterTheme.DefaultTooltipDelay,
    placement: EasyTooltipPlacement = TooltipDefaults.Placement
) = if (keyShortcut == null) {
    tooltip(
        text = text,
        delayMillis = delayMillis,
        placement = placement,
    )
} else {
    easyTooltip(
        delayMillis = delayMillis,
        placement = placement
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.small)
        ) {
            Text(text)
            Text(
                text = keyShortcut.displayString(),
                color = TheorycrafterTheme.colors.menuItemSecondary(),
            )
        }
    }
}


/**
 * A custom content tooltip.
 */
fun Modifier.tooltip(
    delayMillis: Int = TheorycrafterTheme.DefaultTooltipDelay,
    placement: EasyTooltipPlacement = TooltipDefaults.Placement,
    modifier: Modifier = Modifier,
    content: (@Composable () -> Unit)?
) = easyTooltip(
    delayMillis = delayMillis,
    placement = placement,
    modifier = modifier,
    content = content
)


/**
 * The default textfield scale hack factor.
 */
private const val DefaultTextFieldScaleHackFactor = 0.8f


/**
 * A local that saves the origin density, before the textfield scale hack.
 */
private val LocalUnhackedDensity = staticCompositionLocalOf<Density?> { null }


/**
 * A hack to scale textfields so their size is more suitable for desktop apps (the ones Material has are too big and
 * can't be customized).
 *
 * Note that unfortunately this causes the cursor to be thinner than desired (1px vs 2px).
 */
@Composable
private fun TheorycrafterTheme.TextFieldScaleHack(
    textStyle: TextStyle,
    factor: Float = DefaultTextFieldScaleHackFactor,
    content: @Composable () -> Unit
) {
    val density = LocalDensity.current
    AdjustedMaterialTheme(
        typography = MaterialTheme.typography.copy(
            subtitle1 = textStyles.default.copy(fontSize = textStyles.default.fontSize / factor),
            caption = textStyles.caption.copy(fontSize = textStyles.caption.fontSize / factor)
        )
    ) {
        CompositionLocalProvider(
            LocalDensity provides Density(density.density * factor),
            LocalUnhackedDensity provides density,
            LocalTextStyle provides textStyle.copy(fontSize = textStyle.fontSize / factor),
            LocalTextSelectionColors provides colors.textSelection()
        ) {
            content()
        }
    }
}


/**
 * A hack to allow textfields to be shorter than what Material demands.
 * See https://github.com/JetBrains/compose-multiplatform/issues/202 for details.
 */
private fun Modifier.overrideTextFieldMinHeight() = this.heightIn(min = 1.dp)


/**
 * A desktop-suitable [OutlinedTextField].
 */
@Composable
fun TheorycrafterTheme.OutlinedTextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = LocalTextStyle.current,
    label: @Composable (() -> Unit)? = null,
    placeholder: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions(),
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
    colors: TextFieldColors = TheorycrafterTheme.colors.outlinedTextFieldColors(),
    scaleHackFactor: Float = DefaultTextFieldScaleHackFactor,
) {
    TextFieldScaleHack(textStyle, scaleHackFactor) {
        androidx.compose.material.OutlinedTextField(
            value = value,
            onValueChange = onValueChange,
            modifier = modifier.overrideTextFieldMinHeight(),
            enabled = enabled,
            readOnly = readOnly,
            label = label,
            placeholder = placeholder,
            leadingIcon = leadingIcon,
            trailingIcon = trailingIcon,
            isError = isError,
            visualTransformation = visualTransformation,
            keyboardOptions = keyboardOptions,
            keyboardActions = keyboardActions,
            singleLine = singleLine,
            maxLines = maxLines,
            minLines = minLines,
            interactionSource = interactionSource,
            shape = shape,
            colors = colors,
        )
    }
}


/**
 * A desktop-suitable [OutlinedTextField].
 */
@Composable
fun TheorycrafterTheme.OutlinedTextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = LocalTextStyle.current,
    label: @Composable (() -> Unit)? = null,
    placeholder: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape = MaterialTheme.shapes.small,
    colors: TextFieldColors = TheorycrafterTheme.colors.outlinedTextFieldColors(),
    scaleHackFactor: Float = DefaultTextFieldScaleHackFactor,
) {
    TextFieldScaleHack(textStyle, scaleHackFactor) {
        androidx.compose.material.OutlinedTextField(
            value = value,
            onValueChange = onValueChange,
            modifier = modifier.overrideTextFieldMinHeight(),
            enabled = enabled,
            readOnly = readOnly,
            label = label,
            placeholder = placeholder,
            leadingIcon = leadingIcon,
            trailingIcon = trailingIcon,
            isError = isError,
            visualTransformation = visualTransformation,
            keyboardOptions = keyboardOptions,
            keyboardActions = keyboardActions,
            singleLine = singleLine,
            maxLines = maxLines,
            minLines = minLines,
            interactionSource = interactionSource,
            shape = shape,
            colors = colors,
        )
    }
}


/**
 * A desktop-suitable [TextField].
 */
@Composable
fun TheorycrafterTheme.TextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = LocalTextStyle.current,
    label: @Composable (() -> Unit)? = null,
    placeholder: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions(),
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape =
        MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
    colors: TextFieldColors = TheorycrafterTheme.colors.textFieldColors(),
    scaleHackFactor: Float = DefaultTextFieldScaleHackFactor,
) {
    TextFieldScaleHack(textStyle, scaleHackFactor) {
        androidx.compose.material.TextField(
            value = value,
            onValueChange = onValueChange,
            modifier = modifier.overrideTextFieldMinHeight(),
            enabled = enabled,
            readOnly = readOnly,
            label = label,
            placeholder = placeholder,
            leadingIcon = leadingIcon,
            trailingIcon = trailingIcon,
            isError = isError,
            visualTransformation = visualTransformation,
            keyboardOptions = keyboardOptions,
            keyboardActions = keyboardActions,
            singleLine = singleLine,
            maxLines = maxLines,
            minLines = minLines,
            interactionSource = interactionSource,
            shape = shape,
            colors = colors,
        )
    }
}


/**
 * A desktop-suitable [TextField].
 */
@Composable
fun TheorycrafterTheme.TextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = LocalTextStyle.current,
    label: @Composable (() -> Unit)? = null,
    placeholder: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions(),
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape =
        MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
    colors: TextFieldColors = TheorycrafterTheme.colors.textFieldColors(),
    scaleHackFactor: Float = DefaultTextFieldScaleHackFactor,
) {
    TextFieldScaleHack(textStyle, scaleHackFactor) {
        androidx.compose.material.TextField(
            value = value,
            onValueChange = onValueChange,
            modifier = modifier.overrideTextFieldMinHeight(),
            enabled = enabled,
            readOnly = readOnly,
            label = label,
            placeholder = placeholder,
            leadingIcon = leadingIcon,
            trailingIcon = trailingIcon,
            isError = isError,
            visualTransformation = visualTransformation,
            keyboardOptions = keyboardOptions,
            keyboardActions = keyboardActions,
            singleLine = singleLine,
            maxLines = maxLines,
            minLines = minLines,
            interactionSource = interactionSource,
            shape = shape,
            colors = colors,
        )
    }
}


/**
 * A desktop-suitable checkbox.
 */
@Composable
fun TheorycrafterTheme.Checkbox(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = TheorycrafterTheme.colors.checkboxColors()
) {
    androidx.compose.material.Checkbox(
        checked = checked,
        onCheckedChange = onCheckedChange,
        modifier = modifier.padding(spacing.xxsmall),
        enabled = enabled,
        interactionSource = interactionSource,
        colors = colors
    )
}


/**
 * A checkbox that takes and modifies [MutableState] as its state.
 */
@Composable
@Suppress("UnusedReceiverParameter")
fun TheorycrafterTheme.Checkbox(
    state: MutableState<Boolean>,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = TheorycrafterTheme.colors.checkboxColors()
) {
    TheorycrafterTheme.Checkbox(
        checked = state.value,
        onCheckedChange = state.component2(),
        modifier = modifier,
        enabled = enabled,
        interactionSource = interactionSource,
        colors = colors
    )
}


/**
 * A desktop-suitable radio button.
 */
@Composable
fun TheorycrafterTheme.RadioButton(
    selected: Boolean,
    onClick: (() -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: RadioButtonColors = TheorycrafterTheme.colors.radioButtonColors()
) {
    androidx.compose.material.RadioButton(
        selected = selected,
        onClick = onClick,
        modifier = modifier.padding(spacing.xxsmall),
        enabled = enabled,
        interactionSource = interactionSource,
        colors = colors
    )
}


/**
 * A desktop-suitable switch.
 */
@Composable
fun TheorycrafterTheme.Switch(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: SwitchColors = TheorycrafterTheme.colors.switchColors()
) {
    androidx.compose.material.Switch(
        checked = checked,
        onCheckedChange = onCheckedChange,
        modifier = modifier.padding(spacing.xxsmall),
        enabled = enabled,
        interactionSource = interactionSource,
        colors = colors
    )
}


/**
 * A raised button displaying text.
 */
@Composable
fun TheorycrafterTheme.RaisedButtonWithText(
    text: String,
    style: TextStyle = LocalTextStyle.current,
    contentPadding: PaddingValues = spacing.buttonContentPadding,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    onClick: () -> Unit
) {
    compose.widgets.RaisedButtonWithText(
        text = text,
        style = style,
        modifier = modifier,
        contentPadding = contentPadding,
        enabled = enabled,
        onClick = onClick,
        colors = colors
    )
}


/**
 * A flat button displaying text with the regular content color.
 */
@Composable
fun TheorycrafterTheme.IntegratedButtonWithText(
    text: String,
    style: TextStyle = LocalTextStyle.current,
    contentPadding: PaddingValues = spacing.buttonContentPadding,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    colors: ButtonColors = ButtonDefaults.textButtonColors(
        contentColor = LocalContentColor.current
    ),
    onClick: () -> Unit
) {
    compose.widgets.FlatButtonWithText(
        text = text,
        style = style,
        modifier = modifier,
        contentPadding = contentPadding,
        enabled = enabled,
        onClick = onClick,
        colors = colors
    )
}


/**
 * A flat button displaying content with the regular content color.
 */
@Composable
fun TheorycrafterTheme.IntegratedButton(
    contentPadding: PaddingValues = spacing.buttonContentPadding,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    colors: ButtonColors = ButtonDefaults.textButtonColors(
        contentColor = LocalContentColor.current
    ),
    onClick: () -> Unit,
    content: @Composable RowScope.() -> Unit
) {
    TextButton(
        modifier = modifier,
        contentPadding = contentPadding,
        enabled = enabled,
        onClick = onClick,
        colors = colors,
        content = content
    )
}


/**
 * A button with an outline.
 */
@Suppress("unused")
@Composable
fun TheorycrafterTheme.OutlinedButtonWithText(
    text: String,
    style: TextStyle = LocalTextStyle.current,
    contentPadding: PaddingValues = spacing.buttonContentPadding,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    colors: ButtonColors = ButtonDefaults.textButtonColors(
        contentColor = LocalContentColor.current
    ),
    onClick: () -> Unit
) {
    compose.widgets.OutlinedButtonWithText(
        text = text,
        style = style,
        modifier = modifier,
        contentPadding = contentPadding,
        enabled = enabled,
        onClick = onClick,
        colors = colors
    )
}


/**
 * The [AutoSuggestMenuItemProvider] we use for auto-suggest dropdown menus.
 */
private val TheorycrafterAutoSuggestMenuItemProvider = AutoSuggestMenuItemProvider { onClick, modifier, content ->
    DropdownMenuItem(
        modifier = modifier,
        contentPadding = PaddingValues(
            horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin
        ),
        onClick = onClick
    ) {
        ProvideTextStyle(TheorycrafterTheme.textStyles.default) {
            content()
        }
    }
}


/**
 * A modified [MaterialTheme].
 */
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun AdjustedMaterialTheme(
    typography: Typography = MaterialTheme.typography,
    content: @Composable () -> Unit
) {
    val baseColors = TheorycrafterTheme.colors.base()
    MaterialTheme(
        colors = baseColors.asMaterialColors(),
        typography = typography
    ) {
        CompositionLocalProvider(
            LocalTextStyle provides TheorycrafterTheme.textStyles.default,
            LocalTextSelectionColors provides TheorycrafterTheme.colors.textSelection(),
            LocalContentColor provides baseColors.content,
            LocalMinimumInteractiveComponentEnforcement provides false
        ) {
            content()
        }
    }
}


/**
 * The theme for Theorycrafter.
 */
@Composable
fun TheorycrafterTheme(content: @Composable () -> Unit) {
    ProvideSystemTheme {
        AdjustedMaterialTheme {
            CompositionLocalProvider(
                LocalContextMenuRepresentation provides TheorycrafterContextMenuRepresentation,
                LocalAutoSuggestMenuItemProvider provides TheorycrafterAutoSuggestMenuItemProvider,
            ) {
                content()
            }
        }
    }
}


/**
 * The representation of context menus in Theorycrafter.
 *
 * This is used to provide the context menu in e.g. a text field.
 */
private val TheorycrafterContextMenuRepresentation = object: ContextMenuRepresentation {

    @Composable
    override fun Representation(state: ContextMenuState, items: () -> List<ContextMenuItem>) {
        val dropdownMenuState = remember {
            DropdownMenuState()
        }.also {
            it.status = when (val status = state.status) {
                is ContextMenuState.Status.Open -> DropdownMenuState.Status.Open(status.rect.topLeft)
                else -> DropdownMenuState.Status.Closed
            }
        }

        // If shown from inside a textfield, the local density is hacked via TextFieldScaleHack,
        // so we need to unhack it to show the menu at normal size
        val unhackedDensity = LocalUnhackedDensity.current ?: LocalDensity.current
        CompositionLocalProvider(LocalDensity provides unhackedDensity) {
            ContextMenu(
                state = dropdownMenuState,
                onDismissRequest = { state.status = ContextMenuState.Status.Closed }
            ) {
                // Remember the items to avoid having them change when an item is clicked
                // and the underlying state changes.
                val localItems = remember { items() }
                for (item in localItems) {
                    val keyShortcut = when (item.label) {
                        "Copy" -> KeyShortcut.CopyToClipboard
                        "Paste" -> KeyShortcut.PasteFromClipboard
                        "Cut" -> KeyShortcut.CutToClipboard
                        else -> null
                    }

                    MenuItem(
                        text = item.label,
                        icon = {
                            when (item.label) {
                                "Copy" -> theorycrafter.ui.Icons.Copy()
                                "Paste" -> theorycrafter.ui.Icons.Paste()
                                "Cut" -> theorycrafter.ui.Icons.Cut()
                            }
                        },
                        displayedKeyShortcut = keyShortcut,
                        reserveSpaceForKeyShortcut = true,
                        onCloseMenu = { state.status = ContextMenuState.Status.Closed },
                        action = item.onClick
                    )
                }
            }
        }
    }

}


/**
 * The scope of the content of an actions menu.
 */
class ActionsMenuScope(val onCloseMenu: () -> Unit) {

    /**
     * Adds a menu item with the given parameters.
     */
    @Composable
    fun menuItem(
        text: String,
        enabled: Boolean = true,
        icon: (@Composable () -> Unit)? = EmptyIcon,
        keyShortcut: KeyShortcut? = null,
        reserveSpaceForKeyShortcut: Boolean = false,
        action: () -> Unit
    ) {
        MenuItem(
            text = text,
            enabled = enabled,
            icon = icon,
            displayedKeyShortcut = keyShortcut,
            reserveSpaceForKeyShortcut = reserveSpaceForKeyShortcut,
            keyShortcutDescriptorWidth = TheorycrafterTheme.sizes.menuItemKeyShortcutWidthMedium,
            onCloseMenu = onCloseMenu,
            action = action
        )
    }

    /**
     * Adds a menu separator.
     */
    @Composable
    fun separator() {
        MenuSeparator()
    }

}


/**
 * An actions (hamburger) menu.
 */
@Composable
fun ActionsMenuButton(
    contentDescription: String,
    content: @Composable ActionsMenuScope.() -> Unit,
) {
    val padding = TheorycrafterTheme.spacing.actionsButtonIconPadding
    IconMenuButton(
        modifier = Modifier
            .padding(end = (TheorycrafterTheme.spacing.horizontalEdgeMargin - padding).coerceAtLeast(0.dp)),
        iconPadding = PaddingValues(padding),
        contentDescription = contentDescription,
    ) { onCloseMenu ->
        val scope = remember(onCloseMenu) { ActionsMenuScope(onCloseMenu) }
        scope.content()
    }
}


/**
 * A rounded-corner border with the appropriate color to indicate the focused state, similar to [OutlinedTextField].
 */
@Composable
fun Modifier.roundedFocusBorder(
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
): Modifier {
    val borderColor by TheorycrafterTheme.colors.outlinedTextFieldColors().indicatorColor(
        enabled = true,
        isError = false,
        interactionSource = interactionSource
    )

    val shape = MaterialTheme.shapes.medium
    val isFocused by interactionSource.collectIsFocusedAsState()
    val width = if (isFocused) 2.dp else 1.dp
    return this
        .clip(shape)
        .border(width, borderColor, shape)
}


/**
 * Returns the padding modifier to apply to trailing icons in text fields.
 */
fun Modifier.textFieldTrailingIconPadding() = this.padding(end = TheorycrafterTheme.spacing.larger)
