/**
 * The implementation of base functionality for remote (command, hostile and friendly) fits.
 */

package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import compose.input.MouseButton
import compose.input.onMousePress
import compose.utils.HSpacer
import compose.utils.VerticallyCenteredRow
import compose.widgets.GridScope
import compose.widgets.SingleLineText
import eve.data.*
import theorycrafter.FitHandle
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.*
import theorycrafter.ui.FitOpener
import theorycrafter.ui.Icons
import theorycrafter.ui.LocalFitOpener
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.effectcolumn.displayedRemoteDroneEffect
import theorycrafter.ui.fiteditor.effectcolumn.displayedRemoteModuleEffect
import theorycrafter.ui.widgets.LocalStandardDialogs
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*


/**
 * The information regarding a remote effect that we remember in order to fit it.
 */
class RemoteEffectInfo(
    val fitHandle: FitHandle,
    val isEnabled: Boolean
)


/**
 * Implements the actions of managing a type of remote effects (e.g. command, hostile, friendly) on a fit.
 */
abstract class RemoteEffectFitActions {


    /**
     * Returns the fit's current [RemoteEffect]s of the appropriate type.
     */
    abstract val Fit.effects: List<RemoteEffect>


    /**
     * Adds a remote effect to the target fit.
     */
    abstract fun FittingEngine.ModificationScope.addRemoteEffect(
        targetFit: Fit,
        sourceFit: Fit,
        slotIndex: Int
    ): RemoteEffect


    /**
     * Removes a remote effect from the target fit.
     */
    abstract fun FittingEngine.ModificationScope.removeRemoteEffect(remoteEffect: RemoteEffect)


    /**
     * Returns an [UndoRedoAction] that adds a [RemoteEffect] to the fit.
     */
    fun addRemoteEffectAction(fit: Fit, sourceFitHandle: FitHandle): PlainUndoRedoAction {
        return RemoteEffectReplacement(
            fit = fit,
            slotIndex = fit.effects.size,
            removed = null,
            added = RemoteEffectInfo(sourceFitHandle, isEnabled = true)
        )
    }


    /**
     * Returns an [UndoRedoAction] that removes a [RemoteEffect] fit.
     */
    fun removeRemoteFitEffectAction(fit: Fit, slotIndex: Int): PlainUndoRedoAction {
        val currentEffect = fit.effects[slotIndex]
        val currentSourceFitHandle = TheorycrafterContext.fits.handleOf(currentEffect.source)

        return RemoteEffectReplacement(
            fit = fit,
            slotIndex = slotIndex,
            removed = RemoteEffectInfo(currentSourceFitHandle, currentEffect.enabledState.value),
            added = null
        )
    }


    /**
     * Returns an [UndoRedoAction] that toggles the enabled state of the [RemoteEffect].
     */
    fun toggleRemoteFitEffectEnabledAction(fit: Fit, slotIndex: Int): PlainUndoRedoAction {
        val remoteEffect = fit.effects[slotIndex]
        val newState = !remoteEffect.enabledState.value

        return object: PlainUndoRedoAction() {

            override suspend fun plainPerform() {
                TheorycrafterContext.fits.modifyAndSave {
                    fit.effects[slotIndex].setEnabled(newState)
                }
            }

            override suspend fun plainRevert() {
                TheorycrafterContext.fits.modifyAndSave {
                    fit.effects[slotIndex].setEnabled(!newState)
                }
            }

        }
    }


    /**
     * The [UndoRedoAction] for replacing a remote effect with another.
     */
    private inner class RemoteEffectReplacement(
        private val fit: Fit,
        private val slotIndex: Int,
        private val removed: RemoteEffectInfo?,
        private val added: RemoteEffectInfo?
    ): PlainUndoRedoAction() {


        /**
         * Performs the replacement on the given fit.
         */
        override suspend fun plainPerform() {
            val addedFit = added?.let { TheorycrafterContext.fits.engineFitOf(it.fitHandle) }
            TheorycrafterContext.fits.modifyAndSave {
                if (removed != null) {
                    val effect = fit.effects[slotIndex]
                    removeRemoteEffect(effect)
                }
                if (added != null) {
                    val effect = addRemoteEffect(targetFit = fit, sourceFit = addedFit!!, slotIndex)
                    effect.setEnabled(added.isEnabled)
                }
            }
        }


        /**
         * Reverts the replacement on the given fit.
         */
        override suspend fun plainRevert() {
            val removedFit = removed?.let { TheorycrafterContext.fits.engineFitOf(it.fitHandle) }
            TheorycrafterContext.fits.modifyAndSave {
                if (added != null) {
                    val effect = fit.effects[slotIndex]
                    removeRemoteEffect(effect)
                }
                if (removed != null) {
                    val effect = addRemoteEffect(targetFit = fit, sourceFit = removedFit!!, slotIndex)
                    effect.setEnabled(removed.isEnabled)
                }
            }
        }


    }


}


/**
 * The information about a remote module effect we keep in order to add it.
 */
private class RemoteModuleEffectInfo(
    val moduleType: ModuleType,
    val chargeType: ChargeType?,
    val state: Module.State,
)


/**
 * The exception thrown when there is no room to add a module effect.
 */
class NoRoomForModuleEffectException(val slotIndex: Int): Exception()


/**
 * The [FitEditingAction] for replacing a remote module effect with another.
 */
private class ModuleEffectReplacement(
    private val fit: Fit,
    private val slotType: ModuleSlotType,
    private val slotIndex: Int,
    private val removed: RemoteModuleEffectInfo?,
    private val added: RemoteModuleEffectInfo?
): FitEditingAction() {


    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.performEdit() {
        if (removed != null) {
            val module = fit.auxiliaryFit!!.modules.inSlot(slotType, slotIndex)!!
            fit.removeModuleEffect(module)
        }
        if (added != null) {
            // We checked whether there's room before this instance was created, but just in case...
            val module = fit.addModuleEffect(moduleType = added.moduleType, index = slotIndex)
            if (module == null)
                throw NoRoomForModuleEffectException(slotIndex)
            if (added.chargeType != null)
                module.setCharge(added.chargeType)
            module.setState(added.state)
        }
    }


    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.revertEdit() {
        if (added != null) {
            val module = fit.auxiliaryFit!!.modules.inSlot(slotType, slotIndex)!!
            fit.removeModuleEffect(module)
        }
        if (removed != null) {
            // When reverting an action, we should always have room to add the module, hence the !!
            val module = fit.addModuleEffect(moduleType = removed.moduleType, index = slotIndex)!!
            if (removed.chargeType != null)
                module.setCharge(removed.chargeType)
            module.setState(removed.state)
        }
    }


}


/**
 * Returns the initial state of a newly fitted module effect.
 */
private fun initialModuleState(moduleType: ModuleType): Module.State {
    return if (moduleType.isActivable)
        Module.State.ACTIVE
    else
        Module.State.ONLINE
}


/**
 * Returns a [FitEditingAction] that adds a module effect to the fit.
 */
private fun addModuleEffectAction(fit: Fit, moduleType: ModuleType): FitEditingAction {
    val moduleSlots = fit.auxiliaryFit?.modules?.slotsInRack(moduleType.slotType)?.toList()
    val slotIndex = moduleSlots?.indexOf(null) ?: 0
    if (slotIndex == -1)
        throw NoRoomForModuleEffectException(moduleSlots!!.size)

    return ModuleEffectReplacement(
        fit = fit,
        slotType = moduleType.slotType,
        slotIndex = slotIndex,
        removed = null,
        added = RemoteModuleEffectInfo(
            moduleType = moduleType,
            chargeType = preloadedCharge(fit, moduleType),
            state = initialModuleState(moduleType)
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes a module effect from the fit.
 */
private fun removeModuleEffectAction(fit: Fit, module: Module): FitEditingAction {
    return ModuleEffectReplacement(
        fit = fit,
        slotType = module.type.slotType,
        // We can remember the slot index here because we assume the state
        // of the fit will be the same when the action is undone/redone.
        slotIndex = module.indexInRack(),
        removed = RemoteModuleEffectInfo(
            moduleType = module.type,
            chargeType = module.loadedCharge?.type,
            state = module.state
        ),
        added = null
    )
}


/**
 * Returns an [FitEditingAction] that replaces an existing module effect in the fit with another.
 */
private fun replaceModuleEffectAction(
    fit: Fit,
    currentModule: Module,
    newModuleType: ModuleType,
    keepState: Boolean
): FitEditingAction? {
    if (currentModule.type == newModuleType)
        return null
    val newModuleState = if (keepState) currentModule.state else initialModuleState(newModuleType)
    val newChargeType = preloadedChargeWhenReplacingModule(currentModule.fit, newModuleType, currentModule)

    return ModuleEffectReplacement(
        fit = fit,
        slotType = currentModule.type.slotType,
        slotIndex = currentModule.indexInRack(),
        removed = RemoteModuleEffectInfo(
            moduleType = currentModule.type,
            chargeType = currentModule.loadedCharge?.type,
            state = currentModule.state
        ),
        added = RemoteModuleEffectInfo(
            moduleType = newModuleType,
            chargeType = newChargeType,
            state = newModuleState
        )
    )
}


/**
 * Returns an action that toggles a module's primary state.
 */
private fun toggleModulePrimaryStateAction(module: Module) =
    toggleModulePrimaryStateAction(
        fit = module.fit,
        slotGroup = SingleModuleSlot(module.fit, module.type.slotType, module.indexInRack())
    )


/**
 * Returns an action that toggles a module's overload state.
 */
private fun toggleModuleOverloadStateAction(module: Module) =
    toggleModuleOverloadStateAction(
        fit = module.fit,
        slotGroup = SingleModuleSlot(module.fit, module.type.slotType, module.indexInRack())
    )


/**
 * The information about a remote drone effect we keep in order to add it.
 */
class RemoteDroneEffectInfo(
    val droneType: DroneType,
    val size: Int,
    val isActive: Boolean
)


/**
 * The [FitEditingAction] for replacing a remote drone effect with another.
 */
class DroneEffectReplacement(
    private val fit: Fit,
    private val slotIndex: Int,
    private val removed: RemoteDroneEffectInfo?,
    private val added: RemoteDroneEffectInfo?
): FitEditingAction() {


    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.performEdit() {
        if (removed != null) {
            val droneGroup = fit.auxiliaryFit!!.drones.all[slotIndex]
            fit.removeDroneEffect(droneGroup)
        }
        if (added != null) {
            val droneGroup = fit.addDroneEffect(added.droneType, size = added.size, index = slotIndex)
            droneGroup.setActive(added.isActive)
        }
    }


    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.revertEdit() {
        if (added != null) {
            val droneGroup = fit.auxiliaryFit!!.drones.all[slotIndex]
            fit.removeDroneEffect(droneGroup)
        }
        if (removed != null) {
            val droneGroup = fit.addDroneEffect(removed.droneType, size = removed.size, index = slotIndex)
            droneGroup.setActive(removed.isActive)
        }
    }


}


/**
 * Returns a [FitEditingAction] that adds a drone effect to the fit.
 */
private fun addDroneEffectAction(fit: Fit, droneType: DroneType, amount: Int): FitEditingAction {
    val fittedDrones = fit.auxiliaryFit?.drones?.all
    return DroneEffectReplacement(
        fit = fit,
        slotIndex = fittedDrones?.size ?: 0,
        removed = null,
        added = RemoteDroneEffectInfo(droneType, size = amount, isActive = true)
    )
}


/**
 * Returns a [FitEditingAction] that removes a drone effect from the fit.
 */
private fun removeDroneEffectAction(fit: Fit, droneGroup: DroneGroup): FitEditingAction {
    return DroneEffectReplacement(
        fit = fit,
        slotIndex = droneGroup.indexInFit(),
        removed = RemoteDroneEffectInfo(droneGroup.type, size = droneGroup.size, isActive = droneGroup.active),
        added = null
    )
}


/**
 * Returns a [FitEditingAction] that replaces an existing drone effect with another.
 */
private fun replaceDroneEffectAction(
    fit: Fit,
    currentDroneGroup: DroneGroup,
    newDroneType: DroneType,
    size: Int,
    keepActiveState: Boolean
): FitEditingAction {
    return DroneEffectReplacement(
        fit = fit,
        slotIndex = currentDroneGroup.indexInFit(),
        removed = RemoteDroneEffectInfo(
            droneType = currentDroneGroup.type,
            size = currentDroneGroup.size,
            isActive = currentDroneGroup.active
        ),
        added = RemoteDroneEffectInfo(
            droneType = newDroneType,
            size = size,
            isActive = if (keepActiveState) currentDroneGroup.active else true
        )
    )
}


/**
 * The types of remote effects the user can apply to the fit.
 */
sealed interface RemoteEffectSelection {


    /**
     * A remote fit effect.
     */
    class Fit(val fitHandle: FitHandle): RemoteEffectSelection


    /**
     * A remote module effect.
     */
    class Module(val moduleType: ModuleType): RemoteEffectSelection


    /**
     * A remote drone effect.
     */
    class Drone(val droneType: DroneType, val amount: Int): RemoteEffectSelection


}


/**
 * A remote effect selection widget.
 */
@Composable
fun <T: RemoteEffectSelection> GridScope.GridRowScope.RemoteEffectSelectorRow(
    hint: String,
    autoSuggest: AutoSuggest<T>,
    onRemoteEffectSelected: (T) -> Unit,
    onEditingCancelled: () -> Unit,
) {
    emptyCell(cellIndex = GridCols.STATE_ICON)
    emptyCell(cellIndex = GridCols.TYPE_ICON)
    cell(
        cellIndex = GridCols.NAME,
        modifier = Modifier.weight(1f)
    ) {
        val eveData = TheorycrafterContext.eveData
        Selector(
            onItemSelected = onRemoteEffectSelected,
            onEditingCancelled = onEditingCancelled,
            autoSuggest = autoSuggest,
            suggestedItemContent = { selection, _ ->
                when (selection) {
                    is RemoteEffectSelection.Fit -> {
                        val shipType = eveData.shipType(selection.fitHandle.shipTypeId)
                        DefaultSuggestedEveItemTypeIcon(shipType)
                        Text(text = "${selection.fitHandle.name} (${shipType.name})")
                    }
                    is RemoteEffectSelection.Module -> {
                        DefaultSuggestedEveItemTypeIcon(selection.moduleType)
                        Text(text = selection.moduleType.name)
                    }
                    is RemoteEffectSelection.Drone -> {
                        DefaultSuggestedEveItemTypeIcon(selection.droneType)
                        Text(text = droneGroupDisplayText(selection.amount, selection.droneType))
                    }
                }
            },
            autoSuggestHorizontalAnchorPadding = AutoSuggestHorizontalAnchorPaddingWithIcon,
            hint = hint
        )
    }
}


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

    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(item = remoteEffect.source.ship)
    }

    cell(cellIndex = GridCols.NAME, colSpan = 5) {
        val sourceFitHandle = remember(remoteEffect) {
            TheorycrafterContext.fits.handleOf(remoteEffect.source)
        }
        val shipType = remember(sourceFitHandle) {
            TheorycrafterContext.eveData.shipType(sourceFitHandle.shipTypeId)
        }
        SingleLineText("${sourceFitHandle.name} (${shipType.name})")
    }
}


/**
 * Bundles the actions passed to [RemoteFitEffectSlotRow].
 */
class RemoteFitEffectSlotActions(
    val clear: () -> Unit,
    val toggleEnabled: () -> Unit,
)


/**
 * The slot action to show the source of a [RemoteEffect] in a new window.
 */
private fun SlotContextAction.Companion.openSourceFit(fitOpener: FitOpener, remoteEffect: RemoteEffect) =
    SlotContextAction(
        displayName = "Open in Window",
        icon = { Icons.OpenInNew() },
        shortcuts = FitEditorKeyShortcuts.OpenRemoteEffectSource,
        action = {
            val fitHandle = TheorycrafterContext.fits.handleOf(remoteEffect.source)
            fitOpener.openFitInSecondaryWindow(fitHandle)
        }
    )


/**
 * The row displaying a non-empty remote fit effect slot.
 */
@Composable
private fun GridScope.RemoteFitEffectSlotRow(
    testTag: String,
    remoteEffect: RemoteEffect,
    actions: RemoteFitEffectSlotActions,
) {
    CompositionCounters.recomposed(testTag)

    val fitOpener = LocalFitOpener.current
    val contextActions = remember(actions, fitOpener) {
        buildList(3) {
            add(SlotContextAction.openSourceFit(fitOpener, remoteEffect))
            add(SlotContextAction.clear(actions.clear))
            add(SlotContextAction.toggleEnabledState(actions.toggleEnabled))
        }
    }

    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        contextActions = contextActions,
        editedRowContent = null
    ) {
        RemoteFitEffectSlotContent(
            remoteEffect = remoteEffect,
            toggleEnabled = actions.toggleEnabled
        )
    }
}


/**
 * Provides the content of the remote effect slot row when the value is being edited.
 */
@Stable
fun interface EditedRemoteEffectRow<out T: RemoteEffectSelection> {

    @Composable
    fun content(
        scope: GridScope.GridRowScope,
        onRemoteEffectSelected: (T) -> Unit,
        onEditingCancelled: () -> Unit
    )

}


/**
 * The row displaying an empty remote effect slot.
 */
@Composable
private fun GridScope.EmptyRemoteEffectSlotRow(
    testTag: String,
    text: String,
    remoteEffectSelectionFromClipboardText: EveData.(String) -> RemoteEffectSelection?,
    setRemoteEffect: (RemoteEffectSelection) -> Unit,
    editedRemoteEffectRow: EditedRemoteEffectRow<RemoteEffectSelection>
) {
    val clipboardManager = LocalClipboardManager.current
    val contextActions = remember(remoteEffectSelectionFromClipboardText, clipboardManager) {
        listOf(
            SlotContextAction.pasteFromClipboard(
                clipboardManager = clipboardManager,
                itemFromClipboardText = remoteEffectSelectionFromClipboardText,
                pasteItem = setRemoteEffect
            )
        )
    }

    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            editedRemoteEffectRow.content(
                scope = this,
                onRemoteEffectSelected = {
                    setRemoteEffect(it)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted,
            )
        }
    ) {
        EmptyRowContent(text)
    }
}


/**
 * Remembers and returns the [RemoteFitEffectSlotActions] for the given remote effect slot.
 */
@Composable
fun rememberRemoteFitEffectSlotActions(
    actions: RemoteEffectFitActions,
    fit: Fit,
    slotIndex: Int,
    undoRedoQueue: FitEditorUndoRedoQueue,
) = rememberSlotActions(actions, fit, slotIndex, undoRedoQueue) {
    RemoteFitEffectSlotActions(
        clear = {
            if (stale)
                return@RemoteFitEffectSlotActions
            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    actions.removeRemoteFitEffectAction(fit, slotIndex),
                    markStaleAction()
                )
            )
        },
        toggleEnabled = {
            if (stale)
                return@RemoteFitEffectSlotActions
            undoRedoQueue.performAndAppend(
                actions.toggleRemoteFitEffectEnabledAction(fit, slotIndex)
            )
        },
    )
}


/**
 * A [ModuleSlotContentProvider] that provides [AffectingModuleSlotContent].
 */
@Stable
private fun AffectingModuleSlotContent(remoteEffect: RemoteEffect) = ModuleSlotContentProvider {
        scope: GridScope.GridRowScope,
        module: Module,
        _: Int?,
        carousel: Carousel<ModuleType>,
        togglePrimaryState: () -> Unit,
        toggleOverloaded: () -> Unit,
        toggleOnline: () -> Unit,
        showSpoolupCyclesSelector: () -> Unit,
        showAdaptationCyclesSelector: () -> Unit,
    ->
    with(scope) {
        AffectingModuleSlotContent(
            module = module,
            carousel = carousel,
            remoteEffect = remoteEffect,
            togglePrimaryState = togglePrimaryState,
            toggleOverloaded = toggleOverloaded,
            toggleOnline = toggleOnline,
            showSpoolupCyclesSelector = showSpoolupCyclesSelector,
            showAdaptationCyclesSelector = showAdaptationCyclesSelector,
        )
    }
}


/**
 * The slot content for an affecting module of a remote fit.
 */
@Composable
private fun GridScope.GridRowScope.AffectingModuleSlotContent(
    module: Module,
    carousel: Carousel<ModuleType>,
    remoteEffect: RemoteEffect,
    togglePrimaryState: () -> Unit,
    toggleOverloaded: () -> Unit,
    toggleOnline: () -> Unit,
    showSpoolupCyclesSelector: () -> Unit,
    showAdaptationCyclesSelector: () -> Unit,
) {
    emptyCell(cellIndex = GridCols.STATE_ICON)
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(module)
    }
    cell(cellIndex = GridCols.NAME, colSpan = GridCols.EFFECT - GridCols.NAME) {
        ProvideTextStyle(
            TheorycrafterTheme.textStyles.fitEditorSecondarySlot
        ) {
            VerticallyCenteredRow(modifier = Modifier.fillMaxWidth()) {
                ModuleStateIcon(
                    module = module,
                    modifier = Modifier.scale(0.8f).wrapContentSize(),
                    togglePrimaryState = togglePrimaryState,
                    toggleOverloaded = toggleOverloaded,
                    toggleOnline = toggleOnline
                )
                CarouselSlotContent(
                    carousel = carousel,
                    targetState = module.type,
                    modifier = Modifier.align(Alignment.CenterStart).fillMaxWidth(),
                    text = { it.name },
                    extraContent = {
                        if (module.spoolupCycles != null) {
                            HSpacer(TheorycrafterTheme.spacing.medium)
                            SpoolupCyclesIndicator(module, showSpoolupCyclesSelector)
                        }
                        if (module.adaptationCycles != null) {
                            HSpacer(TheorycrafterTheme.spacing.medium)
                            AdaptationCyclesIndicator(module, showAdaptationCyclesSelector)
                        }
                    }
                )
            }
        }
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedRemoteModuleEffect(remoteEffect, module))
    }
    EmptyPriceCell()
}


/**
 * A [ModuleSlotContentProvider] that provides [ModuleEffectSlotContent].
 */
@Stable
private fun ModuleEffectSlotContent(remoteEffect: RemoteEffect) = ModuleSlotContentProvider {
        scope: GridScope.GridRowScope,
        module: Module,
        _: Int?,
        carousel: Carousel<ModuleType>,
        togglePrimaryState: () -> Unit,
        toggleOverloaded: () -> Unit,
        toggleOnline: () -> Unit,
        showSpoolupCyclesSelector: () -> Unit,
        showAdaptationCyclesSelector: () -> Unit,
    ->
    with(scope) {
        ModuleEffectSlotContent(
            module = module,
            carousel = carousel,
            remoteEffect = remoteEffect,
            togglePrimaryState = togglePrimaryState,
            toggleOverloaded = toggleOverloaded,
            toggleOnline = toggleOnline,
            showSpoolupCyclesSelector = showSpoolupCyclesSelector,
            showAdaptationCyclesSelector = showAdaptationCyclesSelector,
        )
    }
}


/**
 * The row for a module effect.
 */
@Composable
private fun GridScope.GridRowScope.ModuleEffectSlotContent(
    module: Module,
    carousel: Carousel<ModuleType>,
    remoteEffect: RemoteEffect,
    togglePrimaryState: () -> Unit,
    toggleOverloaded: () -> Unit,
    toggleOnline: () -> Unit,
    showSpoolupCyclesSelector: () -> Unit,
    showAdaptationCyclesSelector: () -> Unit,
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        ModuleStateIcon(
            module = module,
            togglePrimaryState = togglePrimaryState,
            toggleOverloaded = toggleOverloaded,
            toggleOnline = toggleOnline
        )
    }
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(module)
    }
    cell(cellIndex = GridCols.NAME, colSpan = GridCols.EFFECT - GridCols.NAME) {
        CarouselSlotContent(
            carousel = carousel,
            targetState = module.type,
            modifier = Modifier.fillMaxWidth(),
            text = { it.name },
            extraContent = {
                if (module.spoolupCycles != null) {
                    HSpacer(TheorycrafterTheme.spacing.medium)
                    SpoolupCyclesIndicator(module, showSpoolupCyclesSelector)
                }
                if (module.adaptationCycles != null) {
                    HSpacer(TheorycrafterTheme.spacing.medium)
                    AdaptationCyclesIndicator(module, showAdaptationCyclesSelector)
                }
            }
        )
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedRemoteModuleEffect(remoteEffect, module))
    }
    EmptyPriceCell()
}


/**
 * An [ModuleSelectorProvider] that provides [AffectingModuleSelectorRow].
 */
private val AffectingModuleSelector = ModuleSelectorProvider {
        scope,
        carousel,
        onModuleSelected,
        onEditingCancelled ->
    with(scope) {
        AffectingModuleSelectorRow(
            carousel = carousel!!,
            onModuleSelected = onModuleSelected,
            onEditingCancelled = onEditingCancelled
        )
    }
}


/**
 * The module selector for the affecting module of a [RemoteEffect] or a standalone module effect.
 */
@Composable
private fun GridScope.GridRowScope.AffectingModuleSelectorRow(
    carousel: Carousel<ModuleType>,
    onModuleSelected: (ModuleType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = remember(carousel) {
        autoSuggest(carousel.items).onEmptyQueryReturn { carousel.items }
    }
    ItemSelectorRow(
        onItemSelected = onModuleSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        hint = "Module name"
    )
}


/**
 * Associates each module with its index in its rack.
 */
private fun Set<Module>.associateWithIndexInRack(fit: Fit): Map<Module, Int> {
    return ModuleSlotType.entries.flatMap { slotType ->
        fit.modules.slotsInRack(slotType).mapIndexedNotNull { index, module ->
            if ((module != null) && (module in this))
                module to index
            else
                null
        }
    }.toMap()
}


/**
 * The affecting modules of a [RemoteEffect], or standalone effect modules.
 */
@Composable
private fun GridScope.AffectingModules(
    firstRowIndex: Int,
    remoteEffect: RemoteEffect,
    moduleSlotContentProvider: ModuleSlotContentProvider,
    moduleSlotActionsProvider: @Composable (SingleModuleSlot) -> ModuleSlotActions,
    chargeSlotTextStyle: TextStyle,
    chargeSlotContentModifier: Modifier,
    moduleTestTag: (Int) -> String,
    chargeTestTag: (Int) -> String
): Int {
    var rowIndex = firstRowIndex
    val sourceFit = remoteEffect.source
    val modules = remoteEffect.affectingModules
    // We can't remember indexInRackByModule because the indices can change even without the set of modules changing,
    // because the auxiliary fit is shared by the hostile and friendly auxiliary remote effects.
    val indexInRackByModule = modules.associateWithIndexInRack(sourceFit)
    val sortedModules = modules.sortedByPositionInFit(indexInRackByModule)
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current

    for ((moduleRowIndex, module) in sortedModules.withIndex()) {
        val indexInRack = indexInRackByModule[module]!!
        val slotGroup = remember(sourceFit, module.type.slotType, indexInRack) {
            SingleModuleSlot(sourceFit, module.type.slotType, indexInRack)
        }
        inRow(rowIndex++) {
            ModuleSlotRow(
                testTag = moduleTestTag(moduleRowIndex),
                slotGroup = slotGroup,
                actions = moduleSlotActionsProvider(slotGroup),
                slotContentProvider = moduleSlotContentProvider,
                moduleSelectorProvider = AffectingModuleSelector
            )
        }

        if (module.canLoadCharges) {
            inRow(rowIndex++) {
                val charge = module.loadedCharge
                ChargeSlotRow(
                    testTag = chargeTestTag(moduleRowIndex),
                    charge = charge,
                    module = module,
                    textStyle = chargeSlotTextStyle,
                    slotContentModifier = chargeSlotContentModifier,
                    loadCharge = remember(sourceFit, slotGroup, undoRedoQueue) {
                        { chargeType, triggerCarouselAnimation ->
                            setChargeAction(sourceFit, slotGroup, chargeType)?.let {
                                undoRedoQueue.performAndAppend(
                                    it.withCarouselAnimation(triggerCarouselAnimation)
                                )
                            }
                        }
                    },
                    isRemoteFit = true
                )
            }
        }
    }
    return rowIndex - firstRowIndex
}


/**
 * Remembers and returns the [ModuleSlotActions] for a module effect slot.
 */
@Composable
private fun rememberModuleEffectSlotActions(
    fit: Fit,
    module: Module,
    undoRedoQueue: FitEditorUndoRedoQueue,
    onNoRoomForModule: (Int) -> Unit,
): ModuleSlotActions = rememberSlotActions(fit, module, undoRedoQueue, onNoRoomForModule) {
    ModuleSlotActions(
        cannotFitReason = { null },
        fit = { moduleType, preserveState, triggerCarouselAnimation ->
            if (stale)
                return@ModuleSlotActions

            replaceModuleEffectAction(fit, module, moduleType, preserveState)?.let {
                undoRedoQueue.performAndAppend(
                    it.withCarouselAnimation(triggerCarouselAnimation)
                )
            }
        },
        replaceModuleAction = { moduleType ->
            replaceModuleEffectAction(fit, module, moduleType, true)
        },
        clear = {
            if (stale)
                return@ModuleSlotActions

            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    removeModuleEffectAction(fit, module),
                    markStaleAction()
                )
            )
        },
        togglePrimaryState = {
            if (stale)
                return@ModuleSlotActions
            undoRedoQueue.performAndAppend(
                toggleModulePrimaryStateAction(module)
            )
        },
        toggleOnlineState = { },
        toggleOverloadState = {
            if (stale)
                return@ModuleSlotActions
            toggleModuleOverloadStateAction(module)?.let {
                undoRedoQueue.performAndAppend(it)
            }
        },
        addOne = {
            if (stale)
                return@ModuleSlotActions

            try {
                undoRedoQueue.performAndAppend(
                    addModuleEffectAction(fit, module.type)
                )
            } catch (e: NoRoomForModuleEffectException) {
                onNoRoomForModule(e.slotIndex)
            }
        },
        canMoveModules = { false },
        moveModuleTo = null,
        canMoveModuleBy = null,
        moveModuleBy = null,
        removeOne = {
            if (stale)
                return@ModuleSlotActions

            val moduleType = module.type
            val allModules = fit.auxiliaryFit!!.modules.slotsInRack(moduleType.slotType)
            val slotWithSameModuleType = allModules.lastOrNull { otherModule ->
                (otherModule?.type == moduleType) && (otherModule != module)
            }
            if (slotWithSameModuleType != null) {
                undoRedoQueue.performAndAppend(
                    removeModuleEffectAction(fit, slotWithSameModuleType)
                )
            }
        },
        setSpoolupCycles = { module, cycles ->
            if (stale)
                return@ModuleSlotActions

            val action = setSpoolupCyclesAction(module, cycles) ?: return@ModuleSlotActions
            undoRedoQueue.performAndAppend(action)
        },
        setAdaptationCycles = { module, cycles ->
            if (stale)
                return@ModuleSlotActions

            val action = setAdaptationCyclesAction(module, cycles) ?: return@ModuleSlotActions
            undoRedoQueue.performAndAppend(action)
        }
    )
}


/**
 * A [DroneSlotContentProvider] that provides [AffectingDroneSlotContent].
 */
private fun AffectingDronesSlotContent(remoteEffect: RemoteEffect) = DroneSlotContentProvider {
          scope,
          droneGroup,
          carousel,
          toggleActive ->
    with(scope) {
        AffectingDroneSlotContent(
            droneGroup = droneGroup,
            carousel = carousel,
            remoteEffect = remoteEffect,
            toggleActive = toggleActive
        )
    }
}


/**
 * The slot content for an affecting drone of a remote fit.
 */
@Composable
private fun GridScope.GridRowScope.AffectingDroneSlotContent(
    droneGroup: DroneGroup,
    carousel: Carousel<CarouselDroneType>,
    remoteEffect: RemoteEffect,
    toggleActive: () -> Unit,
) {
    emptyCell(cellIndex = GridCols.STATE_ICON)
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(droneGroup)
    }
    cell(cellIndex = GridCols.NAME, colSpan = GridCols.EFFECT - GridCols.NAME) {
        ProvideTextStyle(TheorycrafterTheme.textStyles.fitEditorSecondarySlot) {
            VerticallyCenteredRow(modifier = Modifier.fillMaxWidth()) {
                DroneStateIcon(
                    droneGroup = droneGroup,
                    modifier = Modifier.scale(0.8f).wrapContentSize(),
                    toggleActive = toggleActive
                )
                CarouselSlotContent(
                    carousel = carousel,
                    targetState = CarouselDroneType(droneGroup),
                    modifier = Modifier.align(Alignment.CenterStart).fillMaxWidth(),
                    text = { droneGroupDisplayText(it.amount, it.droneType) }
                )
            }
        }
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedRemoteDroneEffect(remoteEffect, droneGroup))
    }
    EmptyPriceCell()
}


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


/**
 * The slot content for a standalone affecting drone.
 */
@Composable
private fun GridScope.GridRowScope.DroneEffectSlotContent(
    droneGroup: DroneGroup,
    carousel: Carousel<CarouselDroneType>,
    remoteEffect: RemoteEffect,
    toggleActive: () -> Unit
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        DroneStateIcon(
            droneGroup = droneGroup,
            toggleActive = toggleActive
        )
    }
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(droneGroup)
    }
    cell(cellIndex = GridCols.NAME, colSpan = GridCols.EFFECT - GridCols.NAME) {
        CarouselSlotContent(
            carousel = carousel,
            targetState = CarouselDroneType(droneGroup),
            modifier = Modifier.fillMaxWidth(),
            text = { droneGroupDisplayText(it.amount, it.droneType) }
        )
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedRemoteDroneEffect(remoteEffect, droneGroup))
    }
    EmptyPriceCell()
}


/**
 * The affecting drones of a [RemoteEffect], or standalone effect drones.
 */
@Composable
private fun GridScope.AffectingDrones(
    firstRowIndex: Int,
    remoteEffect: RemoteEffect,
    droneSlotActionsProvider: @Composable (DroneGroup) -> DroneSlotActions,
    slotContentProvider: DroneSlotContentProvider,
    testTag: (Int) -> String
): Int {
    var rowIndex = firstRowIndex
    val droneGroups = remoteEffect.affectingDrones
    if (droneGroups.isNotEmpty()) {
        val sortedDrones = remember(droneGroups) {
            droneGroups.sortedBy { it.indexInFit() }
        }
        for ((droneRowIndex, droneGroup) in sortedDrones.withIndex()) {
            inRow(rowIndex++) {
                DroneSlotRow(
                    testTag = testTag(droneRowIndex),
                    droneGroup = droneGroup,
                    actions = droneSlotActionsProvider(droneGroup),
                    slotContentProvider = slotContentProvider
                )
            }
        }
    }
    return rowIndex - firstRowIndex
}


/**
 * Remembers and returns the actions for a drone slot displaying an affecting drone.
 */
@Composable
private fun rememberDroneEffectSlotActions(
    fit: Fit,
    droneGroup: DroneGroup,
    undoRedoQueue: FitEditorUndoRedoQueue
) = rememberSlotActions(fit, droneGroup, undoRedoQueue) {
    DroneSlotActions(
        fit = { droneType, amount, keepActiveState, triggerCarouselAnimation ->
            if (stale)
                return@DroneSlotActions

            undoRedoQueue.performAndAppend(
                replaceDroneEffectAction(
                    fit, currentDroneGroup = droneGroup, newDroneType = droneType, size = amount, keepActiveState
                ).withCarouselAnimation(triggerCarouselAnimation)
            )
        },
        replaceDroneAction = { droneType ->
            replaceDroneEffectAction(
                fit, currentDroneGroup = droneGroup, newDroneType = droneType, size = droneGroup.size, true
            )
        },
        clear = {
            if (stale)
                return@DroneSlotActions

            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    removeDroneEffectAction(fit, droneGroup),
                    markStaleAction()
                )
            )
        },
        toggleActive = {
            if (stale)
                return@DroneSlotActions
            undoRedoQueue.performAndAppend(
                toggleDroneActiveStateAction(droneGroup.fit, droneGroup.indexInFit())
            )
        },
        addAmount = { addedAmount ->
            if (stale)
                return@DroneSlotActions
            val newAmount = droneGroup.size + addedAmount
            if ((newAmount > 0) && (newAmount < 100)){  // The actual limit is 127, due to serialization
                setDroneGroupSizeAction(droneGroup.fit, droneGroup.indexInFit(), newAmount)?.let {
                    undoRedoQueue.performAndAppend(it)
                }
            }
        },
    )
}


/**
 * Returns the given list of modules, sorted by their position in the given fit.
 */
private fun Collection<Module>.sortedByPositionInFit(indexInRackByModule: Map<Module, Int>) = sortedWith(
    compareBy(
        { it.type.slotType },
        { indexInRackByModule[it] }
    )
)


/**
 * Returns the index of the drone group in its fit.
 */
private fun DroneGroup.indexInFit() = this.fit.drones.all.indexOf(this)


/**
 * The section for specifying remote effects.
 */
@Composable
fun GridScope.RemoteEffectsSection(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
    remoteEffects: List<RemoteEffect>,
    sectionTitle: AnnotatedString,
    remoteFitActions: RemoteEffectFitActions,
    remoteEffectSlotTestTag: (Int) -> String,
    affectingModuleSlotTestTag: (remoteEffectIndex: Int, moduleIndex: Int) -> String,
    affectingModuleChargeTestTag: (remoteEffectIndex: Int, moduleIndex: Int) -> String,
    affectingDroneSlotTestTag: ((remoteEffectIndex: Int, droneIndex: Int) -> String)?,  // No drones in command effects
    moduleEffectSlotTestTag: ((Int) -> String)?,
    moduleEffectChargeSlotTestTag: ((Int) -> String)?,
    droneEffectSlotTestTag: ((Int) -> String)?,
    emptySlotText: String,
    emptySlotTestTag: String,
    remoteEffectSelectionFromClipboardText: EveData.(String) -> RemoteEffectSelection?,
    editedRemoteEffectRow: EditedRemoteEffectRow<RemoteEffectSelection>,
): Int {
    var rowIndex = firstRowIndex
    val auxFit = fit.auxiliaryFit

    val remoteFitEffects by remember(remoteEffects, auxFit) {
        derivedStateOf(structuralEqualityPolicy()) {
            if (auxFit == null)
                remoteEffects
            else
                remoteEffects.filter { it.source != auxFit }
        }
    }
    val auxRemoteEffect by remember(remoteEffects, auxFit) {
        derivedStateOf(structuralEqualityPolicy()) {
            if (auxFit == null)
                null
            else
                remoteEffects.find { it.source == auxFit }
        }
    }

    SectionTitleRow(
        rowIndex = rowIndex++,
        isFirst = isFirst,
        text = sectionTitle
    )

    // Remote fit effects
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    for ((remoteEffectRowIndex, remoteEffect) in remoteFitEffects.withIndex()) {
        val remoteEffectSlotIndex = remoteEffects.indexOf(remoteEffect)
        inRow(rowIndex++) {
            RemoteFitEffectSlotRow(
                testTag = remoteEffectSlotTestTag(remoteEffectRowIndex),
                remoteEffect = remoteEffect,
                actions = rememberRemoteFitEffectSlotActions(remoteFitActions, fit, remoteEffectSlotIndex, undoRedoQueue),
            )
        }

        rowIndex += AffectingModules(
            firstRowIndex = rowIndex,
            remoteEffect = remoteEffect,
            moduleSlotContentProvider = remember(remoteEffect) { AffectingModuleSlotContent(remoteEffect) },
            moduleSlotActionsProvider = { moduleSlot ->
                rememberModuleSlotActions(
                    fit = remoteEffect.source,
                    slotGroup = moduleSlot,
                    canMoveSlots = false,
                    rackSlotGroupingActions = null,
                    undoRedoQueue = undoRedoQueue
                )
            },
            chargeSlotTextStyle = TheorycrafterTheme.textStyles.fitEditorTertiarySlot,
            chargeSlotContentModifier = Modifier.padding(start = 16.dp),
            moduleTestTag = { moduleRowIndex ->
                affectingModuleSlotTestTag(remoteEffectRowIndex, moduleRowIndex)
            },
            chargeTestTag = { moduleRowIndex ->
                affectingModuleChargeTestTag(remoteEffectRowIndex, moduleRowIndex)
            },
        )

        rowIndex += AffectingDrones(
            firstRowIndex = rowIndex,
            remoteEffect = remoteEffect,
            droneSlotActionsProvider = { droneGroup ->
                rememberDroneSlotActions(droneGroup.fit, droneGroup.indexInFit(), undoRedoQueue)
            },
            slotContentProvider = remember(remoteEffect) { AffectingDronesSlotContent(remoteEffect) },
            testTag = { droneRowIndex ->
                affectingDroneSlotTestTag!!(remoteEffectRowIndex, droneRowIndex)
            }
        )
    }

    val dialogs = LocalStandardDialogs.current

    fun showModuleLimitReachedMessage(maxAmount: Int) {
        dialogs.showErrorDialog(
            message = "Can use up to $maxAmount modules of this slot type",
            // We don't focus the button because we open this dialog on a key-pressed event,
            // and if the button is focused immediately, the key-up event presses it, immediately closing the dialog
            focusConfirmButton = false
        )
    }

    // Standalone remote module and drone effects
    auxRemoteEffect?.let { remoteEffect ->
        val moduleEffectSlotContentProvider = remember(remoteEffect) { ModuleEffectSlotContent(remoteEffect) }
        val droneSlotContentProvider = remember(remoteEffect) { DroneEffectSlotContent(remoteEffect) }

        rowIndex += AffectingModules(
            firstRowIndex = rowIndex,
            moduleSlotContentProvider = moduleEffectSlotContentProvider,
            moduleSlotActionsProvider = { moduleSlot ->
                rememberModuleEffectSlotActions(
                    fit = fit,
                    module = moduleSlot.requireRepModule(),
                    undoRedoQueue = undoRedoQueue,
                    onNoRoomForModule = { showModuleLimitReachedMessage(it) }
                )
            },
            chargeSlotTextStyle = TheorycrafterTheme.textStyles.fitEditorSecondarySlot,
            chargeSlotContentModifier = Modifier,
            remoteEffect = remoteEffect,
            moduleTestTag = moduleEffectSlotTestTag!!,
            chargeTestTag = moduleEffectChargeSlotTestTag!!,
        )

        rowIndex += AffectingDrones(
            firstRowIndex = rowIndex,
            remoteEffect = remoteEffect,
            droneSlotActionsProvider = { droneGroup ->
                rememberDroneEffectSlotActions(fit, droneGroup, undoRedoQueue)
            },
            slotContentProvider = droneSlotContentProvider,
            testTag = droneEffectSlotTestTag!!
        )
    }

    // An extra slot where the user can add effects
    inRow(rowIndex++) {
        EmptyRemoteEffectSlotRow(
            testTag = emptySlotTestTag,
            text = emptySlotText,
            remoteEffectSelectionFromClipboardText = remoteEffectSelectionFromClipboardText,
            setRemoteEffect = remember(fit, undoRedoQueue, remoteFitActions) {
                fun (effectSelection: RemoteEffectSelection) {
                    try {
                        undoRedoQueue.performAndAppend(
                            when (effectSelection) {
                                is RemoteEffectSelection.Fit -> remoteFitActions.addRemoteEffectAction(fit, effectSelection.fitHandle)
                                is RemoteEffectSelection.Module -> addModuleEffectAction(fit, effectSelection.moduleType)
                                is RemoteEffectSelection.Drone ->
                                    addDroneEffectAction(fit, effectSelection.droneType, effectSelection.amount)
                            }
                        )
                    } catch (e: NoRoomForModuleEffectException) {
                        showModuleLimitReachedMessage(e.slotIndex)
                    }
                }
            },
            editedRemoteEffectRow = editedRemoteEffectRow
        )
    }

    return rowIndex - firstRowIndex
}
