package theorycrafter.optimizer

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import compose.utils.*
import compose.widgets.*
import eve.data.*
import eve.data.typeid.*
import eve.data.utils.ValueByEnum
import eve.data.utils.valueByEnum
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import theorycrafter.*
import theorycrafter.fits.FitHandle
import theorycrafter.fitting.*
import theorycrafter.optimizer.OptimizationGoal.Companion.ArmorEhp
import theorycrafter.optimizer.OptimizationGoal.Companion.ArmorResistanceFactor
import theorycrafter.optimizer.OptimizationGoal.Companion.LowestArmorResistance
import theorycrafter.optimizer.OptimizationGoal.Companion.LowestShieldResistance
import theorycrafter.optimizer.OptimizationGoal.Companion.ShieldEhp
import theorycrafter.optimizer.OptimizationGoal.Companion.ShieldResistanceFactor
import theorycrafter.optimizer.OptimizationGoal.Companion.TotalEhp
import theorycrafter.optimizer.OptimizationGoal.Companion.ehpAndArmorResistance
import theorycrafter.optimizer.OptimizationGoal.Companion.ehpAndShieldResistance
import theorycrafter.storage.isFlagship
import theorycrafter.tournaments.TournamentDescriptor
import theorycrafter.tournaments.TournamentRules
import theorycrafter.ui.*
import theorycrafter.ui.fiteditor.*
import theorycrafter.ui.tooltip
import theorycrafter.ui.widgets.*
import theorycrafter.utils.thenIf
import theorycrafter.utils.timeAction
import theorycrafter.utils.weightedAverage
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.math.log10
import kotlin.math.pow
import kotlin.math.roundToInt


/**
 * A dialog with the fit optimizer UI.
 */
@Composable
fun FitOptimizerDialog(
    fit: Fit,
    fitHandle: FitHandle,
    onCloseRequest: () -> Unit,
) {
    TheorycrafterDialogWindow(
        title = "Optimizing ${fitHandle.name} (${fit.ship.type.name})",
        state = rememberCenteredDialogState(FitOptimizerDialogSize),
        onCloseRequest = onCloseRequest,
        onKeyEvent = closeDialog(onCloseRequest)
    ) {
        FitOptimizerDialogContent(
            fit = fit,
            fitHandle = fitHandle,
            onCloseRequest = onCloseRequest
        )
    }
}


/**
 * The initial size of a fit optimizer window.
 */
private val FitOptimizerDialogSize = DpSize(940.dp, 1000.dp)


/**
 * The content of the fit optimizer dialog.
 */
@Composable
private fun FitOptimizerDialogContent(
    fit: Fit,
    fitHandle: FitHandle,
    onCloseRequest: () -> Unit
) {
    val optimizer: FitOptimizer = remember(fitHandle) { ConcurrentFitOptimizer(TheorycrafterContext.eveData) }
    val optimizedFit = optimizer.bestFit

    ThreePanelScaffold(
        modifier = Modifier.fillMaxSize(),
        middle = {
            FitOptimizerMainUi(
                originalFit = fit,
                optimizer = optimizer,
                onCloseRequest = onCloseRequest
            )
        },
        right = {
            FitStatsOrNone(optimizedFit ?: fit, changeKey = fitHandle)
        }
    )
}


/**
 * The main UI of the fit optimizer (the left side).
 */
@Composable
private fun FitOptimizerMainUi(
    originalFit: Fit,
    optimizer: FitOptimizer,
    onCloseRequest: () -> Unit,
) {
    val displayedFit = optimizer.bestFit ?: originalFit

    val coroutineScope = rememberCoroutineScope()
    var optimizationJob: Job? by remember { mutableStateOf(null) }
    val isOptimizationInProgress = optimizationJob != null

    var showSaveNewFitDialog by remember { mutableStateOf(false) }

    Column(Modifier.padding(top = TheorycrafterTheme.spacing.verticalEdgeMargin)) {
        val tournamentDescriptor = TheorycrafterContext.tournaments.activeTournamentDescriptor
        val settings = TheorycrafterContext.settings.fitOptimizer

        var optimizationParams by remember(originalFit, tournamentDescriptor) {
            mutableStateOf(defaultOptimizationParams(originalFit, settings, tournamentDescriptor))
        }

        val defaultTempRange = optimizationParams.optimizationGoal.defaultTempRange(originalFit)
        var saConfig by remember(originalFit, optimizationParams.optimizationGoal) {
            mutableStateOf(
                SimulatedAnnealingConfig(
                    concurrentExecutions = settings.concurrentExecutions,
                    initialTemp = defaultTempRange.endInclusive,
                    finalTemp = defaultTempRange.start,
                    coolingRate = settings.coolingRate,
                    iterationsPerTemp = settings.iterationsPerTemperature
                )
            )
        }

        LaunchedEffect(defaultTempRange) {
            saConfig = saConfig.copy(
                initialTemp = defaultTempRange.endInclusive,
                finalTemp = defaultTempRange.start,
            )
        }

        ProvideEnabled(enabled = !isOptimizationInProgress) {
            // Optimization params editors
            Column(
                verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.medium),
                modifier = Modifier
                    .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin)
                    .width(350.dp)
            ) {
                OptimizationGoalEditor(
                    originalFit = originalFit,
                    params = optimizationParams,
                    onParamsChanged = { optimizationParams = it },
                )

                AllowedModulesEditor(
                    params = optimizationParams,
                    onParamsChanged = { optimizationParams = it },
                )

                PriceLimitEditor(
                    fit = displayedFit,
                    params = optimizationParams,
                    onParamsChanged = {
                        optimizationParams = it
                        settings.priceLimit = it.priceLimit
                        settings.priceLimitEnabled = it.priceLimitEnabled
                    }
                )

                SimulatedAnnealingConfigEditor(
                    key = optimizationParams.optimizationGoal,
                    config = saConfig,
                    onConfigChanged = {
                        saConfig = it
                        settings.concurrentExecutions = it.concurrentExecutions
                        settings.coolingRate = it.coolingRate
                        settings.iterationsPerTemperature = it.iterationsPerTemp
                    }
                )
            }

            VSpacer(TheorycrafterTheme.spacing.larger)
            Divider(Modifier.fillMaxWidth())

            ContentWithScrollbar(Modifier.weight(1f)) {
                FitDisplay(
                    fit = displayedFit,
                    originalFit = originalFit,
                    optimizationParams = optimizationParams,
                    modifier = Modifier
                        .verticalScroll(scrollState)
                        .padding(top = TheorycrafterTheme.spacing.larger)
                )
                ScrollShadow(scrollState, top = true, bottom = true)
            }

            VSpacer(TheorycrafterTheme.spacing.large)
        }

        // Progress bar
        AnimatedVisibility(
            visible = optimizationJob != null,
            modifier = Modifier
                .height(6.dp)
                .fillMaxWidth()
        ) {
            LinearProgressIndicator(
                progress = optimizer.progress,
                modifier = Modifier
                    .fillMaxWidth()
            )
        }

        // Button row
        Row(
            modifier = Modifier
                .padding(TheorycrafterTheme.spacing.edgeMargins)
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(
                space = TheorycrafterTheme.spacing.medium,
                alignment = Alignment.End
            ),
        ) {
            if (!isOptimizationInProgress) {
                val dialogs = LocalStandardDialogs.current
                TheorycrafterTheme.RaisedButtonWithText(
                    text = "Optimize",
                    onClick = {
                        if (optimizationParams.priceLimitEnabled) {
                            val prices = TheorycrafterContext.eveItemPrices
                            if (prices == null) {
                                dialogs.showErrorDialog(
                                    title = "Can't limit cost",
                                    message = "Prices have not been loaded yet"
                                )
                                return@RaisedButtonWithText
                            }

                            val fitPrice = with(prices) { displayedFit.price(includeCharges = true) }
                            if (optimizationParams.priceLimit < fitPrice) {
                                dialogs.showErrorDialog(
                                    title = "Can't limit cost",
                                    message = "Cost limit below current fit price"
                                )
                                return@RaisedButtonWithText
                            }
                        }

                        optimizationJob = coroutineScope.launch(Dispatchers.Default) {
                            timeAction("Optimizing fit") {
                                optimizer.optimize(
                                    fit = displayedFit,
                                    optConfig = optimizationParams.toOptimizationConfig(TheorycrafterContext.eveData),
                                    saConfig = saConfig,
                                )
                            }
                            optimizationJob = null
                        }
                    },
                )
            } else {
                TheorycrafterTheme.RaisedButtonWithText(
                    text = "Stop Optimizing",
                    onClick = {
                        optimizationJob?.cancel()
                        optimizationJob = null
                    },
                )
            }
            TheorycrafterTheme.RaisedButtonWithText(
                text = "Revert to Original",
                enabled = !isOptimizationInProgress && (displayedFit != originalFit),
                onClick = {
                    coroutineScope.launch {
                        optimizer.reset()
                    }
                },
            )
            TheorycrafterTheme.RaisedButtonWithText(
                text = "Save",
                enabled = !isOptimizationInProgress && (displayedFit != originalFit),
                onClick = {
                    coroutineScope.launch {
                        displayedFit.saveTo(originalFit)
                        onCloseRequest()
                    }
                },
            )
            TheorycrafterTheme.RaisedButtonWithText(
                text = "Save As…",
                enabled = !isOptimizationInProgress && (displayedFit != originalFit),
                onClick = {
                    showSaveNewFitDialog = true
                },
            )
        }
    }

    if (showSaveNewFitDialog) {
        val fits = TheorycrafterContext.fits
        val originalFitHandle = fits.handleOf(originalFit)
        val originalTags = fits.storedFitOf(originalFitHandle).tags
        val optimizedTag = "optimized"
        val fitOpener = LocalFitOpener.current
        NewFitDialog(
            onDismiss = { showSaveNewFitDialog = false },
            forcedShipType = originalFit.ship.type,
            initialName = "Optimized ${originalFitHandle.name}",
            initialTags = if (optimizedTag in originalTags) originalTags else (originalTags + optimizedTag),
            createNewFit = { _, fitName, tags ->
                coroutineScope.launch {
                    val newFitHandle = createNewOptimizedFit(
                        original = originalFit,
                        optimized = displayedFit,
                        name = fitName,
                        tags = tags
                    )
                    fitOpener.openFitPreferCurrentWindow(newFitHandle)
                    onCloseRequest()
                }
            }
        )
    }
}


/**
 * Saves the fit into the [target] fit, by overwriting all the optimized items.
 */
@OptIn(ExperimentalAtomicApi::class)
private suspend fun Fit.saveTo(target: Fit) {
    val savedState = TheorycrafterContext.fitEditorSavedFitStateFor(target)
    val tempContext = FitEditorUndoRedoContext(
        fit = target,
        selectionModel = null,
        moduleSlotGroupsState = ModuleSlotGroupsState(target, savedState),
        showError = {}
    )
    savedState.undoRedoQueueContext.compareAndSet(null, tempContext)
    try {
        val undoRedoQueue = savedState.undoRedoQueue
        undoRedoQueue.performAndAppend(
            saveFitAction(source = this@saveTo, target = target)
        )
    } finally {
        savedState.undoRedoQueueContext.compareAndSet(tempContext, null)
    }
}


/**
 * Encapsules information about an overwritten module, so that its replacement can be undone.
 */
private class RememberedModule(
    val moduleType: ModuleType,
    val slotIndex: Int,
    val moduleState: Module.State,
    val enabled: Boolean,
    val chargeType: ChargeType?,
    val spoolupCycles: Double?,
) {


    constructor(module: Module, slotIndex: Int): this(
        moduleType = module.type,
        slotIndex = slotIndex,
        moduleState = module.state,
        enabled = module.enabled,
        chargeType = module.loadedCharge?.type,
        spoolupCycles = module.spoolupCycles?.doubleValue
    )


    /**
     * Fits the module into the given fit.
     */
    context(FittingEngine.ModificationScope)
    fun fitTo(fit: Fit) {
        val module = fit.fitModule(moduleType, slotIndex)
        module.setState(moduleState)
        module.setEnabled(enabled)
        if (chargeType != null)
            module.setCharge(chargeType)
        if (spoolupCycles != null)
            module.setSpoolupCycles(spoolupCycles)
    }


}


/**
 * Returns all the modules of the fit, as [RememberedModule].
 */
private fun Fit.rememberedModules(): List<RememberedModule> {
    return ModuleSlotType.entries.flatMap { rack ->
        modules.slotsInRack(rack).mapIndexedNotNull { index, module ->
            if (module == null)
                null
            else
                RememberedModule(module, index)
        }
    }
}


/**
 * Fits the given [modules] into the [fit].
 */
private fun FittingEngine.ModificationScope.fitRememberedModules(fit: Fit, modules: List<RememberedModule>) {
    for (module in fit.modules.all)
        fit.removeModule(module)
    for (module in modules)
        module.fitTo(fit)
}


/**
 * Returns an (undo-redo) action that "saves" the [source] fit into the [target] fit.
 */
private fun saveFitAction(source: Fit, target: Fit): FitEditingAction {
    val sourceModules = source.rememberedModules()
    val targetModules = target.rememberedModules()

    return object: FitEditingAction() {
        context(FitEditorUndoRedoContext)
        private fun FittingEngine.ModificationScope.replaceModulesWith(modules: List<RememberedModule>) {
            selectionModel?.clearSelection()
            fitRememberedModules(fit, modules)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            replaceModulesWith(sourceModules)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            replaceModulesWith(targetModules)
        }
    }
}


/**
 * Creates a new, "optimized" fit in the main fitting engine from the [optimized] fit returned by the fit optimizer.
 */
private suspend fun createNewOptimizedFit(
    original: Fit,
    optimized: Fit,
    name: String,
    tags: List<String>
): FitHandle {
    val fits = TheorycrafterContext.fits
    val originalHandle = fits.handleOf(original)
    val newFitHandle = fits.duplicate(fitHandle = originalHandle, name = name, tags = tags)
    val newFit = fits.engineFitOf(newFitHandle)
    fits.modifyAndSave {
        val optimizedModules = optimized.rememberedModules()
        fitRememberedModules(newFit, optimizedModules)
    }
    return newFitHandle
}


/**
 * Encapsulates the optimization params that can be specified by the user.
 */
@Stable
private data class OptimizationParams(
    private val fit: Fit,
    val optimizationGoal: OptimizationGoal,
    val allowedModules: AllowedModules,
    val allowedSlotChangesBySlotType: ValueByEnum<ModuleSlotType, List<MutableState<AllowedSlotChange?>>>,
    val allowFittingActiveModules: Boolean = true,
    val allowFittingRah: Boolean = true,
    val priceLimit: Double,
    val priceLimitEnabled: Boolean
) {

    fun toOptimizationConfig(eveData: EveData): OptimizationConfig {
        val fittableModules = allowedModules.modules(TheorycrafterContext.eveData, fit.ship.type, fit.isFlagship)
        val allowRah = !optimizationGoal.rahInclusionChoice || allowFittingRah
        val allowActiveModules = !optimizationGoal.activeModulesInclusionChoice || allowFittingActiveModules
        val fittableNewModules = fittableModules.filter {
            with(eveData) {
                optimizationGoal.moduleFilter?.let { moduleFilter ->
                    if (!moduleFilter(it) && !it.canImproveFitting())
                        return@with false
                }
                if (!allowRah && it.isReactiveArmorHardener())
                    return@with false
                if (!allowActiveModules && it.isActivable)
                    return@with false
                true
            }
        }
        val allowedSlotChanges = ModuleSlotType.entries.map { slotType ->
            allowedSlotChangesBySlotType[slotType].mapIndexedNotNull { index, allowedChangeState ->
                val allowedChange = allowedChangeState.value ?: return@mapIndexedNotNull null
                slotType.slotAtIndex(index) to allowedChange
            }
        }.flatten()

        return OptimizationConfig(
            score = optimizationGoal,
            fittableModules = fittableModules,
            fittableNewModules = fittableNewModules,
            allowedSlotChanges = allowedSlotChanges,
            pricesAndLimit = if (priceLimitEnabled) {
                val prices = TheorycrafterContext.eveItemPrices ?: error("Can't limit cost before prices are available")
                prices to priceLimit
            } else null
        )
    }

}


/**
 * Returns the default [OptimizationParams] for the given fit, with the given active tournament.
 */
private fun defaultOptimizationParams(
    fit: Fit,
    settings: TheorycrafterSettings.FitOptimizerSettings,
    tournamentDescriptor: TournamentDescriptor?,
): OptimizationParams {
    return OptimizationParams(
        fit = fit,
        optimizationGoal = TotalEhp,
        allowedModules =
            if (tournamentDescriptor == null) AllowedModules.Tech2 else AllowedModules.LegalInActiveTournament,
        allowedSlotChangesBySlotType = valueByEnum { slotType: ModuleSlotType ->
            fit.modules.slotsLimitedByRackSize(slotType).map { module ->
                mutableStateOf(
                    if (module == null)
                        AllowedSlotChange.AnyModule
                    else
                        AllowedSlotChange.VariationModule
                )
            }
        },
        priceLimit = settings.priceLimit,
        priceLimitEnabled = settings.priceLimitEnabled,
    )
}


/**
 * A group of optimization goals.
 */
private class OptimizationGoalCategory(
    val name: String,
    val goals: List<OptimizationGoal>,
) {

    constructor(name: String, vararg goals: OptimizationGoal): this(name, goals.toList())

    override fun toString() = name

}


/**
 * The editor UI for the optimization goals.
 */
@Composable
private fun ColumnScope.OptimizationGoalEditor(
    originalFit: Fit,
    params: OptimizationParams,
    onParamsChanged: (OptimizationParams) -> Unit,
) {
    val optimizationGoalCategories = remember(originalFit) {
        listOf(
            OptimizationGoalCategory(TotalEhp.name, TotalEhp),
            OptimizationGoalCategory(
                name = "Shield",
                ShieldEhp,
                LowestShieldResistance,
                ShieldResistanceFactor,
                ehpAndShieldResistance(originalFit, useLowestResistance = true, useShieldEhp = false),
                ehpAndShieldResistance(originalFit, useLowestResistance = true, useShieldEhp = true),
                ehpAndShieldResistance(originalFit, useLowestResistance = false, useShieldEhp = false),
                ehpAndShieldResistance(originalFit, useLowestResistance = false, useShieldEhp = true),
            ),
            OptimizationGoalCategory(
                name = "Armor",
                ArmorEhp,
                LowestArmorResistance,
                ArmorResistanceFactor,
                ehpAndArmorResistance(originalFit, useLowestResistance = true, useArmorEhp = false),
                ehpAndArmorResistance(originalFit, useLowestResistance = true, useArmorEhp = true),
                ehpAndArmorResistance(originalFit, useLowestResistance = false, useArmorEhp = false),
                ehpAndArmorResistance(originalFit, useLowestResistance = false, useArmorEhp = true),
            )
        )
    }
    var selectedCategory by remember(params.optimizationGoal) {
        mutableStateOf(optimizationGoalCategories.first { it.goals.contains(params.optimizationGoal) })
    }
    VerticallyCenteredRow {
        SingleLineText("Optimize: ")
        DropdownField(
            items = optimizationGoalCategories,
            selectedItem = selectedCategory,
            onItemSelected = { _, category ->
                selectedCategory = category
                onParamsChanged(params.copy(optimizationGoal = category.goals.first()))
            },
            enabled = LocalEnabled.current,
            modifier = Modifier.weight(1f),
        )
    }
    if (selectedCategory.goals.size > 1) {
        VerticallyCenteredRow {
            SingleLineText("Optimize for: ")
            DropdownField(
                items = selectedCategory.goals,
                selectedItem = params.optimizationGoal,
                onItemSelected = { _, goal ->
                    onParamsChanged(params.copy(optimizationGoal = goal))
                },
                enabled = LocalEnabled.current,
                modifier = Modifier.weight(1f),
            )
        }
    }

    params.optimizationGoal.extraUi?.invoke(this)
}


/**
 * The UI for editing the set of allowed modules for the fit optimizer to fit.
 */
@Suppress("UnusedReceiverParameter")
@Composable
private fun ColumnScope.AllowedModulesEditor(
    params: OptimizationParams,
    onParamsChanged: (OptimizationParams) -> Unit,
) {
    VerticallyCenteredRow {
        SingleLineText("Allowed modules: ")
        val activeTournamentRules = TheorycrafterContext.tournaments.activeRules
        val activeTournamentDescriptor = TheorycrafterContext.tournaments.activeTournamentDescriptor
        DropdownField(
            items = AllowedModules.entries.filter { it.available(activeTournamentRules) },
            selectedItem = params.allowedModules,
            onItemSelected = { _, allowedModules ->
                onParamsChanged(params.copy(allowedModules = allowedModules))
            },
            itemToString = {
                it.displayName(activeTournamentDescriptor)
            },
            modifier = Modifier.weight(1f),
            enabled = LocalEnabled.current,
        )
    }

    if (params.optimizationGoal.activeModulesInclusionChoice) {
        CheckboxedText(
            text = "Allow Active Modules",
            checked = params.allowFittingActiveModules,
            onCheckedChange = {
                onParamsChanged(params.copy(allowFittingActiveModules = !params.allowFittingActiveModules))
            },
            enabled = LocalEnabled.current
        )
    }

    if (params.optimizationGoal.rahInclusionChoice) {
        CheckboxedText(
            text = "Allow Reactive Armor Hardener",
            checked = params.allowFittingRah,
            enabled = LocalEnabled.current && params.allowFittingActiveModules,
            onCheckedChange = {
                onParamsChanged(params.copy(allowFittingRah = !params.allowFittingRah))
            },
        )
    }
}


/**
 * The editor for the fit price limit.
 */
@Composable
private fun PriceLimitEditor(
    fit: Fit,
    params: OptimizationParams,
    onParamsChanged: (OptimizationParams) -> Unit,
) {
    VerticallyCenteredRow(
        modifier = Modifier.fillMaxWidth()
    ) {
        val prices = TheorycrafterContext.eveItemPrices

        val textColor = if (prices == null)
            TheorycrafterTheme.colors.base().errorContent
        else
            LocalContentColor.current

        val fitPrice = if (prices == null) null else remember(fit, prices) {
            derivedStateOf {
                with(prices) {
                    fit.price(includeCharges = true)
                }
            }
        }.value

        CompositionLocalProvider(LocalContentColor provides textColor) {
            CheckboxedText(
                text = "Limit total cost to",
                checked = params.priceLimitEnabled,
                enabled = LocalEnabled.current,
                onCheckedChange = {
                    onParamsChanged(params.copy(priceLimitEnabled = it))
                },
                modifier = Modifier
                    .thenIf(prices == null) {
                        tooltip("Prices have not been loaded yet")
                    }
            )
        }

        HSpacer(TheorycrafterTheme.spacing.small)

        val textFieldEnabled = LocalEnabled.current && params.priceLimitEnabled
        IskTextField(
            value = params.priceLimit,
            onValueChange = {
                if (it != null) {
                    onParamsChanged(params.copy(priceLimit = it))
                }
            },
            enabled = textFieldEnabled,
            constraint =
                if ((fitPrice != null) && textFieldEnabled)
                    { value -> value > fitPrice }
                else
                    null,
            constraintFailureMessage = { "Limit below current fit cost" },
            modifier = Modifier.weight(1f),
            textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
        )

        Box(
            modifier = Modifier
                .size(0.dp)
                .wrapContentSize(Alignment.CenterStart, unbounded = true)
                .padding(start = TheorycrafterTheme.spacing.small),
            contentAlignment = Alignment.Center
        ) {
            Icons.Info(
                modifier = Modifier
                    .tooltip(
                        text = "Items with unknown price will not be considered for fitting",
                        placement = EasyTooltipPlacement.ElementBottomCenter
                    )
            )
        }
    }
}


/**
 * The editor UI for [SimulatedAnnealingConfig].
 */
@Suppress("UnusedReceiverParameter")
@Composable
private fun ColumnScope.SimulatedAnnealingConfigEditor(
    key: Any,
    config: SimulatedAnnealingConfig,
    onConfigChanged: (SimulatedAnnealingConfig) -> Unit
) {
    @Suppress("RemoveRedundantQualifierName", "RedundantSuppression")
    theorycrafter.ui.widgets.Disclosure(
        label = { Text("Simulated Annealing options (advanced)") },
    ) {
        key(key) {
            Column(
                verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.small),
                modifier = Modifier
                    .padding(top = TheorycrafterTheme.spacing.small)
                    .width(320.dp)
            ) {
                DoubleTextField(
                    value = config.initialTemp,
                    onValueChange = {
                        if (it != null)
                            onConfigChanged(config.copy(initialTemp = it))
                    },
                    formatter = maxSignificantDigitsFormatter(6),
                    label = { Text("Initial temperature") },
                    modifier = Modifier.fillMaxWidth(),
                    enabled = LocalEnabled.current
                )

                DoubleTextField(
                    value = config.finalTemp,
                    onValueChange = {
                        if (it != null)
                            onConfigChanged(config.copy(finalTemp = it))
                    },
                    formatter = maxSignificantDigitsFormatter(6),
                    label = { Text("Final temperature") },
                    modifier = Modifier.fillMaxWidth(),
                    enabled = LocalEnabled.current
                )

                DoubleTextField(
                    value = config.coolingRate,
                    onValueChange = {
                        if (it != null)
                            onConfigChanged(config.copy(coolingRate = it))
                    },
                    constraint = { (0 < it) && (it < 1.0) },
                    label = { Text("Cooling rate (0-1)") },
                    modifier = Modifier.fillMaxWidth(),
                    enabled = LocalEnabled.current
                )

                IntTextField(
                    value = config.iterationsPerTemp,
                    onValueChange = {
                        if (it != null)
                            onConfigChanged(config.copy(iterationsPerTemp = it))
                    },
                    range = 1 .. 100,
                    label = { Text("Iterations per temperature") },
                    modifier = Modifier.fillMaxWidth(),
                    enabled = LocalEnabled.current
                )

                IntTextField(
                    value = config.concurrentExecutions,
                    onValueChange = {
                        if (it != null)
                            onConfigChanged(config.copy(concurrentExecutions = it))
                    },
                    range = 1 .. 100,
                    label = { Text("Concurrent executions") },
                    modifier = Modifier.fillMaxWidth(),
                    enabled = LocalEnabled.current
                )

                UrlText(
                    text = "What is Simulated Annealing?",
                    url = "https://en.wikipedia.org/wiki/Simulated_annealing",
                    enabled = LocalEnabled.current,
                )
            }
        }
    }

}


/**
 * The options for the user to limit the set of modules the fit optimizer can use.
 */
private enum class AllowedModules(
    val displayName: (TournamentDescriptor?) -> String,
    val modules: EveData.(ShipType, isFlagship: Boolean) -> Collection<ModuleType>,
    val tournamentOnly: Boolean = false,
) {

    /**
     * The set of modules legal in the current tournament.
     */
    LegalInActiveTournament(
        displayName = { "Legal in ${it?.shortName}" },
        filter = { module, ship, isFlagship, activeTournamentRules ->
            activeTournamentRules.isModuleLegal(module, ship, isFlagship)
        },
        tournamentOnly = true
    ),

    /**
     * Tech 1 modules only.
     */
    Tech1(
        displayName = "Tech 1 only",
        filter = { it.isTech1Item }
    ),

    /**
     * Tech 2 or tech 1 modules.
     */
    Tech2(
        displayName = "Tech 2 or lower",
        filter = { it.isTech1Item || it.isTech2Item }
    ),

    /**
     * Faction, tech 2 or tech 1 modules.
     */
    Faction(
        displayName = "Faction or lower",
        filter = { it.isTech1Item || it.isTech2Item || it.isEmpireNavyFactionItem }
    ),

    /**
     * Deadspace, faction, tech 2 or tech 1 modules.
     */
    DeadSpace(
        displayName = "Deadspace or lower",
        filter = { it.isTech1Item || it.isTech2Item || it.isEmpireNavyFactionItem || it.isDeadspaceItem }
    ),

    /**
     * Any modules, including officer ones.
     */
    Any(
        displayName = { "Any" },
        modules = { _, _ -> moduleTypes }
    );

    constructor(
        displayName: String,
        filter: EveData.(ModuleType) -> Boolean,
    ): this(
        displayName = { displayName },
        modules = { _, _ ->
            moduleTypes.filter { filter(it) }
        }
    )

    constructor(
        displayName: (TournamentDescriptor?) -> String,
        filter: EveData.(ModuleType, ShipType, isFlagship: Boolean, TournamentRules?) -> Boolean,
        tournamentOnly: Boolean = false
    ): this(
        displayName = displayName,
        modules = { shipType, isFlagship ->
            val rules = TheorycrafterContext.tournaments.activeRules
            moduleTypes.filter { filter(it, shipType, isFlagship, rules) }
        },
        tournamentOnly = tournamentOnly
    )


    /**
     * Returns whether this [AllowedModules] rule should be available as a choice, given the current tournament.
     */
    fun available(tournamentRules: TournamentRules?) = !tournamentOnly || (tournamentRules != null)

}


/**
 * Encapsulates information about an optimization goal - what to optimize for.
 */
private class OptimizationGoal(
    val name: String,
    val score: (Fit) -> Double,
    val moduleFilter: (EveData.(ModuleType) -> Boolean)? = null,
    val activeModulesInclusionChoice: Boolean = false,
    val rahInclusionChoice: Boolean = false,
    val extraUi: (@Composable ColumnScope.() -> Unit)? = null,
    val defaultTempRange: (Fit) -> ClosedFloatingPointRange<Double>,
): ScoreFunction {


    /**
     * A constructor that takes a fixed [defaultTempRange].
     */
    constructor(
        name: String,
        score: (Fit) -> Double,
        moduleFilter: (EveData.(ModuleType) -> Boolean)? = null,
        activeModulesInclusionChoice: Boolean = false,
        rahInclusionChoice: Boolean = false,
        extraUi: (@Composable ColumnScope.() -> Unit)? = null,
        defaultTempRange: ClosedFloatingPointRange<Double>
    ): this(
        name = name,
        score = score,
        moduleFilter = moduleFilter,
        activeModulesInclusionChoice = activeModulesInclusionChoice,
        rahInclusionChoice = rahInclusionChoice,
        extraUi = extraUi,
        defaultTempRange = { defaultTempRange }
    )


    override fun eval(fit: Fit) = score(fit)


    override fun toString() = name


    companion object {


        /**
         * The default temperature range for resistance factor optimizations.
         */
        private val ResistanceOptimizationDefaultTempRange = 0.01 .. 100.0


        /**
        * Optimize for total EHP fit.
         */
        val TotalEhp = OptimizationGoal(
            name = "Total EHP",
            score = { it.defenses.ehp },
            moduleFilter = ModuleType::canImproveEhp,
            activeModulesInclusionChoice = true,
            rahInclusionChoice = true,
            defaultTempRange = {
                val baseEhp = it.defenses.ehp
                baseEhp / 100 .. baseEhp * 100
            }
        )

        /**
        * Optimize for shield EHP.
         */
        val ShieldEhp = OptimizationGoal(
            name = "Shield EHP",
            score = { it.defenses.shield.ehp },
            moduleFilter = ModuleType::canImproveShieldEhp,
            activeModulesInclusionChoice = true,
            defaultTempRange = {
                val baseEhp = it.defenses.shield.ehp
                baseEhp / 100 .. baseEhp * 100
            }
        )

        /**
        * Optimize for armor EHP.
         */
        val ArmorEhp = OptimizationGoal(
            name = "Armor EHP",
            score = { it.defenses.armor.ehp },
            moduleFilter = ModuleType::canImproveArmorEhp,
            activeModulesInclusionChoice = true,
            rahInclusionChoice = true,
            defaultTempRange = {
                val baseEhp = it.defenses.armor.ehp
                baseEhp / 100 .. baseEhp * 100
            }
        )


        /**
         * Optimize for lowest shield resistance.
         */
        val LowestShieldResistance = OptimizationGoal(
            name = "Lowest resistance",
            score = {
                DamageType.entries.minOf { damageType ->
                    resistFactor(resonance = it.defenses.shield.resonances[damageType].doubleValue)
                }
            },
            moduleFilter = ModuleType::canImproveShieldResists,
            activeModulesInclusionChoice = true,
            defaultTempRange = ResistanceOptimizationDefaultTempRange
        )


        /**
         * Optimize for lowest armor resistance.
         */
        val LowestArmorResistance = OptimizationGoal(
            name = "Lowest resistance",
            score = {
                DamageType.entries.minOf { damageType ->
                    resistFactor(it.defenses.armor.resonances[damageType].doubleValue)
                }
            },
            moduleFilter = ModuleType::canImproveArmorResists,
            activeModulesInclusionChoice = true,
            rahInclusionChoice = true,
            defaultTempRange = ResistanceOptimizationDefaultTempRange
        )


        /**
         * Optimize for shield resistance factor (average shield resistance).
         */
        val ShieldResistanceFactor = OptimizationGoal(
            name = "Average resistance",
            score = { it.defenses.shield.resistFactor },
            moduleFilter = ModuleType::canImproveShieldResists,
            activeModulesInclusionChoice = true,
            defaultTempRange = ResistanceOptimizationDefaultTempRange
        )


        /**
         * Optimize for armor resistance factor (average armor resistance).
         */
        val ArmorResistanceFactor = OptimizationGoal(
            name = "Average resistance",
            score =  { it.defenses.armor.resistFactor },
            moduleFilter = ModuleType::canImproveArmorResists,
            activeModulesInclusionChoice = true,
            rahInclusionChoice = true,
            defaultTempRange = ResistanceOptimizationDefaultTempRange
        )


        /**
         * Creates a dual optimization goal balancing between EHP and shield resistance.
         */
        fun ehpAndShieldResistance(
            originalFit: Fit,
            useLowestResistance: Boolean,
            useShieldEhp: Boolean,
        ) = ehpAndResistance(
            originalFit = originalFit,
            name = "${if (useShieldEhp) "Shield EHP" else "Total EHP"} & ${if (useLowestResistance) "lowest" else "average"} resistance",
            resistanceGoal = if (useLowestResistance) LowestShieldResistance else ShieldResistanceFactor,
            ehpGoal = if (useShieldEhp) ShieldEhp else TotalEhp,
        )


        /**
         * Creates a dual optimization goal balancing between EHP and armor resistance.
         */
        fun ehpAndArmorResistance(
            originalFit: Fit,
            useLowestResistance: Boolean,
            useArmorEhp: Boolean,
        ) = ehpAndResistance(
            originalFit = originalFit,
            name = "${if (useArmorEhp) "Armor EHP" else "Total EHP"} & ${if (useLowestResistance) "lowest" else "average"} resistance",
            resistanceGoal = if (useLowestResistance) LowestArmorResistance else ArmorResistanceFactor,
            ehpGoal = if (useArmorEhp) ArmorEhp else TotalEhp,
        )


        /**
         * A dual optimization goal balancing between EHP and some kind of resistance factor goal.
         */
        private fun ehpAndResistance(
            originalFit: Fit,
            name: String,
            resistanceGoal: OptimizationGoal,
            ehpGoal: OptimizationGoal,
        ) = dualOptimizationGoal(
            originalFit = originalFit,
            name = name,
            goal1 = ehpGoal,
            goal1Label = "EHP",
            goal2 = resistanceGoal,
            goal2Label = "Resistance",
            balanceText = { ratio ->
                when {
                    ratio.isInfinite() -> "EHP only"
                    ratio == 0.0 -> "Resistance only"
                    else -> {
                        val ehpDigits = log10(ehpGoal.score(originalFit)).toInt()
                        val ehpValue = 10.0.pow(ehpDigits)
                        "${ehpValue.asHitPoints(ehp = true)} = ${(ehpValue * ratio).toDecimalWithSignificantDigitsAtMost(2)} of resistance factor"
                    }
                }
            }
        )


        /**
         * Creates an [OptimizationGoal] that balances two score functions, with a weight determined by a slider.
         */
        fun dualOptimizationGoal(
            originalFit: Fit,
            name: String,
            goal1: OptimizationGoal,
            goal1Label: String,
            goal2: OptimizationGoal,
            goal2Label: String,
            balanceText: (scaledWeightRatio: Double) -> String,
            defaultWeight: Double = 0.5,
        ): OptimizationGoal {
            val goal1Scale = goal1.score(originalFit)
            val goal2Scale = goal2.score(originalFit)
            val weight = mutableStateOf(defaultWeight)
            return OptimizationGoal(
                name = name,
                score = { fit ->
                    weightedAverage(
                        goal1.score(fit) * goal2Scale,
                        goal2.score(fit) * goal1Scale,
                        weight = weight.value
                    )
                },
                moduleFilter = run {
                    val filter1 = goal1.moduleFilter
                    val filter2 = goal2.moduleFilter
                    when {
                        filter1 == null -> filter2
                        filter2 == null -> filter1
                        else -> { moduleType: ModuleType ->
                            filter1(moduleType) || filter2(moduleType)
                        }
                    }
                },
                activeModulesInclusionChoice = goal1.activeModulesInclusionChoice || goal2.activeModulesInclusionChoice,
                rahInclusionChoice = goal1.rahInclusionChoice || goal2.rahInclusionChoice,
                extraUi = {
                    BiWeightSlider(
                        weightState = weight,
                        defaultWeight = defaultWeight,
                        goal1Label = goal1Label,
                        goal2Label = goal2Label,
                        balanceText = balanceText(
                            (goal2Scale * weight.value) / (goal1Scale * (1 - weight.value))
                        ),
                        enabled = LocalEnabled.current,
                    )
                },
                defaultTempRange = {
                    val goal1Range = goal1.defaultTempRange(it)
                    val goal2Range = goal2.defaultTempRange(it)
                    val start = weightedAverage(
                        goal1Range.start * goal2Scale,
                        goal2Range.start * goal1Scale,
                        weight = weight.value
                    )
                    val end = weightedAverage(
                        goal1Range.endInclusive * goal2Scale,
                        goal2Range.endInclusive * goal1Scale,
                        weight = weight.value
                    )
                    start .. end
                }
            )
        }


    }


}


/**
 * The UI for selecting the weight in an [OptimizationGoal.dualOptimizationGoal].
 */
@Composable
private fun BiWeightSlider(
    weightState: MutableState<Double>,
    defaultWeight: Double,
    goal1Label: String,
    goal2Label: String,
    balanceText: String,
    enabled: Boolean
) {
    var weight by weightState
    var sliderCenterX: Float? by remember { mutableStateOf(null) }
    var balanceLabelWidth: Int? by remember { mutableStateOf(null) }
    VerticallyCenteredRow(
        horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.small)
    ) {
        SingleLineText(text = "Weight:")
        SingleLineText(goal1Label)
        AttributeMutationSlider(
            baseValue = 1 - defaultWeight,
            highIsGood = true,
            drawValueBackground = false,
            value = 1 - weight,
            onValueChange = { weight = 1 - it },
            valueRange = 0.0..1.0,
            extraTickValue = 0.5,
            modifier = Modifier
                .weight(1f)
                .onPlaced {
                    sliderCenterX = it.boundsInParent().center.x
                },
            enabled = enabled,
        )
        SingleLineText(goal2Label)
    }
    Row {
        SingleLineText(
            text = balanceText,
            style = TheorycrafterTheme.textStyles.detailedText(),
            modifier = Modifier
                .onPlaced {
                    balanceLabelWidth = it.size.width
                }
                .offset {
                    val centerX = sliderCenterX ?: return@offset IntOffset.Zero
                    val width = balanceLabelWidth ?: return@offset IntOffset.Zero
                    IntOffset(x = (centerX - width/2).roundToInt(), y = 0)
                }
        )
    }
}


/**
 * The grid column indices.
 */
private object GridCols {
    const val ITEM_ICON = 0
    const val ITEM_NAME = 1
    const val CHANGES = 2
    const val LAST = CHANGES
}


/**
 * The alignment in each column.
 */
private val ColumnAlignment = listOf(
    Alignment.Center,       // type icon
    Alignment.CenterStart,  // name
    Alignment.CenterStart,  // changes
)


/**
 * The widths of the columns in the grid.
 */
private val ColumnWidths = listOf(
    32.dp,              // module icon
    Dp.Unspecified,     // module name
    100.dp              // changes
)


/**
 * Displays the fit being optimized.
 */
@Composable
private fun FitDisplay(
    fit: Fit,
    originalFit: Fit,
    optimizationParams: OptimizationParams,
    modifier: Modifier = Modifier,
) {
    Column(modifier) {
        val rowHorizontalPadding = PaddingValues(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin)
        SimpleGridHeaderRow(
            columnWidths = ColumnWidths,
            defaultCellContentAlignment = ColumnAlignment::get,
            modifier = Modifier
                .padding(rowHorizontalPadding)
        ) {
            EmptyCell(0)
            EmptyCell(1)
            TextCell(2, "Changes")
        }
        SimpleGrid(
            columnWidths = ColumnWidths,
            defaultRowModifier = Modifier
                .padding(rowHorizontalPadding)
                .height(24.dp),
            defaultRowAlignment = Alignment.CenterVertically,
            defaultCellContentAlignment = ColumnAlignment::get,
        ) {
            var rowIndex = 0
            ProvideEnabledLocalContentAlpha(enabled = false) {
                fit.tacticalMode?.let { tacticalMode ->
                    rowIndex += TacticalModeSection(
                        rowIndex = rowIndex,
                        tacticalMode = tacticalMode
                    )
                }

                fit.subsystemByKind?.let { subsystems ->
                    rowIndex += SubsystemsSection(
                        rowIndex = rowIndex,
                        subsystems = subsystems
                    )
                }
            }

            for (slotType in ModuleSlotType.entries) {
                rowIndex += ModuleRack(
                    slotType = slotType,
                    rowIndex = rowIndex,
                    modules = fit.modules.slotsLimitedByRackSize(slotType),
                    allowedSlotChanges = optimizationParams.allowedSlotChangesBySlotType[slotType],
                )
            }

            ProvideEnabledLocalContentAlpha(enabled = false) {
                rowIndex += ItemSection(
                    title = "Drones",
                    rowIndex = rowIndex,
                    items = fit.drones.all,
                )
                rowIndex += ItemSection(
                    title = "Implants",
                    rowIndex = rowIndex,
                    items = fit.implants.fitted,
                    itemToString = { it.type.shortName() }
                )
                rowIndex += ItemSection(
                    title = "Boosters",
                    rowIndex = rowIndex,
                    items = fit.boosters.fitted,
                )
                rowIndex += ItemSection(
                    title = "Cargo",
                    rowIndex = rowIndex,
                    items = fit.cargohold.contents,
                    itemToString = { it.displayedText() }
                )
                rowIndex += CommandEffectSection(
                    rowIndex = rowIndex,
                    // We use the command effects of the original fit because the command fits in the optimized fit
                    // don't have a name, as they are not stored fits.
                    // This shouldn't cause any trouble, because the optimizer doesn't optimize the command effects.
                    commandEffects = originalFit.remoteEffects.command.allExcludingAuxiliary,
                )
                rowIndex += ItemSection(
                    title = "Environmental Effects",
                    rowIndex = rowIndex,
                    items = originalFit.environments,
                )
            }
        }
    }
}


/**
 * Displays a title of a section in the grid.
 */
@Composable
private fun GridScope.SectionTitle(
    text: String,
    isTopRow: Boolean = false
) {
    row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(
                top = if (isTopRow) 0.dp else TheorycrafterTheme.spacing.large,
                bottom = TheorycrafterTheme.spacing.xxxsmall
            )
            .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin)
    ) {
        cell(
            cellIndex = 0,
            colSpan = GridCols.LAST + 1,
            contentAlignment = Alignment.CenterStart
        ) {
            SingleLineText(
                text = text,
                style = TheorycrafterTheme.textStyles.fitEditorSectionTitle,
            )
        }
    }
}


/**
 * Displays the tactical mode.
 */
@Composable
private fun GridScope.TacticalModeSection(
    rowIndex: Int,
    tacticalMode: TacticalMode,
): Int {
    var currentRowIndex = rowIndex
    inRow(currentRowIndex++) {
        SectionTitle("Tactical Mode", isTopRow = rowIndex == 0)
    }

    inRow(currentRowIndex++) {
        TacticalModeRow(tacticalMode)
    }

    return currentRowIndex - rowIndex
}


/**
 * Displays the tactical mode in a row in the grid.
 */
@Composable
private fun GridScope.TacticalModeRow(tacticalMode: TacticalMode) {
    row {
        emptyCell(GridCols.ITEM_ICON)
        cell(cellIndex = GridCols.ITEM_NAME, colSpan = GridCols.LAST + 1 - GridCols.ITEM_NAME) {
            SingleLineText(
                with(TheorycrafterContext.eveData) {
                    tacticalMode.type.shortName()
                }
            )
        }
    }
}


/**
 * Displays the subsystems.
 */
@Composable
private fun GridScope.SubsystemsSection(
    rowIndex: Int,
    subsystems: ValueByEnum<SubsystemType.Kind, Subsystem?>
): Int {
    if (subsystems.values.all { it == null })
        return 0

    var currentRowIndex = rowIndex
    inRow(currentRowIndex++) {
        SectionTitle("Subsystems", isTopRow = rowIndex == 0)
    }

    for (kind in SubsystemType.Kind.entries) {
        val subsystem = subsystems[kind] ?: continue
        inRow(currentRowIndex++) {
            SubsystemRow(subsystem)
        }
    }

    return currentRowIndex - rowIndex
}


/**
 * Displays a subsystem in a row in the grid.
 */
@Composable
private fun GridScope.SubsystemRow(subsystem: Subsystem) {
    row {
        cell(cellIndex = GridCols.ITEM_ICON) {
            TypeIconCellContent(subsystem)
        }
        cell(cellIndex = GridCols.ITEM_NAME, colSpan = GridCols.LAST + 1 - GridCols.ITEM_NAME) {
            SingleLineText(subsystem.type.shortName(includeKind = true))
        }
    }
}


/**
 * Displays a rack of modules in the grid.
 */
@Composable
private fun GridScope.ModuleRack(
    slotType: ModuleSlotType,
    rowIndex: Int,
    modules: Iterable<Module?>,
    allowedSlotChanges: List<MutableState<AllowedSlotChange?>>,
): Int {
    var currentRowIndex = rowIndex
    inRow(currentRowIndex++) {
        SectionTitle("${slotType.slotName} Slots", isTopRow = rowIndex == 0)
    }

    for ((module, allowedSlotChangeState) in (modules zip allowedSlotChanges)) {
        inRow(currentRowIndex++) {
            ModuleRow(
                module = module,
                allowedSlotChange = allowedSlotChangeState,
            )
        }
    }

    return currentRowIndex - rowIndex
}


/**
 * Displays a module in a row in the grid.
 */
@Composable
private fun GridScope.ModuleRow(
    module: Module?,
    allowedSlotChange: MutableState<AllowedSlotChange?>,
) {
    val state = when (allowedSlotChange.value) {
        AllowedSlotChange.AnyModule -> ToggleableState.On
        AllowedSlotChange.VariationModule -> ToggleableState.Indeterminate
        else -> ToggleableState.Off
    }

    row(
        modifier = Modifier
            .clickable(
                enabled = LocalEnabled.current,
            ) {
                allowedSlotChange.value = if (module == null) {
                    when (allowedSlotChange.value) {
                        AllowedSlotChange.AnyModule -> null
                        else -> AllowedSlotChange.AnyModule
                    }
                } else {
                    when (allowedSlotChange.value) {
                        AllowedSlotChange.AnyModule -> AllowedSlotChange.VariationModule
                        AllowedSlotChange.VariationModule -> null
                        else -> AllowedSlotChange.AnyModule
                    }
                }
            }
            .thenIf(LocalEnabled.current) {
                highlightOnHover()
            }
            .then(defaultRowModifier)
    ) {
        cell(cellIndex = GridCols.ITEM_ICON) {
            TypeIconCellContent(module)
        }
        cell(cellIndex = GridCols.ITEM_NAME) {
            SingleLineText(module?.name ?: "Empty Slot")
        }
        cell(cellIndex = GridCols.CHANGES) {
            VerticallyCenteredRow(
                modifier = Modifier
                    .tooltip(
                        when (state) {
                            ToggleableState.Off -> "Optimizer will not change this slot"
                            ToggleableState.On -> "Optimizer may fit any module in this slot"
                            ToggleableState.Indeterminate -> "Optimizer may fit a variation of the current module in this slot"
                        }
                    ),
                horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxsmall)
            ) {
                TriStateCheckbox(
                    state = state,
                    enabled = LocalEnabled.current,
                    onClick = null,
                    colors = TheorycrafterTheme.colors.checkboxColors(),
                )

                SingleLineText(
                    text = when (state) {
                        ToggleableState.Off -> "None"
                        ToggleableState.On -> "Any"
                        ToggleableState.Indeterminate -> "Variants"
                    },
                    style = TheorycrafterTheme.textStyles.caption,
                )
            }
        }
    }
}


/**
 * Displays a section with eve items in the grid.
 */
@Composable
private fun <T: EveItem<*>> GridScope.ItemSection(
    title: String,
    rowIndex: Int,
    items: List<T>,
    itemToString: (T) -> String = { it.name }
): Int {
    if (items.isEmpty())
        return 0

    var currentRowIndex = rowIndex
    inRow(currentRowIndex++) {
        SectionTitle(title)
    }

    for (item in items) {
        inRow(currentRowIndex++) {
            ItemRow(item, itemToString)
        }
    }

    return currentRowIndex - rowIndex
}


/**
 * Displays an item in a row in the grid.
 */
@Composable
private fun <T: EveItem<*>> GridScope.ItemRow(
    item: T,
    itemToString: (T) -> String,
) {
    row {
        cell(cellIndex = GridCols.ITEM_ICON) {
            TypeIconCellContent(item)
        }
        cell(cellIndex = GridCols.ITEM_NAME, colSpan = GridCols.LAST + 1 - GridCols.ITEM_NAME) {
            SingleLineText(itemToString(item))
        }
    }
}


/**
 * Displays the section with command effects.
 */
@Composable
private fun GridScope.CommandEffectSection(
    rowIndex: Int,
    commandEffects: List<RemoteEffect>,
): Int {
    if (commandEffects.isEmpty())
        return 0

    var currentRowIndex = rowIndex
    inRow(currentRowIndex++) {
        SectionTitle("Command Fits")
    }

    for (commandEffect in commandEffects) {
        inRow(currentRowIndex++) {
            CommandEffectRow(commandEffect)
        }
    }

    return currentRowIndex - rowIndex
}


/**
 * Displays a single command effect in a row in the grid.
 */
@Composable
private fun GridScope.CommandEffectRow(commandEffect: RemoteEffect) {
    row {
        cell(cellIndex = GridCols.ITEM_ICON) {
            TypeIconCellContent(commandEffect.source.ship)
        }
        cell(cellIndex = GridCols.ITEM_NAME, colSpan = GridCols.LAST + 1 - GridCols.ITEM_NAME) {
            val fitHandle = TheorycrafterContext.fits.handleOf(commandEffect.source)
            SingleLineText(fitHandle.name)
        }
    }
}
