package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import compose.input.MouseButton
import compose.input.onMousePress
import compose.widgets.GridScope
import compose.widgets.SingleLineText
import eve.data.*
import theorycrafter.fits.FitHandle
import theorycrafter.LocalTheorycrafterWindowManager
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.DamageEffect
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.fitting.setEnabled
import theorycrafter.ui.Icons
import theorycrafter.ui.LocalFitOpener
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*
import kotlin.math.roundToInt


/**
 * The information regarding an incoming damage effect that we remember in order to add and remove it.
 */
private sealed class DamageEffectInfo(
    val index: Int,
    val isEnabled: Boolean,
) {


    /**
     * Returns the [FitHandle] of a fit that needs to be loaded to add the effect.
     * It will be loaded and passed to [addTo].
     */
    open val fitHandleToLoad: FitHandle? = null


    /**
     * Adds the damage effect to the fit.
     */
    abstract fun addTo(scope: FittingEngine.ModificationScope, targetFit: Fit, loadedFit: Fit?)


    /**
     * Removes the damage effect from the fit.
     */
    fun removeFrom(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            fit.removeDamageEffect(fit.incomingDamage.effects[index])
        }
    }

}

/**
 * A [DamageEffectInfo] that has an [Int] amount that can be changed.
 */
private abstract class DamageEffectInfoWithVariableAmount(
    index: Int,
    isEnabled: Boolean,
): DamageEffectInfo(index, isEnabled) {

    /**
     * The amount.
     */
    abstract val amount: Int

    /**
     * Returns a copy of this [FitDamageEffectInfo] with the given amount.
     */
    abstract fun withAmount(amount: Int): DamageEffectInfoWithVariableAmount

}


/**
 * Returns a [DamageEffectInfo] for an existing [DamageEffectInfo] from a fit.
 */
private fun DamageEffect.info(): DamageEffectInfo = when (this) {
    is DamageEffect.Fit -> FitDamageEffectInfo(this)
    is DamageEffect.Ammo -> AmmoDamageEffectInfo(this)
    is DamageEffect.Drone -> DroneDamageEffectInfo(this)
    is DamageEffect.ExactType -> ExactDamageEffectInfo(this)
}


/**
 * The [DamageEffectInfo] for a damage effect from a fit.
 */
private class FitDamageEffectInfo(
    index: Int,
    isEnabled: Boolean,
    val damagingFit: FitHandle,
    override val amount: Int,
): DamageEffectInfoWithVariableAmount(index, isEnabled) {

    constructor(effect: DamageEffect.Fit): this(
        index = effect.target.incomingDamage.effects.indexOf(effect),
        isEnabled = effect.enabled,
        damagingFit = TheorycrafterContext.fits.handleOf(effect.source),
        amount = effect.amount,
    )

    override fun withAmount(amount: Int) = FitDamageEffectInfo(
        index = index,
        isEnabled = isEnabled,
        damagingFit = damagingFit,
        amount = amount,
    )

    override val fitHandleToLoad: FitHandle
        get() = damagingFit

    override fun addTo(
        scope: FittingEngine.ModificationScope,
        targetFit: Fit,
        loadedFit: Fit?
    ) {
        with(scope) {
            val effect = targetFit.addDamageEffect(amount = amount, damagingFit = loadedFit!!, index = index)
            effect.setEnabled(isEnabled)
        }
    }

}


/**
 * The [DamageEffectInfo] for a damage effect from ammo.
 */
private class AmmoDamageEffectInfo(
    index: Int,
    isEnabled: Boolean,
    override val amount: Int,
    val chargeType: ChargeType
): DamageEffectInfoWithVariableAmount(index, isEnabled) {

    constructor(effect: DamageEffect.Ammo): this(
        index = effect.fit.incomingDamage.effects.indexOf(effect),
        isEnabled = effect.enabled,
        amount = effect.amount,
        chargeType = effect.chargeType
    )

    override fun withAmount(amount: Int) = AmmoDamageEffectInfo(
        index = index,
        isEnabled = isEnabled,
        amount = amount,
        chargeType = chargeType,
    )

    override fun addTo(
        scope: FittingEngine.ModificationScope,
        targetFit: Fit,
        loadedFit: Fit?
    ) {
        with(scope) {
            val effect = targetFit.addDamageEffect(
                amount = amount,
                chargeType = chargeType,
                index = index
            )
            effect.setEnabled(isEnabled)
        }
    }
}


/**
 * The [DamageEffectInfo] for a damage effect from drones.
 */
private class DroneDamageEffectInfo(
    index: Int,
    isEnabled: Boolean,
    override val amount: Int,
    val droneType: DroneType
): DamageEffectInfoWithVariableAmount(index, isEnabled) {

    constructor(effect: DamageEffect.Drone): this(
        index = effect.fit.incomingDamage.effects.indexOf(effect),
        isEnabled = effect.enabled,
        amount = effect.droneGroup.size,
        droneType = effect.droneGroup.type
    )

    override fun withAmount(amount: Int) = DroneDamageEffectInfo(
        index = index,
        isEnabled = isEnabled,
        amount = amount,
        droneType = droneType,
    )

    override fun addTo(
        scope: FittingEngine.ModificationScope,
        targetFit: Fit,
        loadedFit: Fit?
    ) {
        with(scope) {
            val effect = targetFit.addDamageEffect(
                amount = amount,
                droneType = droneType,
                index = index
            )
            effect.setEnabled(isEnabled)
        }
    }
}


/**
 * The [DamageEffectInfo] for a damage effect from drones.
 */
private class ExactDamageEffectInfo(
    index: Int,
    isEnabled: Boolean,
    val damageType: DamageType,
    val damage: Int,
): DamageEffectInfoWithVariableAmount(index, isEnabled) {

    constructor(effect: DamageEffect.ExactType): this(
        index = effect.fit.incomingDamage.effects.indexOf(effect),
        isEnabled = effect.enabled,
        damageType = effect.damageType,
        damage = damageAsAmount(effect.damage),
    )

    override val amount: Int
        get() = damage

    override fun withAmount(amount: Int) = ExactDamageEffectInfo(
        index = index,
        isEnabled = isEnabled,
        damageType = damageType,
        damage = amount,
    )

    override fun addTo(
        scope: FittingEngine.ModificationScope,
        targetFit: Fit,
        loadedFit: Fit?
    ) {
        with(scope) {
            val effect = targetFit.addDamageEffect(
                damageType = damageType,
                damage = damage.toDouble(),
                index = index
            )
            effect.setEnabled(isEnabled)
        }
    }
}


private fun damageAsAmount(damage: Double) = damage.roundToInt()


/**
 * A [FitEditingAction] that replaces a damage effect with another.
 */
private class DamageEffectReplacement(
    private val targetFit: Fit,
    private val removed: DamageEffectInfo?,
    private val added: DamageEffectInfo?
): PlainUndoRedoAction() {

    override suspend fun plainPerform() {
        val loadedFit = added?.fitHandleToLoad?.let { TheorycrafterContext.fits.engineFitOf(it) }
        TheorycrafterContext.fits.modifyAndSave {
            removed?.removeFrom(this, targetFit)
            added?.addTo(this, targetFit = targetFit, loadedFit = loadedFit)
        }
    }

    override suspend fun plainRevert() {
        val loadedFit = removed?.fitHandleToLoad?.let { TheorycrafterContext.fits.engineFitOf(it) }
        TheorycrafterContext.fits.modifyAndSave {
            added?.removeFrom(this, targetFit)
            removed?.addTo(this, targetFit = targetFit, loadedFit = loadedFit)
        }
    }

}


private fun actualSlotIndex(fit: Fit, slotIndex: Int?): Int {
    return slotIndex ?: fit.incomingDamage.effects.size
}


private fun currentEffectInfo(fit: Fit, slotIndex: Int?): DamageEffectInfo? {
    return slotIndex?.let { fit.incomingDamage.effects[slotIndex].info() }
}


private fun replaceDamageEffect(
    targetFit: Fit,
    slotIndex: Int?,
    amount: Int,
    damagingFit: FitHandle
): UndoRedoAction<Any?>? {
    val currentEffectInfo = currentEffectInfo(targetFit, slotIndex)
    if (currentEffectInfo is FitDamageEffectInfo) {
        if ((currentEffectInfo.damagingFit == damagingFit) && (currentEffectInfo.amount == amount))
            return null
    }

    return DamageEffectReplacement(
        targetFit = targetFit,
        removed = currentEffectInfo,
        added = FitDamageEffectInfo(
            index = actualSlotIndex(targetFit, slotIndex),
            isEnabled = true,
            damagingFit = damagingFit,
            amount = amount
        ),
    )
}


private fun replaceDamageEffect(
    targetFit: Fit,
    slotIndex: Int?,
    amount: Int,
    chargeType: ChargeType
): UndoRedoAction<Any?>? {
    val currentEffect = slotIndex?.let { targetFit.incomingDamage.effects[it] }
    val currentChargeType = (currentEffect as? DamageEffect.Ammo)?.chargeType
    if ((chargeType == currentChargeType) && (amount == currentEffect.amount))
        return null

    return DamageEffectReplacement(
        targetFit = targetFit,
        removed = currentEffect?.info(),
        added = AmmoDamageEffectInfo(
            index = actualSlotIndex(targetFit, slotIndex),
            isEnabled = true,
            amount = amount,
            chargeType = chargeType
        )
    )
}


private fun replaceDamageEffect(
    targetFit: Fit,
    slotIndex: Int?,
    amount: Int,
    droneType: DroneType
): UndoRedoAction<Any?>? {
    val currentEffect = slotIndex?.let { targetFit.incomingDamage.effects[it] }
    val currentDroneType = (currentEffect as? DamageEffect.Drone)?.droneGroup?.type
    if ((droneType == currentDroneType) && (amount == currentEffect.droneGroup.size))
        return null

    return DamageEffectReplacement(
        targetFit = targetFit,
        removed = currentEffect?.info(),
        added = DroneDamageEffectInfo(
            index = actualSlotIndex(targetFit, slotIndex),
            isEnabled = true,
            amount = amount,
            droneType = droneType
        )
    )
}


private fun replaceDamageEffect(
    targetFit: Fit,
    slotIndex: Int?,
    damageType: DamageType,
    damage: Int,
): UndoRedoAction<Any?>? {
    val currentEffectInfo = currentEffectInfo(targetFit, slotIndex)
    if (currentEffectInfo is ExactDamageEffectInfo) {
        if (currentEffectInfo.damageType == damageType && currentEffectInfo.amount == damage)
            return null
    }

    return DamageEffectReplacement(
        targetFit = targetFit,
        removed = currentEffectInfo,
        added = ExactDamageEffectInfo(
            index = actualSlotIndex(targetFit, slotIndex),
            isEnabled = true,
            damageType = damageType,
            damage = damage,
        )
    )
}


private fun removeDamageEffect(targetFit: Fit, slotIndex: Int?): UndoRedoAction<Any?> {
    return DamageEffectReplacement(
        targetFit = targetFit,
        removed = currentEffectInfo(targetFit, slotIndex),
        added = null,
    )
}


/**
 * Returns a [FitEditingAction] that toggles the enabled state of the damage effect.
 */
private fun toggleEnabledStateAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val effect = fit.incomingDamage.effects[slotIndex]
    val newState = !effect.enabled

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.incomingDamage.effects[slotIndex].setEnabled(newState)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.incomingDamage.effects[slotIndex].setEnabled(!newState)
        }

    }
}


/**
 * Returns a [FitEditingAction] that adds [addAmount] to the amount of a damage effect.
 *
 * [addAmount] can be negative to decrease the amount. The final amount will not be reduced below 1.
 *
 * Note that this doesn't work for a [DamageEffect.ExactType].
 */
private fun increaseDamageEffectAmountByAction(fit: Fit, slotIndex: Int, addAmount: Int): PlainUndoRedoAction? {
    val currentEffect = fit.incomingDamage.effects[slotIndex]
    val currentEffectInfo = currentEffect.info()
    val currentAmount = if (currentEffectInfo is DamageEffectInfoWithVariableAmount)
        currentEffectInfo.amount
    else
        error("Can't adjust amount for $currentEffect")
    val newAmount = (currentAmount + addAmount).coerceAtLeast(1)
    if (newAmount == currentAmount)
        return null

    return DamageEffectReplacement(
        targetFit = fit,
        removed = currentEffectInfo,
        added = currentEffectInfo.withAmount(newAmount),
    )
}


private sealed interface IncomingDamageSelection {

    fun matches(effect: DamageEffect): Boolean

    class Fit(val source: FitHandle): IncomingDamageSelection {
        override fun matches(effect: DamageEffect): Boolean {
            return (effect is DamageEffect.Fit) && (TheorycrafterContext.fits.handleOf(effect.source) == source)
        }
    }

    class Ammo(val chargeType: ChargeType): IncomingDamageSelection {
        override fun matches(effect: DamageEffect): Boolean {
            return (effect is DamageEffect.Ammo) && (effect.chargeType == chargeType)
        }
    }

    class Drone(val droneType: DroneType): IncomingDamageSelection {
        override fun matches(effect: DamageEffect): Boolean {
            return (effect is DamageEffect.Drone) && (effect.droneGroup.type == droneType)
        }
    }

    class ExactDamage(val damageType: DamageType): IncomingDamageSelection {
        override fun matches(effect: DamageEffect): Boolean {
            return (effect is DamageEffect.ExactType) && (effect.damageType == damageType)
        }
    }

}


/**
 * An incoming damage selection widget.
 */
@Composable
private fun GridScope.GridRowScope.IncomingDamageSelectorRow(
    currentEffect: DamageEffect?,
    onFitSelected: (amount: Int, FitHandle) -> Unit,
    onAmmoSelected: (amount: Int, ChargeType) -> Unit,
    onDroneSelected: (amount: Int, DroneType) -> Unit,
    onExactDamageSelected: (amount: Int, DamageType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    var editingAmountOnly by remember { mutableStateOf(false) }
    val autoSuggest = rememberIncomingDamageAutoSuggest(currentEffect)
        .filterResults {
            // When editing only the amount, filter out everything except the current item.
            // This is needed because the autosuggest can match other items even when given the full item name.
            !editingAmountOnly || ((currentEffect != null) && it.matches(currentEffect))
        }

    var amount by remember(currentEffect) {
        val initialAmount = if (currentEffect is DamageEffect.ExactType) damageAsAmount(currentEffect.damage) else 1
        mutableIntStateOf(initialAmount)
    }
    val eveData = TheorycrafterContext.eveData

    val currentEffectName = remember(currentEffect) {
        when (currentEffect) {
            is DamageEffect.Fit -> TheorycrafterContext.fits.handleOf(currentEffect.source).name
            is DamageEffect.Ammo -> currentEffect.chargeType.name
            is DamageEffect.Drone -> currentEffect.droneGroup.type.name
            is DamageEffect.ExactType -> currentEffect.damageType.displayName
            else -> null
        }
    }

    SelectorRow(
        onValueChange = {
            val parsedItem = parseItemWithAmountText(currentEffectName, it)
            amount = parsedItem.amount
            editingAmountOnly = parsedItem.hasAmountOnly
        },
        onItemSelected = {
            when (it) {
                is IncomingDamageSelection.Fit -> onFitSelected(amount, it.source)
                is IncomingDamageSelection.Ammo -> onAmmoSelected(amount, it.chargeType)
                is IncomingDamageSelection.Drone -> onDroneSelected(amount, it.droneType)
                is IncomingDamageSelection.ExactDamage -> onExactDamageSelected(amount, it.damageType)
            }
        },
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        autoSuggestItemToString = {
            // This is never displayed, but when the user selects an item, the result of this function is passed to
            // onValueChange, so we must return something that parses correctly
            "Something".withAmount(amount)
        },
        autoSuggestInputTransform = { parseItemWithAmountText(currentEffectName, it).itemName },
        suggestedItemContent = { selection, _ ->
            when (selection) {
                is IncomingDamageSelection.Fit -> {
                    val fitHandle = selection.source
                    val shipType = eveData.shipType(fitHandle.shipTypeId)
                    DefaultSuggestedEveItemTypeIcon(shipType)
                    Text(text = "${fitHandle.name} (${shipType.name})".withAmount(amount))
                }
                is IncomingDamageSelection.Ammo -> {
                    val chargeType = selection.chargeType
                    DefaultSuggestedEveItemTypeIcon(chargeType)
                    Text(text = chargeType.name.withAmount(amount))
                }
                is IncomingDamageSelection.Drone -> {
                    val droneType = selection.droneType
                    DefaultSuggestedEveItemTypeIcon(droneType)
                    Text(text = droneType.name.withAmount(amount))
                }
                is IncomingDamageSelection.ExactDamage -> {
                    val damageType = selection.damageType
                    DefaultSuggestedIconContainer {
                        Icons.DamageType(selection.damageType)
                    }
                    Text(text = damageType.displayName.withAmount(amount))
                }
            }
        },
        hint = "Amount x (damage type, fit, ammo or drone name)",
    )
}


/**
 * Returns an [AutoSuggest] for incoming damage.
 */
@Composable
private fun rememberIncomingDamageAutoSuggest(currentEffect: DamageEffect?): AutoSuggest<IncomingDamageSelection> {
    val remoteFitsAutoSuggest = TheorycrafterContext.fitsAutoSuggest
    val ammoAutoSuggest = TheorycrafterContext.autoSuggest.ammo
    val droneAutoSuggest = TheorycrafterContext.autoSuggest.damageDrones
    val damageTypeAutoSuggest = remember {
        autoSuggest(
            items = DamageType.entries,
            itemToSearchString = DamageType::displayName,
            minCharacters = 1,  // To match the others
            suggestionComparator = compareBy { it.ordinal }
        )
    }
    val damageTypeSelections = remember {
        DamageType.entries.map { IncomingDamageSelection.ExactDamage(it) }
    }

    return remember(currentEffect, remoteFitsAutoSuggest, ammoAutoSuggest, droneAutoSuggest) {
        AutoSuggest { query ->
            if (query.isBlank() && ((currentEffect == null) || (currentEffect is DamageEffect.ExactType)))
                return@AutoSuggest damageTypeSelections

            val fits = remoteFitsAutoSuggest(query)
            val ammo = ammoAutoSuggest(query)
            val drones = droneAutoSuggest(query)
            val damageTypes = damageTypeAutoSuggest(query)

            if ((fits == null) && (ammo == null) && (drones == null) && (damageTypes == null))
                return@AutoSuggest null

            (damageTypes?.map(IncomingDamageSelection::ExactDamage) ?: emptyList()) +
                    interleave(
                        fits?.map(IncomingDamageSelection::Fit) ?: emptyList(),
                        ammo?.map(IncomingDamageSelection::Ammo) ?: emptyList(),
                        drones?.map(IncomingDamageSelection::Drone) ?: emptyList(),
                    )
        }
    }
}


/**
 * The row displaying a non-empty incoming damage slot.
 */
@Composable
private fun GridScope.IncomingDamageSlotRow(
    testTag: String,
    damageEffect: DamageEffect,
    actions: IncomingDamageSlotActions
) {
    CompositionCounters.recomposed(testTag)

    val clipboard = LocalClipboard.current
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    val windowManager = LocalTheorycrafterWindowManager.current
    val fit = LocalFit.current
    val fitOpener = LocalFitOpener.current
    val contextActions = remember(damageEffect, actions, clipboard, undoRedoQueue, windowManager, fit, fitOpener) {
        buildList {
            if (damageEffect is DamageEffect.Fit) {
                add(SlotContextAction.openFit(fitOpener, damageEffect.source))
            }
            add(SlotContextAction.Separator)
            add(SlotContextAction.clear(actions.clear))
            add(SlotContextAction.addOneItem(actions.addAmount))
            add(SlotContextAction.removeOneItem(actions.addAmount, showInContextMenu = true))
            add(SlotContextAction.Separator)
            add(SlotContextAction.toggleEnabledState(actions.toggleEnabled))
        }
    }
    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            IncomingDamageSelectorRow(
                currentEffect = damageEffect,
                onFitSelected = { amount, fitHandle ->
                    actions.setDamageFit(amount, fitHandle)
                    onEditingCompleted()
                },
                onAmmoSelected = { amount, chargeType ->
                    actions.setDamageAmmo(amount, chargeType)
                    onEditingCompleted()
                },
                onDroneSelected = { amount, droneType ->
                    actions.setDamageDrone(amount, droneType)
                    onEditingCompleted()
                },
                onExactDamageSelected = { amount, damageType ->
                    actions.setExactDamage(damageType, damage = amount)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        IncomingDamageSlotContent(
            damageEffect = damageEffect,
            onToggleEnabled = actions.toggleEnabled,
        )
    }
}


/**
 * The row displaying the "Empty Incoming Damage Slot".
 */
@Composable
private fun GridScope.EmptyIncomingDamageSlotRow(
    actions: IncomingDamageSlotActions
) {
    val clipboard = LocalClipboard.current
    val contextActions = remember(clipboard, actions) {
        listOf<SlotContextAction>(

        )
    }

    SlotRow(
        modifier = Modifier
            .testTag(TestTags.FitEditor.EmptyIncomingDamageRow),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            IncomingDamageSelectorRow(
                currentEffect = null,
                onFitSelected = { fitHandle, amount ->
                    actions.setDamageFit(fitHandle, amount)
                    onEditingCompleted()
                },
                onAmmoSelected = { amount, moduleType ->
                    actions.setDamageAmmo(amount, moduleType)
                    onEditingCompleted()
                },
                onDroneSelected = { amount, droneType ->
                    actions.setDamageDrone(amount, droneType)
                    onEditingCompleted()
                },
                onExactDamageSelected = { amount, damageType ->
                    actions.setExactDamage(damageType, damage = amount)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyRowContent(text = "Empty damage slot")
    }
}


/**
 * The row for a slot with a (non-`null`) incoming damage effect.
 */
@Composable
private fun GridScope.GridRowScope.IncomingDamageSlotContent(
    damageEffect: DamageEffect,
    onToggleEnabled: () -> Unit,
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        Icons.ItemEnabledState(
            enabled = damageEffect.enabled,
            modifier = Modifier
                .onMousePress(consumeEvent = true) {  // Consume to prevent selecting the row
                    onToggleEnabled()
                }
                .onMousePress(MouseButton.Middle, consumeEvent = true) {  // Consume just in case
                    onToggleEnabled()
                }
        )
    }

    when (damageEffect) {
        is DamageEffect.Fit -> {
            cell(cellIndex = GridCols.TYPE_ICON) {
                TypeIconCellContent(item = damageEffect.source.ship)
            }
            cell(cellIndex = GridCols.NAME) {
                val damagingFitHandle = remember(damageEffect) {
                    TheorycrafterContext.fits.handleOf(damageEffect.source)
                }
                val shipType = remember(damagingFitHandle) {
                    TheorycrafterContext.eveData.shipType(damagingFitHandle.shipTypeId)
                }
                SingleLineText("${damagingFitHandle.name} (${shipType.name})".withAmount(damageEffect.amount))
            }
        }
        is DamageEffect.Ammo -> {
            cell(cellIndex = GridCols.TYPE_ICON) {
                TypeIconCellContent(damageEffect.chargeType)
            }
            cell(cellIndex = GridCols.NAME) {
                SingleLineText(damageEffect.chargeType.name.withAmount(damageEffect.amount))
            }
        }
        is DamageEffect.Drone -> {
            cell(cellIndex = GridCols.TYPE_ICON) {
                TypeIconCellContent(damageEffect.droneGroup)
            }
            cell(cellIndex = GridCols.NAME) {
                SingleLineText(damageEffect.droneGroup.name.withAmount(damageEffect.droneGroup.size))
            }
        }
        is DamageEffect.ExactType -> {
            cell(cellIndex = GridCols.TYPE_ICON) {
                Icons.DamageType(damageEffect.damageType)
            }
            cell(cellIndex = GridCols.NAME) {
                SingleLineText(damageEffect.damageType.displayName.withAmount(damageAsAmount(damageEffect.damage)))
            }
        }
    }

    cell(
        cellIndex = GridCols.NAME + 1,
        colSpan = GridCols.LAST - GridCols.NAME - 1,
        contentAlignment = Alignment.CenterEnd,
    ) {
        if (damageEffect.enabled) {
            DamagePatternWidget(damageEffect.damagePattern)
        }
    }
}


/**
 * The widget displaying the incoming damage pattern.
 */
@Composable
private fun DamagePatternWidget(damagePattern: DamagePattern) {
    Row(horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxsmall)) {
        damagePattern.forEach { damageType, damage ->
            val colors = TheorycrafterTheme.colors.resistanceIndicatorColors(damageType)
            val sizeModifier = Modifier.size(
                width = 60.dp,
                height = TheorycrafterTheme.sizes.fitEditorSlotRowHeight * 0.8f
            )
            ColoredTextBox(
                text = damage.asDps(withUnits = false),
                background = colors.filled,
                textStyle = TheorycrafterTheme.textStyles.percentageView,
                textColor = colors.textColor,
                textShadowColor = colors.textShadowColor,
                modifier = sizeModifier
            )
        }
    }
}


/**
 * Bundles the actions passed to [IncomingDamageSlotRow].
 */
private class IncomingDamageSlotActions(
    private val setFromFit: (amount: Int, FitHandle) -> Unit,
    private val setFromDrone: (amount: Int, DroneType) -> Unit,
    private val setFromAmmo: (amount: Int, ChargeType) -> Unit,
    private val setFromExactDamage: (DamageType, damage: Int) -> Unit,
    val addAmount: (amount: Int) -> Unit,
    val clear: () -> Unit,
    val toggleEnabled: () -> Unit,
) {

    fun setDamageFit(amount: Int, fitHandle: FitHandle) {
        setFromFit(amount, fitHandle)
    }

    fun setDamageAmmo(amount: Int, chargeType: ChargeType) {
        setFromAmmo(amount, chargeType)
    }

    fun setDamageDrone(amount: Int, droneType: DroneType) {
        setFromDrone(amount, droneType)
    }

    fun setExactDamage(damageType: DamageType, damage: Int) {
        setFromExactDamage(damageType, damage)
    }

}


@Composable
private fun rememberIncomingDamageSlotActions(
    targetFit: Fit,
    slotIndex: Int?,
    undoRedoQueue: FitEditorUndoRedoQueue
): IncomingDamageSlotActions {
    return rememberSlotActions(targetFit, slotIndex, undoRedoQueue) {
        IncomingDamageSlotActions(
            setFromFit = { amount, damagingFit ->
                if (stale)
                    return@IncomingDamageSlotActions

                replaceDamageEffect(
                    targetFit = targetFit,
                    slotIndex = slotIndex,
                    amount = amount,
                    damagingFit = damagingFit
                )?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            },
            setFromDrone = { amount, droneType ->
                if (stale)
                    return@IncomingDamageSlotActions

                replaceDamageEffect(
                    targetFit = targetFit,
                    slotIndex = slotIndex,
                    amount = amount,
                    droneType = droneType,
                )?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            },
            setFromAmmo = { amount, chargeType ->
                if (stale)
                    return@IncomingDamageSlotActions

                replaceDamageEffect(
                    targetFit = targetFit,
                    slotIndex = slotIndex,
                    amount = amount,
                    chargeType = chargeType,
                )?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            },
            setFromExactDamage = { damageType, damage ->
                if (stale)
                    return@IncomingDamageSlotActions

                replaceDamageEffect(
                    targetFit = targetFit,
                    slotIndex = slotIndex,
                    damageType = damageType,
                    damage = damage,
                )?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            },
            addAmount = { amount ->
                if (stale)
                    return@IncomingDamageSlotActions

                increaseDamageEffectAmountByAction(
                    fit = targetFit,
                    slotIndex = slotIndex!!,
                    addAmount = amount,
                )?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            },
            clear = {
                if (stale)
                    return@IncomingDamageSlotActions

                require (slotIndex != null) { "Can't remove a non-existing damage effect"}
                undoRedoQueue.performAndAppend(
                    removeDamageEffect(targetFit, slotIndex)
                )
            },
            toggleEnabled = {
                if (stale)
                    return@IncomingDamageSlotActions

                require (slotIndex != null) { "Can't toggle a non-existing damage effect"}
                undoRedoQueue.performAndAppend(
                    toggleEnabledStateAction(targetFit, slotIndex)
                )
            }
        )
    }
}


/**
 * The section for specifying incoming damage.
 */
@Composable
fun GridScope.IncomingDamage(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
): Int {
    var rowIndex = firstRowIndex

    SectionTitleRow(
        rowIndex = rowIndex++,
        isFirst = isFirst,
        text = AnnotatedString("Incoming Damage"),
    )

    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    for ((slotIndex, damageEffect) in fit.incomingDamage.effects.withIndex()) {
        inRow(rowIndex++) {
            IncomingDamageSlotRow(
                testTag = TestTags.FitEditor.incomingDamageRow(slotIndex),
                damageEffect = damageEffect,
                actions = rememberIncomingDamageSlotActions(fit, slotIndex, undoRedoQueue)
            )
        }
    }

    // An extra slot where the user can add items
    inRow(rowIndex++) {
        EmptyIncomingDamageSlotRow(
            actions = rememberIncomingDamageSlotActions(fit, slotIndex = null, undoRedoQueue)
        )
    }

    return rowIndex - firstRowIndex
}