/**
 * 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.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalClipboard
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.fits.FitHandle
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.*
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.ui.widgets.StandardDialogs
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,
    val index: Int
)


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


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


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


    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.revertEdit() {
        if (added != null) {
            val effect = fit.remoteEffects.allModule[added.index]
            fit.removeModuleEffect(effect)
        }
        if (removed != null) {
            // When reverting an action, we should always have room to add the module, hence the !!
            val effect = fit.addModuleEffect(moduleType = removed.moduleType, index = removed.index)!!
            if (removed.chargeType != null)
                effect.module.setCharge(removed.chargeType)
            effect.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 {
    return ModuleEffectReplacement(
        fit = fit,
        removed = null,
        added = RemoteModuleEffectInfo(
            moduleType = moduleType,
            chargeType = preloadedCharge(fit, moduleType),
            state = initialModuleState(moduleType),
            index = fit.remoteEffects.allModule.size
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes a module effect from the fit.
 */
private fun removeModuleEffectAction(fit: Fit, effect: ModuleEffect): FitEditingAction {
    val module = effect.module
    return ModuleEffectReplacement(
        fit = fit,
        removed = RemoteModuleEffectInfo(
            moduleType = module.type,
            chargeType = module.loadedCharge?.type,
            state = module.state,
            index = fit.remoteEffects.allModule.indexOf(effect)
        ),
        added = null
    )
}


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

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


/**
 * 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,
    val index: Int,
)


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


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


    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.revertEdit() {
        if (added != null) {
            val droneGroup = fit.remoteEffects.allDrone[added.index]
            fit.removeDroneEffect(droneGroup)
        }
        if (removed != null) {
            val effect = fit.addDroneEffect(removed.droneType, size = removed.size, index = removed.index)
            effect.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 {
    return DroneEffectReplacement(
        fit = fit,
        removed = null,
        added = RemoteDroneEffectInfo(
            droneType,
            size = amount,
            isActive = true,
            index = fit.remoteEffects.allDrone.size
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes a drone effect from the fit.
 */
private fun removeDroneEffectAction(fit: Fit, effect: DroneEffect): FitEditingAction {
    val droneGroup = effect.droneGroup
    return DroneEffectReplacement(
        fit = fit,
        removed = RemoteDroneEffectInfo(
            droneType = droneGroup.type,
            size = droneGroup.size,
            isActive = droneGroup.active,
            index = fit.remoteEffects.allDrone.indexOf(effect)
        ),
        added = null
    )
}


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


/**
 * 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 = GridCols.LAST - GridCols.NAME) {
        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 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.openFit(fitOpener, remoteEffect.source))
            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 clipboard = LocalClipboard.current
    val contextActions = remember(remoteEffectSelectionFromClipboardText, clipboard) {
        listOf(
            SlotContextAction.pasteFromClipboard(
                clipboard = clipboard,
                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 = 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 Iterable<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,
    sourceFit: Fit,
    modules: Collection<Module>,
    moduleSlotContentProvider: ModuleSlotContentProvider,
    moduleSlotActionsProvider: @Composable (SingleModuleSlot) -> ModuleSlotActions,
    chargeSlotTextStyle: TextStyle,
    chargeSlotContentModifier: Modifier,
    moduleTestTag: (Int) -> String,
    chargeTestTag: (Int) -> String
): Int {
    var rowIndex = firstRowIndex
    // 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,
    moduleEffect: ModuleEffect,
    undoRedoQueue: FitEditorUndoRedoQueue,
    onNoRoomForModule: (ModuleSlotType) -> Unit,
): ModuleSlotActions = rememberSlotActions(fit, moduleEffect, undoRedoQueue, onNoRoomForModule) {
    ModuleSlotActions(
        cannotFitReason = { null },
        fit = { moduleType, preserveState, triggerCarouselAnimation ->
            if (stale)
                return@ModuleSlotActions

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

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

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

            val module = moduleEffect.module
            val moduleType = module.type
            val effectWithSameModuleType = fit.remoteEffects.allModule.lastOrNull { otherEffect ->
                (otherEffect != moduleEffect) && (otherEffect.module.type == moduleType)
            }
            if (effectWithSameModuleType != null) {
                undoRedoQueue.performAndAppend(
                    removeModuleEffectAction(fit, effectWithSameModuleType)
                )
            }
        },
        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,
    droneGroups: Collection<DroneGroup>,
    droneSlotActionsProvider: @Composable (DroneGroup) -> DroneSlotActions,
    slotContentProvider: DroneSlotContentProvider,
    testTag: (Int) -> String
): Int {
    var rowIndex = firstRowIndex
    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,
    droneEffect: DroneEffect,
    undoRedoQueue: FitEditorUndoRedoQueue
) = rememberSlotActions(fit, droneEffect, undoRedoQueue) {
    DroneSlotActions(
        fit = { droneType, amount, keepActiveState, triggerCarouselAnimation ->
            if (stale)
                return@DroneSlotActions

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

            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    removeDroneEffectAction(fit, droneEffect),
                    markStaleAction()
                )
            )
        },
        toggleActive = {
            if (stale)
                return@DroneSlotActions
            undoRedoQueue.performAndAppend(
                toggleDroneActiveStateAction(droneEffect.droneGroup.fit, droneEffect.droneGroup.indexInFit())
            )
        },
        addAmount = { addedAmount ->
            if (stale)
                return@DroneSlotActions
            val droneGroup = droneEffect.droneGroup
            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,
    remoteEffectSet: Fit.RemoteEffects.EffectSet,
    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

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

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

        rowIndex += AffectingModules(
            firstRowIndex = rowIndex,
            sourceFit = remoteEffect.source,
            modules = remoteEffect.affectingModules,
            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,
            droneGroups = remoteEffect.affectingDrones,
            droneSlotActionsProvider = { droneGroup ->
                rememberDroneSlotActions(droneGroup.fit, droneGroup.indexInFit(), undoRedoQueue)
            },
            slotContentProvider = remember(remoteEffect) { AffectingDronesSlotContent(remoteEffect) },
            testTag = { droneRowIndex ->
                affectingDroneSlotTestTag!!(remoteEffectRowIndex, droneRowIndex)
            }
        )
    }

    val dialogs = LocalStandardDialogs.current

    // Standalone remote module and drone effects
    val auxEffect = remoteEffectSet.auxiliaryEffect
    if (auxEffect != null) {
        val moduleEffectSlotContentProvider = remember(auxEffect) {
            ModuleEffectSlotContent(auxEffect)
        }
        rowIndex += AffectingModules(
            firstRowIndex = rowIndex,
            sourceFit = auxEffect.source,
            modules = remoteEffectSet.module.map { it.module },
            moduleSlotContentProvider = moduleEffectSlotContentProvider,
            moduleSlotActionsProvider = { moduleSlot ->
                val module = moduleSlot.requireRepModule()
                val moduleEffect = fit.remoteEffects.allModule.find { it.module == module }!!
                rememberModuleEffectSlotActions(
                    fit = fit,
                    moduleEffect = moduleEffect,
                    undoRedoQueue = undoRedoQueue,
                    onNoRoomForModule = dialogs::showAuxModuleLimitReachedMessage
                )
            },
            chargeSlotTextStyle = TheorycrafterTheme.textStyles.fitEditorSecondarySlot,
            chargeSlotContentModifier = Modifier,
            moduleTestTag = moduleEffectSlotTestTag!!,
            chargeTestTag = moduleEffectChargeSlotTestTag!!,
        )

        val droneSlotContentProvider = remember(auxEffect) {
            DroneEffectSlotContent(auxEffect)
        }
        rowIndex += AffectingDrones(
            firstRowIndex = rowIndex,
            droneGroups = remoteEffectSet.drone.map { it.droneGroup },
            droneSlotActionsProvider = { droneGroup ->
                val droneEffect = fit.remoteEffects.allDrone.find { it.droneGroup == droneGroup }!!
                rememberDroneEffectSlotActions(fit, droneEffect, 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) {
                        dialogs.showAuxModuleLimitReachedMessage(e.slotType)
                    }
                }
            },
            editedRemoteEffectRow = editedRemoteEffectRow
        )
    }

    return rowIndex - firstRowIndex
}


/**
 * Shows an error message explaining that the maximum number of modules for the slot type has been reached.
 */
fun StandardDialogs.showAuxModuleLimitReachedMessage(slotType: ModuleSlotType) {
    showErrorDialog(
        message = "Maximum number of auxiliary modules of slot type ${slotType.lowercaseName} has been reached.",
        // 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
    )
}
