/**
 * The parts of the fit editor that relate to drones.
 */

package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import compose.input.MouseButton
import compose.input.onMousePress
import compose.widgets.GridScope
import eve.data.Attribute
import eve.data.DroneType
import eve.data.asDroneBandwidth
import eve.data.asDroneCapacity
import kotlinx.coroutines.runBlocking
import theorycrafter.LocalTheorycrafterWindowManager
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.DroneGroup
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.fitting.maxDroneGroupSize
import theorycrafter.ui.Icons
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.effectcolumn.displayedDroneEffect
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*
import kotlin.math.min


/**
 * The information regarding a drone group that we remember in order to fit it.
 */
private class DroneGroupInfo(
    private val slotIndex: Int,
    private val type: DroneType,
    private val size: Int,
    private val activeState: Boolean? // Null means to use the default initial state
) {


    /**
     * Adds a drone group to the given fit.
     */
    fun addTo(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            val droneGroup = fit.addDroneGroup(type, size = size, index = slotIndex)
            val isActive = activeState ?: canMakeActive(droneGroup)
            droneGroup.setActive(isActive)
        }
    }


    /**
     * Removes the drone group from the given fit.
     */
    fun removeFrom(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            val droneGroup = fit.drones.all[slotIndex]
            fit.removeDroneGroup(droneGroup)
        }
    }


}


/**
 * A [FitEditingAction] that replaces a drone group with another.
 */
private class DroneGroupReplacement(
    private val fit: Fit,
    private val removed: DroneGroupInfo?,
    private val added: DroneGroupInfo?
): FitEditingAction() {

    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.performEdit() {
        removed?.removeFrom(this, fit)
        added?.addTo(this, fit)
    }

    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.revertEdit() {
        added?.removeFrom(this, fit)
        removed?.addTo(this, fit)
    }

}


/**
 * Returns whether the given drone group can be made active after adding it.
 */
private fun canMakeActive(droneGroup: DroneGroup): Boolean {
    val drones = droneGroup.fit.drones
    return (drones.bandwidth.used + droneGroup.totalBandwidth <= drones.bandwidth.total) &&
            (drones.activeCount.used + droneGroup.size <= drones.activeCount.total)
}


/**
 * Returns a [FitEditingAction] that adds a drone group to the fit.
 */
private fun addDroneGroupAction(fit: Fit, droneType: DroneType, size: Int): FitEditingAction {
    return DroneGroupReplacement(
        fit = fit,
        removed = null,
        added = DroneGroupInfo(
            slotIndex = fit.drones.all.size,
            type = droneType,
            size = size,
            activeState = null
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes a drone group from the fit.
 */
private fun removeDroneGroupAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val droneGroup = fit.drones.all[slotIndex]
    return DroneGroupReplacement(
        fit = fit,
        removed = DroneGroupInfo(
            slotIndex = slotIndex,
            type = droneGroup.type,
            size = droneGroup.size,
            activeState = droneGroup.active
        ),
        added = null
    )
}


/**
 * Returns a [FitEditingAction] that replaces a drone group with a new one.
 */
private fun replaceDroneGroupAction(
    fit: Fit,
    slotIndex: Int,
    droneType: DroneType,
    size: Int,
    preserveActiveState: Boolean
): FitEditingAction? {
    val droneGroup = fit.drones.all[slotIndex]
    if ((droneGroup.type == droneType) && (droneGroup.size == size))
        return null
    val newDroneGroupActiveState = if (preserveActiveState) droneGroup.active else null

    return DroneGroupReplacement(
        fit = fit,
        removed = DroneGroupInfo(
            slotIndex = slotIndex,
            type = droneGroup.type,
            size = droneGroup.size,
            activeState = droneGroup.active
        ),
        added = DroneGroupInfo(
            slotIndex = slotIndex,
            type = droneType,
            size = size,
            activeState = newDroneGroupActiveState
        )
    )
}


/**
 * Returns a [FitEditingAction] that sets the number of drones in the given group.
 */
fun setDroneGroupSizeAction(fit: Fit, slotIndex: Int, newSize: Int): FitEditingAction? {
    val droneGroup = fit.drones.all[slotIndex]
    val prevSize = droneGroup.size
    if (prevSize == newSize)
        return null

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.drones.all[slotIndex].setSize(newSize)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.drones.all[slotIndex].setSize(prevSize)
        }

    }
}


/**
 * Returns a [FitEditingAction] that toggles the active state of the drone group.
 */
fun toggleDroneActiveStateAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val newState = !fit.drones.all[slotIndex].active

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.drones.all[slotIndex].setActive(newState)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.drones.all[slotIndex].setActive(!newState)
        }

    }
}


/**
 * Returns the auto-suggest for the current drone group.
 */
@Composable
private fun rememberDroneAutoSuggest(fit: Fit, carousel: Carousel<CarouselDroneType>?): AutoSuggest<DroneType> {
    val autoSuggest = TheorycrafterContext.autoSuggest.rememberForDroneTypes(fit)

    return remember(autoSuggest, carousel) {
        val variations = carousel?.items?.map { it.droneType }
        autoSuggest.onEmptyQueryReturn { variations }
    }
}


/**
 * Filters the given drone autosuggest based on the active tournament rules.
 */
@Composable
private fun AutoSuggest<DroneType>.withActiveTournamentRules(): AutoSuggest<DroneType> {
    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    return if (tournamentRules == null)
        this
    else remember(this, tournamentRules) {
        this.filterResults { tournamentRules.isDroneLegal(it) }
    }
}


/**
 * Returns the text we display to represent a drone group.
 */
fun droneGroupDisplayText(amount: Int, type: DroneType) =
    "${amount}x ${type.name}"


/**
 * A drone selection widget.
 */
@Composable
private fun GridScope.GridRowScope.DroneSelectorRow(
    fit: Fit,
    currentDrone: DroneGroup?,
    carousel: Carousel<CarouselDroneType>?,
    onDroneSelected: (DroneType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = rememberDroneAutoSuggest(fit, carousel).withActiveTournamentRules()
    val tournamentRules = TheorycrafterContext.tournaments.activeRules

    ItemSelectorRow(
        onItemSelected = onDroneSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { droneType, _ ->
            DefaultSuggestedEveItemTypeIcon(droneType)
            val droneAmount = replacementGroupSize(fit, currentDrone, droneType)
            Text(text = droneGroupDisplayText(droneAmount, droneType))
            if (TheorycrafterContext.settings.prices.showInFitEditorSuggestedItems) {
                Spacer(Modifier.weight(1f).widthIn(min = TheorycrafterTheme.spacing.medium))
                ItemPrice(
                    itemType = droneType,
                    amount = droneAmount,
                    textAlign = TextAlign.End,
                    modifier = Modifier.width(TheorycrafterTheme.sizes.fitEditorSuggestedItemsPriceWidth)
                )
            }
        },
        hint = "Drone name",
        marketGroupsParent = TheorycrafterContext.eveData.marketGroups.drones,
        itemFilter = {
            (it is DroneType) &&
                    (maxDroneGroupSize(fit, it) > 0) &&
                    tournamentRules.isDroneLegal(it)
        },
        showMarketInitially = currentDrone == null  // Because we show the carousel items in this case
    )
}


/**
 * The icon representing the drone group's state.
 */
@Composable
fun DroneStateIcon(
    droneGroup: DroneGroup,
    modifier: Modifier = Modifier,
    toggleActive: () -> Unit
) {
    Icons.DroneGroupState(
        isActive = droneGroup.active,
        modifier = modifier
            .onMousePress(consumeEvent = true) {  // Consume to prevent selecting the row
                toggleActive()
            }
            .onMousePress(MouseButton.Middle, consumeEvent = true) {  // Consume just in case
                toggleActive()
            }
    )
}


/**
 * A [DroneSlotContentProvider] for [DroneSlotContent].
 */
private val DroneSlotContent = DroneSlotContentProvider {
        scope: GridScope.GridRowScope,
        droneGroup: DroneGroup,
        carousel: Carousel<CarouselDroneType>,
        toggleActive: () -> Unit ->
    with(scope) {
        DroneSlotContent(
            droneGroup = droneGroup,
            carousel = carousel,
            toggleActive = toggleActive
        )
    }
}


/**
 * The row for a slot with a (non-`null`) drone group.
 */
@Composable
private fun GridScope.GridRowScope.DroneSlotContent(
    droneGroup: DroneGroup,
    carousel: Carousel<CarouselDroneType>,
    toggleActive: () -> Unit,
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        DroneStateIcon(
            droneGroup = droneGroup,
            toggleActive = toggleActive
        )
    }
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(droneGroup)
    }
    cell(cellIndex = GridCols.NAME, colSpan = 3){
        CarouselSlotContent(
            carousel = carousel,
            carouselItem = CarouselDroneType(droneGroup),
            eveItemType = { it.droneType },
            modifier = Modifier.fillMaxWidth(),
            text = { droneGroupDisplayText(it.amount, it.droneType) }
        )
    }
    cell(cellIndex = GridCols.RANGE) {
        TextAndTooltipCell(displayedDroneRange(droneGroup))
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedDroneEffect(droneGroup))
    }
    PriceCell(droneGroup.type, amount = droneGroup.size)
}


/**
 * Bundles the actions passed to [DroneSlotRow].
 */
class DroneSlotActions(
    private val fit: (DroneType, amount: Int, preserveActiveState: Boolean, triggerCarouselAnimation: Boolean) -> Unit,
    // Returns an action to replace the drone, without performing it.runCarouselAnimation
    val replaceDroneAction: (DroneType) -> FitEditingAction?,
    val clear: () -> Unit,
    val toggleActive: () -> Unit,
    val addAmount: (Int) -> Unit,
) {

    fun fitDroneGroup(
        droneType: DroneType,
        amount: Int,
        preserveActiveState: Boolean,
        triggerCarouselAnimation: Boolean = false
    ) {
        fit(droneType, amount, preserveActiveState, triggerCarouselAnimation)
    }

}


/**
 * Returns the size of the [DroneGroup] to fit when replacing an existing drone group.
 */
private fun replacementGroupSize(
    fit: Fit,
    currentDroneGroup: DroneGroup?,
    replacementDroneType: DroneType,
): Int = replacementGroupSize(
    fitBandwidth = fit.drones.bandwidth.total,
    fitCapacity = fit.drones.capacity.total,
    maxActiveDrones = fit.drones.activeCount.total,
    currentDroneGroup = currentDroneGroup,
    replacementDroneType = replacementDroneType
)



/**
 * Returns the size of the [DroneGroup] to fit when replacing an existing drone group.
 */
private fun replacementGroupSize(
    fitBandwidth: Double,
    fitCapacity: Int,
    maxActiveDrones: Int,
    currentDroneGroup: DroneGroup?,
    replacementDroneType: DroneType,
): Int {
    // Keep the size when not really replacing
    if (replacementDroneType == currentDroneGroup?.type)
        return currentDroneGroup.size

    return min(
        maxDroneGroupSize(
            droneType = replacementDroneType,
            fitBandwidth = fitBandwidth,
            fitCapacity = fitCapacity,
            maxActiveDrones = maxActiveDrones
        ),
        currentDroneGroup?.size ?: Int.MAX_VALUE
    )
}


/**
 * The item type for a drone carousel, which also includes the amount of drones.
 */
data class CarouselDroneType(
    val droneType: DroneType,
    val amount: Int,
)


/**
 * Returns a [CarouselDroneType] for the given drone group.
 */
@Composable
fun CarouselDroneType(droneGroup: DroneGroup): CarouselDroneType {
    return CarouselDroneType(droneGroup.type, droneGroup.size)
}


/**
 * Returns a [remember]ed carousel for drones, including the amount of each drone type.
 */
@Composable
private fun rememberDroneCarousel(fit: Fit, droneGroup: DroneGroup): Carousel<CarouselDroneType> {
    val droneTypes = rememberCarouselDroneTypes(droneGroup.type, fit, TheorycrafterContext.tournaments.activeRules)

    val fitBandwidth = fit.drones.bandwidth.total
    val fitCapacity = fit.drones.capacity.total
    val maxActiveDrones = fit.drones.activeCount.total

    fun computeDroneAmounts(droneTypes: List<DroneType>) = droneTypes.associateWith {
        replacementGroupSize(
            fitBandwidth = fitBandwidth,
            fitCapacity = fitCapacity,
            maxActiveDrones = maxActiveDrones,
            currentDroneGroup = droneGroup,
            replacementDroneType = it
        )
    }

    // A hack to preserve the number of drones as the user traverses the carousel, but also to avoid fitting more than
    // the allowed maximum.
    var amountByDroneType by remember(droneTypes) {
        mutableStateOf(
            computeDroneAmounts(droneTypes)
        )
    }

    // When the amount changed, it means the user has chosen it manually, so we must recompute the map
    if (amountByDroneType[droneGroup.type] != droneGroup.size) {
        amountByDroneType = computeDroneAmounts(droneTypes)
    }

    return remember(amountByDroneType) {
        Carousel(
            amountByDroneType.entries.map { CarouselDroneType(it.key, it.value) }
        )
    }
}


/**
 * The [SlotContextAction] for pasting into a drone slot.
 */
@Composable
private fun SlotContextAction.Companion.rememberPasteDronesAction(
    fit: Fit,
    fitDrones: (DroneType, Int) -> Unit,
) = rememberPastePossiblyDynamicItem(
        dynamicItemFromClipboardText = ::dynamicDroneFromClipboardText,
        localItemFromClipboardText = ::droneGroupFromClipboardText,
        pasteItem = { (droneType, amount) ->
            fitDrones(droneType, amount?.coerceAtLeast(1) ?: maxDroneGroupSize(fit, droneType))
        },
    )


/**
 * The [SlotContextAction] for reverting a drone to its base type.
 */
private fun SlotContextAction.Companion.revertToBase(
    droneGroup: DroneGroup,
    actions: DroneSlotActions,
): SlotContextAction? {
    val droneType = droneGroup.type
    return revertToBase(
        itemType = droneType,
        action = { actions.fitDroneGroup(droneType.baseType, droneGroup.size, true) }
    )
}


/**
 * The interface for providing the drone slot content for [DroneSlotRow].
 */
fun interface DroneSlotContentProvider {

    @Composable
    fun content(
        scope: GridScope.GridRowScope,
        droneGroup: DroneGroup,
        carousel: Carousel<CarouselDroneType>,
        toggleActive: () -> Unit,
    )

}


/**
 * The row displaying a non-empty drone slot.
 */
@Composable
fun GridScope.DroneSlotRow(
    testTag: String,
    droneGroup: DroneGroup,
    actions: DroneSlotActions,
    slotContentProvider: DroneSlotContentProvider
) {
    CompositionCounters.recomposed(testTag)

    val fit = droneGroup.fit
    val droneType = droneGroup.type
    val carousel = rememberDroneCarousel(fit, droneGroup)
    var droneTypeBeingMutated: DroneType? by remember { mutableStateOf(null) }

    val pasteAction = SlotContextAction.rememberPasteDronesAction(
        fit = fit,
        fitDrones = { newDroneType, amount -> actions.fitDroneGroup(newDroneType, amount, preserveActiveState = false) }
    )
    val clipboard = LocalClipboard.current
    val windowManager = LocalTheorycrafterWindowManager.current
    val editPriceOverrideAction = SlotContextAction.rememberEditPriceOverride(droneType)

    val contextActions = remember(fit, droneGroup, actions, clipboard, windowManager, droneType, editPriceOverrideAction) {
        listOfNotNull(
            SlotContextAction.showInfo(windowManager, droneGroup),
            SlotContextAction.Separator,
            SlotContextAction.cutToClipboard(clipboard, actions.clear, droneGroup::clipboardText),
            SlotContextAction.copyToClipboard(clipboard, droneGroup::clipboardText),
            pasteAction,
            SlotContextAction.Separator,
            SlotContextAction.clear(actions.clear),
            SlotContextAction.addOneItem(actions.addAmount),
            SlotContextAction.removeOneItem(actions.addAmount, showInContextMenu = true),
            SlotContextAction.togglePrimaryState(actions.toggleActive),
            SlotContextAction.Separator,
            SlotContextAction.mutateItem(
                itemType = droneType,
                openMutationEditWindow = { droneTypeBeingMutated = droneType }
            ),
            SlotContextAction.editMutation(
                itemType = droneType,
                openMutationEditWindow = { droneTypeBeingMutated = droneType }
            ),
            SlotContextAction.revertToBase(droneGroup, actions),
            editPriceOverrideAction
        )
    }
    val keyShortcuts = Modifier
        .carouselShortcuts(carousel, CarouselDroneType(droneGroup)) { (newDroneType, amount) ->
            actions.fitDroneGroup(newDroneType, amount, preserveActiveState = true, triggerCarouselAnimation = true)
        }

    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    val illegalityInTournamentReason = remember(tournamentRules, droneType) {
        itemIllegalityInTournamentReason(droneType, tournamentRules::isDroneLegal)
    }

    SlotRow(
        modifier = Modifier.testTag(testTag),
        modifierWhenNotEditing = keyShortcuts,
        contextActions = contextActions,
        invalidityReason = droneGroup.illegalFittingReason ?: illegalityInTournamentReason,
        editedRowContent = { onEditingCompleted ->
            DroneSelectorRow(
                fit = droneGroup.fit,
                currentDrone = droneGroup,
                carousel = carousel,
                onDroneSelected = { newDroneType ->
                    val amount = replacementGroupSize(fit, droneGroup, newDroneType)
                    val preserveActiveState = carousel.items.any { it.droneType == newDroneType }
                    actions.fitDroneGroup(newDroneType, amount, preserveActiveState = preserveActiveState)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        slotContentProvider.content(
            scope = this,
            droneGroup = droneGroup,
            carousel = carousel,
            toggleActive = actions.toggleActive,
        )
    }

    droneTypeBeingMutated?.let {
        LocalDroneMutationDialog(
            fit = fit,
            slotIndex = fit.drones.all.indexOf(droneGroup),
            droneType = it,
            actions = actions,
            undoRedoQueue = LocalFitEditorUndoRedoQueue.current,
            onCloseRequest = { droneTypeBeingMutated = null }
        )
    }
}


/**
 * A dialog for mutating, or editing an existing mutation of, a drone.
 */
@Composable
private fun LocalDroneMutationDialog(
    fit: Fit,
    slotIndex: Int,
    droneType: DroneType,
    actions: DroneSlotActions,
    undoRedoQueue: FitEditorUndoRedoQueue,
    onCloseRequest: () -> Unit
) {
    val originalMutatedAttributeValues = remember(droneType) {
        droneType.mutation?.mutatedAttributesAndValues()?.toTypedArray()
    }

    // The action that sets the drone; null if the dialog did not change the drone
    var droneSettingAction: FitEditingAction? by remember { mutableStateOf(null) }

    // The attribute values to which the dialog mutated; `null` if the dialog did not change the attributes
    var changedAttributeValues: List<Pair<Attribute<*>, Double>>? by remember(slotIndex) {
        mutableStateOf(null)
    }

    // TODO: Fix this horror
    val selectionModel = LocalSlotSelectionModel.current
    val moduleSlotGroupsState = LocalModuleSlotGroupsState.current
    val undoRedoContext = FitEditorUndoRedoContext(fit, selectionModel, moduleSlotGroupsState, showError = {})
    DroneMutationDialog(
        droneType = droneType,
        replaceItemInFit = { replacementDroneType ->
            runBlocking {
                with(undoRedoContext) {
                    droneSettingAction?.revert()
                }
                if (replacementDroneType != null) {
                    val action = actions.replaceDroneAction(replacementDroneType)
                    with(undoRedoContext) {
                        action?.perform()
                    }
                    droneSettingAction = action
                }
                else
                    droneSettingAction = null
            }
        },
        setMutatedAttributeValues = { mutatedAttrubuteValues ->
            val editedDroneType = fit.drones.all[slotIndex].type

            // If the original mutated attributes are null, it means droneType is not mutated, and this
            // function should only be called with a null value after reverting the module type to the original.
            val replacementAttributeValues = mutatedAttrubuteValues ?: originalMutatedAttributeValues
            if (replacementAttributeValues != null) {
                runBlocking {
                    TheorycrafterContext.fits.modifyAndSave {
                        editedDroneType.setMutatedAttributeValues(
                            attributesAndValues = replacementAttributeValues
                        )
                    }
                }
            }

            changedAttributeValues = if (mutatedAttrubuteValues == null)
                null
            else
                editedDroneType.mutation!!.mutatedAttributesAndValues()
        },
        onCloseRequest = {
            @Suppress("UnnecessaryVariable", "RedundantSuppression")
            val prevAttributeValues = originalMutatedAttributeValues
            val replacementMutatedAttributeValues = changedAttributeValues?.toTypedArray()
            if ((droneSettingAction != null) || (replacementMutatedAttributeValues != null)) {
                undoRedoQueue.append(
                    restoreSelection = false,
                    action = object: FitEditorUndoRedoAction {

                        context(FitEditorUndoRedoContext)
                        override suspend fun perform() {
                            droneSettingAction?.perform()
                            if (replacementMutatedAttributeValues != null) {
                                TheorycrafterContext.fits.modifyAndSave {
                                    fit.drones.all[slotIndex].type.setMutatedAttributeValues(
                                        attributesAndValues = replacementMutatedAttributeValues
                                    )
                                }
                            }
                        }

                        context(FitEditorUndoRedoContext)
                        override suspend fun revert() {
                            droneSettingAction?.revert()
                            if ((replacementMutatedAttributeValues != null) && (prevAttributeValues != null)) {
                                TheorycrafterContext.fits.modifyAndSave {
                                    fit.drones.all[slotIndex].type.setMutatedAttributeValues(
                                        attributesAndValues = prevAttributeValues
                                    )
                                }
                            }
                        }
                    }
                )
            }

            onCloseRequest()
        }
    )
}


/**
 * The row displaying the "Empty Drone Slot".
 */
@Composable
private fun GridScope.EmptyDroneSlotRow(
    fit: Fit,
    fitDroneGroup: (DroneType, Int) -> Unit
) {
    val clipboard = LocalClipboard.current
    val pasteAction = SlotContextAction.rememberPasteDronesAction(
        fit = fit,
        fitDrones = fitDroneGroup
    )
    val contextActions = remember(fit, fitDroneGroup, clipboard) {
        listOf(pasteAction)
    }

    SlotRow(
        modifier = Modifier
            .testTag(TestTags.FitEditor.EmptyDroneRow),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            DroneSelectorRow(
                fit = fit,
                currentDrone = null,
                carousel = null,
                onDroneSelected = { newDroneType ->
                    val amount = replacementGroupSize(fit, null, newDroneType)
                    fitDroneGroup(newDroneType, amount)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyRowContent(
            typeIcon = { Icons.DroneSlot() },
            text = "Empty drone slot"
        )
    }
}


/**
 * The title for the drone section.
 */
@Composable
private fun droneSectionTitle(fit: Fit) = buildAnnotatedString {
    append("Drones")

    val (capacityTest, capacityIsValid) = resourceUseText(
        resource = fit.drones.capacity,
        valueToText = { value, withUnits -> value.asDroneCapacity(withUnits = withUnits) }
    )

    val (bandwidthText, bandwidthIsValid) = resourceUseText(
        resource = fit.drones.bandwidth,
        valueToText = { value, withUnits -> value.asDroneBandwidth(withUnits = withUnits) }
    )

    val (droneCountText, droneCountIsValid) = resourceUseText(
        resource = fit.drones.activeCount,
        valueToText = { value, _ -> value.toString() }
    )

    val errorColor = TheorycrafterTheme.colors.base().errorContent
    appendSectionTitleExtraInfo {
        append("$capacityTest capacity".withValidUsageStyle(capacityIsValid, errorColor))
        append("  ")
        append("$bandwidthText bandwidth".withValidUsageStyle(bandwidthIsValid, errorColor))
        append("  ")
        append("$droneCountText active drones".withValidUsageStyle(droneCountIsValid, errorColor))
    }
}


/**
 * Returns the maximum number of drones of the given type can be in a group.
 */
private fun maxDroneGroupSize(fit: Fit, droneType: DroneType): Int {
    return maxDroneGroupSize(
        droneType = droneType,
        fitBandwidth = fit.drones.bandwidth.total,
        fitCapacity = fit.drones.capacity.total,
        maxActiveDrones = fit.drones.activeCount.total
    )
}


/**
 * Remembers and returns the [DroneSlotActions] for the given drone slot.
 */
@Composable
fun rememberDroneSlotActions(
    fit: Fit,
    slotIndex: Int,
    undoRedoQueue: FitEditorUndoRedoQueue,
) = rememberSlotActions(fit, slotIndex, undoRedoQueue) {
    DroneSlotActions(
        fit = { droneType, amount, preserveActiveState, triggerCarouselAnimation ->
            if (stale)
                return@DroneSlotActions

            replaceDroneGroupAction(fit, slotIndex, droneType, amount, preserveActiveState)?.let { replaceDroneGroup ->
                undoRedoQueue.performAndAppend(
                    replaceDroneGroup.withCarouselAnimation(triggerCarouselAnimation)
                )
            }
        },
        replaceDroneAction = { droneType ->
            val droneGroup = fit.drones.all[slotIndex]
            replaceDroneGroupAction(fit, slotIndex, droneType, droneGroup.size, true)
        },
        clear = {
            if (stale)
                return@DroneSlotActions

            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    removeDroneGroupAction(fit, slotIndex),
                    markStaleAction()
                )
            )
        },
        toggleActive = {
            if (stale)
                return@DroneSlotActions
            undoRedoQueue.performAndAppend(
                toggleDroneActiveStateAction(fit, slotIndex)
            )
        },
        addAmount = { addedAmount ->
            if (stale)
                return@DroneSlotActions
            val droneGroup = fit.drones.all[slotIndex]
            val newAmount = droneGroup.size + addedAmount
            if (newAmount >= 1) {
                setDroneGroupSizeAction(fit, slotIndex, newAmount)?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            }
        },
    )
}


/**
 * The section for fitting drones.
 */
@Composable
fun GridScope.DroneSection(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
): Int {
    val droneGroups = fit.drones.all
    if (!fit.drones.canFitDrones && droneGroups.isEmpty())
        return 0

    var rowIndex = firstRowIndex

    SectionTitleRow(
        rowIndex = rowIndex++,
        isFirst = isFirst,
        text = droneSectionTitle(fit),
    )

    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    for ((slotIndex, droneGroup) in droneGroups.withIndex()) {
        inRow(rowIndex++) {
            DroneSlotRow(
                testTag = TestTags.FitEditor.droneRow(slotIndex),
                droneGroup = droneGroup,
                actions = rememberDroneSlotActions(fit, slotIndex, undoRedoQueue),
                slotContentProvider = DroneSlotContent
            )
        }
    }

    // An extra slot where the user can add drones
    inRow(rowIndex = rowIndex++) {
        EmptyDroneSlotRow(
            fit = fit,
            fitDroneGroup = remember(fit, undoRedoQueue) {
                fun (droneType: DroneType, amount: Int) {
                    undoRedoQueue.performAndAppend(
                        addDroneGroupAction(fit, droneType, amount)
                    )
                }
            },
        )
    }

    return rowIndex - firstRowIndex
}
