package theorycrafter.ui.tournaments

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.*
import compose.input.*
import compose.utils.EasyTooltipPlacement
import compose.utils.LocalKeyShortcutsManager
import compose.utils.VSpacer
import compose.utils.VerticallyCenteredRow
import compose.utils.tooltip
import compose.widgets.*
import eve.data.ShipType
import eve.data.asDps
import eve.data.asHitPoints
import eve.data.asHitpointsPerSecond
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import theorycrafter.fits.FitHandle
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Fit
import theorycrafter.fitting.ItemDefense
import theorycrafter.tournaments.*
import theorycrafter.tournaments.Composition
import theorycrafter.ui.*
import theorycrafter.ui.fiteditor.*
import theorycrafter.ui.widgets.HORIZONTAL_SLOT_ROW_PADDING
import theorycrafter.ui.widgets.SLOT_ROW_PADDING
import theorycrafter.ui.widgets.Selector
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*


/**
 * The grid column indices.
 */
private object GridCols {
    const val STATE_ICON = 0
    const val SHIP_ICON = 1
    const val SHIP = 2
    const val COST = 3
    const val FIT = 4
    const val EHP = 5
    const val ACTIVE_TANK = 6
    const val DPS = 7
    const val COUNT = 8
}


/**
 * The alignment in each column.
 */
private val ColumnAlignment = listOf(
    Alignment.Center,       // state icon
    Alignment.Center,       // ship icon
    Alignment.CenterStart,  // ship
    Alignment.CenterEnd,    // cost
    Alignment.CenterStart,  // fit
    Alignment.CenterEnd,    // ehp
    Alignment.CenterEnd,    // tank
    Alignment.CenterEnd,    // dps
)


/**
 * The widths of the columns.
 */
private val ColumnWidths = listOf(
    24.dp,           // state icon
    TheorycrafterTheme.sizes.eveTypeIconSmall + 6.dp,           // ship icon
    160.dp,          // ship type
    80.dp,           // points
    Dp.Unspecified,  // fit name
    80.dp,           // EHP
    100.dp,          // Active Tank
    80.dp,           // DPS
)


/**
 * The padding of the "Cost" column.
 *
 * This is needed because otherwise it's too close to the fit column.
 */
private val COST_COLUMN_PADDING = PaddingValues(end = TheorycrafterTheme.spacing.xxlarge)


/**
 * The padding modifier of the "Cost" column.
 */
private val COST_COLUMN_PADDING_MODIFIER = Modifier.padding(COST_COLUMN_PADDING)


/**
 * The grid header row.
 */
@Composable
private fun HeaderRow(
    tournament: Tournament,
    columnWidths: List<Dp>,
    modifier: Modifier = Modifier
) {
    SimpleGridHeaderRow(
        modifier = modifier
            .padding(HORIZONTAL_SLOT_ROW_PADDING),
        columnWidths = columnWidths,
        defaultCellContentAlignment = ColumnAlignment::get
    ) {
        CompositionLocalProvider(LocalTextStyle provides TextStyle(fontWeight = FontWeight.Bold)) {
            EmptyCell(index = GridCols.STATE_ICON)
            EmptyCell(index = GridCols.SHIP_ICON)
            TextCell(index = GridCols.SHIP, "Ship")
            if (tournament.isDoctrines) {
                EmptyCell(index = GridCols.COST)
            } else {
                TextCell(
                    index = GridCols.COST,
                    text = when (tournament.rules.compositionRules) {
                        is PointsCompositionRules -> "Cost"
                        else -> "Points"
                    },
                    modifier = COST_COLUMN_PADDING_MODIFIER
                )
            }

            TextCell(index = GridCols.FIT, "Fit")
            TextCell(index = GridCols.EHP, "EHP")
            TextCell(index = GridCols.ACTIVE_TANK, "Active Tank")
            TextCell(index = GridCols.DPS, "DPS")
        }
    }
}


/**
 * Computes the number of row slots to display for the given composition.
 */
private val Composition.visibleSlotCount: Int
    get() = when(val rules = tournament.rules.compositionRules) {
        is PointsCompositionRules -> rules.maxCompositionSize.coerceAtLeast(size + 1)  // +1 for the empty slot
        is DraftCompositionRules -> rules.positions.size.coerceAtLeast(size) + 1  // +1 for replacement ship slot
        is DoctrineCompositionRules -> size + 1 // +1 for the empty slot
    }


/**
 * The panel showing the composition and allows editing it.
 */
@Composable
fun CompositionEditor(
    composition: Composition,
    modifier: Modifier
) {
    val selectionModel = remember(composition) {
        CompositionShipsSelectionModel(composition)
    }
    // Update the selected index when the list of slots no longer includes it
    LaunchedEffect(selectionModel, selectionModel.maxSelectableIndex) {
        selectionModel.selectedIndex?.let {
            if (it > selectionModel.maxSelectableIndex)
                selectionModel.selectLast()
        }
    }
    val rules = composition.tournament.rules.compositionRules
    val shipsCost by remember(rules, composition) {
        if (composition.isDoctrine)
            immutableStateOf(null)
        else derivedStateOf {
            rules.shipsCost(composition)
        }
    }
    val shipsIllegalityReason by remember(rules, composition) {
        derivedStateOf {
            rules.compositionShipsIllegalityReason(composition)
        }
    }

    val compositionExporter = rememberCompositionExporter()
    LocalKeyShortcutsManager.current.register(CopyCompositionToClipboardKeyShortcut) {
        compositionExporter.copyToClipboard(composition)
    }
    ContentWithScrollbar(modifier.testTag(TestTags.CompEditor.RootNode)) {
        Column(Modifier.verticalScroll(scrollState)) {
            val tournament = composition.tournament
            val columnWidths = remember(tournament) {
                if (tournament.isDoctrines)
                    ColumnWidths.mapIndexed { index, width -> if (index == GridCols.COST) 0.dp else width }
                else
                    ColumnWidths
            }
            HeaderRow(
                tournament = tournament,
                columnWidths = columnWidths,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(
                        top = TheorycrafterTheme.spacing.larger,
                        bottom = TheorycrafterTheme.spacing.xsmall
                    )
            )
            SimpleGrid(
                columnWidths = columnWidths,
                rowSelectionModel = selectionModel,
                defaultCellContentAlignment = ColumnAlignment::get,
                modifier = Modifier
                    .moveSelectionWithKeys(selectionModel),
            ) {
                val slotCount = composition.visibleSlotCount
                for (index in 0 until slotCount) {
                    val slotActions = remember(composition, index, selectionModel) {
                        ShipSlotActions(composition, index, selectionModel)
                    }

                    val ship = composition[index]
                    inRow(index) {
                        if (ship != null) {
                            ShipSlotRow(
                                composition = composition,
                                shipIndex = index,
                                ship = ship,
                                shipCost = shipsCost?.get(index),
                                shipIllegalReason = shipsIllegalityReason[index],
                                selectionModel = selectionModel,
                                actions = slotActions,
                            )
                        }
                        else {
                            EmptyShipSlotRow(
                                composition = composition,
                                shipIndex = index,
                                selectionModel = selectionModel,
                                actions = slotActions,
                            )
                        }
                    }
                }
            }

            // The separator line
            Box(
                modifier = Modifier
                    .padding(
                        top = TheorycrafterTheme.spacing.large,
                        bottom = TheorycrafterTheme.spacing.small)
                    .padding(HORIZONTAL_SLOT_ROW_PADDING)
                    .background(LocalContentColor.current.copy(alpha = 0.4f))
                    .fillMaxWidth()
                    .height(1.dp)
            )

            SummationRow(
                composition = composition,
                shipsCost = shipsCost,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(HORIZONTAL_SLOT_ROW_PADDING)
            )

            VSpacer(TheorycrafterTheme.spacing.larger)

            val sectionsModifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin)

            UtilitySummary(
                composition = composition,
                modifier = sectionsModifier
            )

            VSpacer(TheorycrafterTheme.spacing.larger)

            CompositionNotes(
                composition = composition,
                modifier = sectionsModifier
            )
        }
    }
}


/**
 * Moves the slot selection when Up/Down/Home/End keys are pressed.
 */
@Composable
private fun Modifier.moveSelectionWithKeys(
    selectionModel: CompositionShipsSelectionModel,
): Modifier = with(FitEditorKeyShortcuts) {
    this@moveSelectionWithKeys
        .onKeyShortcut(SelectPrevRow) {
            selectionModel.selectPrevious()
        }
        .onKeyShortcut(SelectNextRow) {
            selectionModel.selectNext()
        }
        .onKeyShortcut(SelectionFirst) {
            selectionModel.selectFirst()
        }
        .onKeyShortcut(SelectionLast) {
            selectionModel.selectLast()
        }
}


/**
 * Groups actions on a composition ship slot.
 */
private class ShipSlotActions(
    private val composition: Composition,
    private val index: Int,
    private val selectionModel: CompositionShipsSelectionModel,
) {


    /**
     * Sets the [Composition.Ship] in the slot to a new one, with the given ship type.
     */
    fun setShip(shipType: ShipType, amount: Int?) {
        val active = when (val rules = composition.tournament.rules.compositionRules) {
            is PointsCompositionRules -> true
            is DraftCompositionRules -> index in rules.positions.indices
            is DoctrineCompositionRules -> true
        }

        // Keep fit if set the same ship type
        val currentShip = composition[index]
        val fitId = if (currentShip?.shipType == shipType) currentShip.fitId else null

        composition[index] = Composition.Ship(
            composition = composition,
            shipType = shipType,
            amount = amount,
            fitId = fitId,
            active = active
        )
    }


    /**
     * Removes the current ship from the slot.
     */
    fun clearSlot() {
        when (val rules = composition.tournament.rules.compositionRules) {
            is PointsCompositionRules -> composition[index] = null
            is DraftCompositionRules ->
                if (index in rules.positions.indices)
                    composition[index] = null
                else
                    composition.removeAt(index)
            is DoctrineCompositionRules -> composition.removeAt(index)

        }
    }


    /**
     * Inserts an empty slot at this index.
     */
    fun insertEmptySlot() {
        composition.insert(index, null)
    }


    /**
     * Removes the slot itself.
     */
    fun removeSlot() {
        composition.removeAt(index)
    }


    /**
     * Returns whether the ship can be moved up one slot.
     */
    fun canMoveUp() = index > 0


    /**
     * Moves the ship up one slot.
     */
    fun moveUp() {
        composition.swap(index, index - 1)
        selectionModel.selectIndex(index - 1)
    }


    /**
     * Returns whether the ship can be moved down one slot.
     */
    fun canMoveDown() = true  // We can always expand the composition


    /**
     * Moves the ship down one slot.
     */
    fun moveDown() {
        composition.swap(index, index + 1)
        selectionModel.selectIndex(index + 1)
    }


    /**
     * Moves the ship to the given slot.
     */
    fun moveTo(targetIndex: Int) {
        composition.move(fromIndex = index, toIndex = targetIndex)
        selectionModel.selectIndex(targetIndex)
    }


    /**
     * Adds another ship like the one in this slot.
     */
    fun addAnother() {
        val thisShip = composition[index] ?: return
        val emptySlot = (index+1 until composition.size).firstOrNull { composition[it] == null } ?:
            (index-1 downTo 0).firstOrNull { composition[it] == null }
        val shipCopy = thisShip.copy()
        if (emptySlot != null)
            composition[emptySlot] = shipCopy
        else
            composition.insert(index+1, shipCopy)
    }


    /**
     * Removes another ship like the one in this slot.
     */
    fun removeOne() {
        val targetIndex = removeOneSlotIndex() ?: return
        if (targetIndex < index)
            composition[targetIndex] = null
        else
            composition.removeAt(targetIndex)
    }


    /**
     * Increases the amount of ships at this slot by one.
     */
    fun increaseAmount() {
        composition[index]?.let {
            it.amount = it.amountOrOne + 1
        }
    }


    /**
     * Decreases the amount of ships at this slot by one (but not to 0).
     */
    fun decreaseAmount() {
        composition[index]?.let {
            it.amount = (it.amountOrOne - 1).coerceAtLeast(1)
        }
    }


    /**
     * Returns whether it's possible to decrease the amount of ships in this slot.
     */
    fun canDecreaseAmount(): Boolean {
        return composition[index]?.let { it.amountOrOne > 1 } ?: false
    }


    /**
     * Returns the index of the slot that will be removed by [removeOne]; `null` if none.
     */
    private fun removeOneSlotIndex(): Int? {
        val thisShip = composition[index] ?: return null
        fun isSameShipAsAt(index: Int): Boolean {
            val s = composition[index]
            return (s != null) && (s.shipType == thisShip.shipType) && (s.fitId == thisShip.fitId)
        }

        return (composition.size-1 downTo index+1).firstOrNull(::isSameShipAsAt) ?:
            (0 until index).firstOrNull(::isSameShipAsAt)
    }


    /**
     * Returns whether the composition has another ship like the one in this slot.
     */
    fun canRemoveOne(): Boolean {
        return removeOneSlotIndex() != null
    }


    /**
     * Toggles the active state of the ship.
     */
    fun toggleActive() {
        val ship = composition[index] ?: return
        val active = !ship.active
        ship.active = active

        // Enable/disable this ship's effect on all other ships in the composition
        val fitContext = TheorycrafterContext.fits
        val fitHandle = ship.fitId?.let { fitContext.handleById(it) } ?: return
        runBlocking {
            val fit = fitContext.engineFitOf(fitHandle)
            val targetFits = composition.ships.mapNotNull {  targetShip ->
                if ((targetShip == null) || (targetShip == ship))
                    return@mapNotNull null
                val targetFitHandle = targetShip.fitId?.let { fitContext.handleById(it) } ?: return@mapNotNull null
                fitContext.engineFitOf(targetFitHandle)
            }

            fitContext.modifyAndSave {
                for (targetFit in targetFits) {
                    val commandEffect = targetFit.remoteEffects.command.allExcludingAuxiliary.find { it.source == fit }
                    val friendlyEffect = targetFit.remoteEffects.friendly.allExcludingAuxiliary.find { it.source == fit }

                    commandEffect?.setEnabled(active)
                    friendlyEffect?.setEnabled(active)
                }
            }
        }
    }


}


/**
 * The shortcut to start editing the fit associated with a composition ship.
 */
private val EditFitKeyShortcut = KeyShortcut.anyOf(
    keys = listOf(Key.Enter, Key.NumPadEnter, Key.F2),
    keyModifier = KeyboardModifierMatcher.Alt
)


/**
 * The shortcut for inserting an empty ship slot.
 */
private val InsertEmptySlotKeyShortcut = KeyShortcut.anyOf(
    keys = listOf(Key.Plus, Key.NumPadAdd, Key.Equals),  // Allow = to match the + key on the regular numbers row
    keyModifier = KeyboardModifierMatcher.Command
)


/**
 * The shortcut for removing a ship slot.
 */
private val RemoveSlotKeyShortcuts = FitEditorKeyShortcuts.ClearSlot +
        KeyShortcut.anyOf(listOf(Key.Minus, Key.NumPadSubtract)) +
        KeyShortcut.anyOf(listOf(Key.Minus, Key.NumPadSubtract), KeyboardModifierMatcher.Command)


/**
 * The slot context action to insert an empty ship slot.
 */
private fun SlotContextAction.Companion.insertEmptySlot(actions: ShipSlotActions) =
    SlotContextAction(
        displayName = "Insert Empty Slot",
        icon = { Icons.Add() },
        shortcuts = InsertEmptySlotKeyShortcut,
        action = actions::insertEmptySlot
    )


/**
 * The slot context action to remove a ship slot.
 */
private fun SlotContextAction.Companion.removeEmptySlot(actions: ShipSlotActions) =
    SlotContextAction(
        displayName = "Remove Slot",
        icon = { Icons.Remove() },
        shortcuts = RemoveSlotKeyShortcuts,
        action = actions::removeSlot
    )


/**
 * Returns the [FitHandle] associated with the given composition ship.
 */
@Composable
private fun Composition.Ship.fitHandle(): FitHandle? {
    val fitId = this.fitId ?: return null
    return remember(fitId, TheorycrafterContext.fits.handlesKey) {
        TheorycrafterContext.fits.handleById(fitId)
    }
}


/**
 * Returns whether in the given tournament, there are no dedicated ship slots.
 */
private fun Composition.isFreeSlotTournament() = tournament.rules.compositionRules !is DraftCompositionRules


/**
 * The row displaying a non-empty ship slot.
 */
@Composable
private fun GridScope.ShipSlotRow(
    composition: Composition,
    shipIndex: Int,
    ship: Composition.Ship,
    shipCost: Int?,
    shipIllegalReason: String?,
    selectionModel: SingleItemSelectionModel,
    actions: ShipSlotActions
) {
    val fitOpener = LocalFitOpener.current
    val fitHandle = ship.fitHandle()
    val canRemoveOne by remember(actions, composition) {
        derivedStateOf {
            when {
                composition.isDoctrine -> actions.canDecreaseAmount()
                composition.isFreeSlotTournament() -> actions.canRemoveOne()
                else -> false
            }
        }
    }
    val contextActions = remember(composition, actions, fitOpener, fitHandle, canRemoveOne) {
        val isDoctrine = composition.isDoctrine
        val isFreeSlotTournament = composition.isFreeSlotTournament()
        buildList {
            add(SlotContextAction.toggleEnabledState(actions::toggleActive))
            add(SlotContextAction.openFit(fitOpener, fitHandle))
            add(SlotContextAction.Separator)
            if (isFreeSlotTournament) {
                add(SlotContextAction.insertEmptySlot(actions))
                add(SlotContextAction.moveSlotUp(enabled = actions.canMoveUp(), action = actions::moveUp))
                add(SlotContextAction.moveSlotDown(enabled = actions.canMoveDown(), action = actions::moveDown))
                add(SlotContextAction.Separator)
            }
            add(SlotContextAction.clear(actions::clearSlot))
            if (isDoctrine) {
                add(SlotContextAction.addOneItem(actions::increaseAmount))
                add(
                    SlotContextAction.removeOneItem(
                        removeOne = actions::decreaseAmount,
                        enabled = canRemoveOne,
                        showInContextMenu = true
                    )
                )
            } else if (isFreeSlotTournament) {
                add(SlotContextAction.addOneItem(actions::addAnother))
                add(
                    SlotContextAction.removeOneItem(
                        removeOne = actions::removeOne,
                        enabled = canRemoveOne,
                        showInContextMenu = true
                    )
                )
            }
        }
    }

    val isEditingState = remember { mutableStateOf(false) }
    var isEditingFit by remember { mutableStateOf(false) }

    fun startEditingFit() {
        isEditingFit = true
        isEditingState.value = true
    }

    SlotRow(
        modifier = Modifier
            .thenIf(!isEditingState.value) {
                onMousePress(clickCount = ClickCount.DOUBLE, keyboardModifier = KeyboardModifierMatcher.Alt) {
                    startEditingFit()
                }
                .onKeyShortcut(EditFitKeyShortcut) {
                    startEditingFit()
                }
            },
        modifierWhenNotEditing = Modifier
            .thenIf(composition.isFreeSlotTournament()) {
                dragShipToReorder(ship, actions)
            },
        contextActions = contextActions,
        invalidityReason = shipIllegalReason,
        isEditingState = isEditingState,
        editedRowContent = { onEditingCompleted ->
            if (isEditingFit) {
                EditedFitRowContent(
                    ship = ship,
                    shipCost = shipCost,
                    selectionModel = selectionModel,
                    onFitSelected = {
                        ship.fitId = it?.fitId
                        isEditingFit = false
                        onEditingCompleted()
                    },
                    onEditingCancelled = {
                        isEditingFit = false
                        onEditingCompleted()
                    }
                )
            }
            else {
                EditedShipTypeRowContent(
                    composition = composition,
                    shipIndex = shipIndex,
                    selectionModel = selectionModel,
                    currentShip = ship,
                    onShipSelected = { shipType, amount ->
                        actions.setShip(shipType, amount)
                        onEditingCompleted()
                    },
                    onEditingCancelled = onEditingCompleted
                )
            }
        },
    ) {
        ShipSlotContent(
            ship = ship,
            shipIndex = shipIndex,
            tournament = composition.tournament,
            shipCost = shipCost,
            actions = actions,
            onFitDoubleClicked = {
                startEditingFit()
            }
        )
    }
}


/**
 * Returns a modifier for dragging the given ship to place it at a new slot.
 */
context(GridScope)
@Composable
private fun Modifier.dragShipToReorder(
    ship: Composition.Ship,
    actions: ShipSlotActions
): Modifier {

    return this.dragRowToReorder(
        draggableContent = { DraggedShipSlotRepresentation(ship) },
        canMoveToRow = { it <= ship.composition.visibleSlotCount },
        onDrop = { draggedRowIndex, dropRowIndex ->
            val targetRowIndex = if (dropRowIndex > draggedRowIndex) dropRowIndex-1 else dropRowIndex
            actions.moveTo(targetRowIndex)
        }
    )
}


/**
 * The dragged module slot.
 */
@Composable
private fun GridScope.DraggedShipSlotRepresentation(ship: Composition.Ship) {
    draggedRow(
        rowIndex = 0,
        modifier = Modifier
            .fillMaxWidth()
            .background(TheorycrafterTheme.colors.draggedSlotBackground())
            .padding(SLOT_ROW_PADDING)
            .height(TheorycrafterTheme.sizes.fitEditorSlotRowHeight)  // Make the height independent of editing
    ) {
        emptyCell(GridCols.STATE_ICON)
        emptyCell(GridCols.SHIP_ICON)
        cell(GridCols.SHIP, colSpan = GridCols.COUNT - GridCols.SHIP, contentAlignment = Alignment.CenterStart) {
            Text(ship.shipType.name)
        }
    }
}


/**
 * Returns the displayed ship type name.
 */
private val Composition.Ship.displayedNameWithAmount: String
    get() = shipType.shortName().let { shipShortName ->
        if (composition.isDoctrine)
            shipShortName.withAmount(amountOrOne)
        else
            shipShortName
    }


/**
 * The content of a non-empty composition ship slot.
 */
@Composable
private fun GridScope.GridRowScope.ShipSlotContent(
    ship: Composition.Ship,
    shipIndex: Int,
    tournament: Tournament,
    shipCost: Int?,
    actions: ShipSlotActions,
    onFitDoubleClicked: () -> Unit,
) {
    val fitHandle = ship.fitHandle()
    val fit by produceState<Fit?>(null, fitHandle) {
        value = fitHandle?.let { TheorycrafterContext.fits.engineFitOf(it) }
    }

    cell(GridCols.STATE_ICON, modifier = Modifier) {
        Icons.ItemEnabledState(
            enabled = ship.active,
            modifier = Modifier
                .onMousePress(consumeEvent = true) {  // Consume to prevent selecting the row
                    actions.toggleActive()
                }
                .onMousePress(MouseButton.Middle, consumeEvent = true) {  // Consume just in case
                    actions.toggleActive()
                }
        )
    }

    ShipIconCell(ship)

    cell(
        cellIndex = GridCols.SHIP,
        modifier = defaultCellModifier
            .shipTypeTraitsTooltip(
                shipType = ship.shipType,
                delayMillis = 1000
            )
    ) {
        val compRules = tournament.rules.compositionRules
        val position = (compRules as? DraftCompositionRules)?.positions?.getOrNull(shipIndex)
        if (position == null) {
            SingleLineText(ship.displayedNameWithAmount)
        } else {
            val lightStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.ExtraLight)
            SingleLineText(
                text = buildAnnotatedString {
                    withStyle(lightStyle.toSpanStyle()) {
                        append(position.prefix)
                        append(" ")
                    }
                    append(ship.shipType.shortName())
                }
            )
        }
    }

    ShipCostCell(ship, shipCost)
    FitCell(
        fitHandle = fitHandle,
        fit = fit,
        modifier = Modifier
            .onMousePress(clickCount = ClickCount.DOUBLE, consumeEvent = true) {
                onFitDoubleClicked()
            }
    )

    cell(GridCols.EHP, contentAlignment = Alignment.CenterEnd) {
        if (ship.active) {
            fit?.let { fit ->
                SingleLineText(fit.defenses.ehp.asHitPoints(ehp = true, withUnits = false))
            }
        }
    }

    cell(GridCols.ACTIVE_TANK) {
        if (ship.active) {
            fit?.let { fit ->
                with(fit.defenses) {
                    fun ItemDefense.hasRepairs() = (hpRepairedLocally > 0) || (hpRepairedRemotely > 0)
                    val includeShieldRegen = shield.hasRepairs() || (!armor.hasRepairs() && !structure.hasRepairs())
                    val totalReps = fit.defenses.ehpRepairedLocally +
                            fit.defenses.ehpRepairedByRemoteEffects +
                            (if (includeShieldRegen) shield.peakRegenEhpPerSecond else 0.0)
                    SingleLineText(totalReps.asHitpointsPerSecond(ehp = true, withUnits = false))
                }
            }
        }
    }

    cell(GridCols.DPS, contentAlignment = Alignment.CenterEnd) {
        if (ship.active) {
            fit?.let {
                val dps = it.firepower.totalDps * ship.amountOrOne
                SingleLineText(dps.asDps(withUnits = false))
            }
        }
    }
}


/**
 * A cell showing the ship's icon.
 */
@Composable
private fun GridScope.GridRowScope.ShipIconCell(ship: Composition.Ship) {
    cell(GridCols.SHIP_ICON) {
        Icons.EveItemType(
            itemType = ship.shipType,
            modifier = Modifier
                .size(TheorycrafterTheme.sizes.eveTypeIconSmall)
                .padding(2.dp)
        )
    }
}


/**
 * The cell showing the ship's cost.
 */
@Composable
private fun GridScope.GridRowScope.ShipCostCell(
    ship: Composition.Ship,
    shipCost: Int?,
) {
    if (shipCost != null) {
        cell(GridCols.COST, modifier = defaultCellModifier.then(COST_COLUMN_PADDING_MODIFIER)) {
            if (ship.active) {
                SingleLineText(text = "$shipCost")
            }
        }
    } else {
        emptyCell(GridCols.COST)
    }
}


/**
 * The cell showing the ships' fit.
 */
@Composable
private fun GridScope.GridRowScope.FitCell(
    fitHandle: FitHandle?,
    fit: Fit?,
    modifier: Modifier
) {
    cell(
        cellIndex = GridCols.FIT,
        modifier = modifier
    ) {
        VerticallyCenteredRow {
            if (fit != null) {
                SimpleAnalysisIconOrNone(
                    fit = fit,
                    tooltipPlacement = EasyTooltipPlacement.ElementBottomCenter(
                        offset = DpOffset(x = 0.dp, y = TheorycrafterTheme.spacing.medium)
                    )
                )
            }
            SingleLineText(
                text = fitHandle?.name ?: "None",
                fontWeight = if (fitHandle == null) FontWeight.ExtraLight else null,
                modifier = Modifier
                    .thenIf(fit != null) {
                        tooltip(text = { fitUtilitiesSummary(fit!!) })
                    }
            )
        }
    }
}


/**
 * The content of the row when the ship type is being edited.
 */
@Composable
private fun GridScope.GridRowScope.EditedShipTypeRowContent(
    composition: Composition,
    shipIndex: Int,
    selectionModel: SingleItemSelectionModel,
    currentShip: Composition.Ship?,
    onShipSelected: (ShipType, amount: Int?) -> Unit,
    onEditingCancelled: () -> Unit
) {
    emptyCell(cellIndex = GridCols.STATE_ICON)  // No state icon
    emptyCell(cellIndex = GridCols.SHIP_ICON)  // No ship icon
    cell(
        cellIndex = GridCols.SHIP,
        colSpan = GridCols.COST - GridCols.SHIP + 1
    ) {
        when (val compRules = composition.tournament.rules.compositionRules) {
            is PointsCompositionRules ->
                PointsBasedShipTypeSelector(
                    composition = composition,
                    rules = compRules,
                    selectionModel = selectionModel,
                    currentShipType = currentShip?.shipType,
                    onShipTypeSelected = { onShipSelected(it, null) },
                    onEditingCancelled = onEditingCancelled
                )
            is DraftCompositionRules ->
                PositionBasedShipTypeSelector(
                    shipIndex = shipIndex,
                    rules = compRules,
                    selectionModel = selectionModel,
                    onShipTypeSelected = { onShipSelected(it, null) },
                    onEditingCancelled = onEditingCancelled
                )
            is DoctrineCompositionRules ->
                DoctrineShipTypeSelector(
                    currentShip = currentShip,
                    selectionModel = selectionModel,
                    onShipSelected = onShipSelected,
                    onEditingCancelled = onEditingCancelled
                )
        }
    }
}


/**
 * The content of the row when the fit is being edited.
 */
@Composable
private fun GridScope.GridRowScope.EditedFitRowContent(
    ship: Composition.Ship,
    shipCost: Int?,
    selectionModel: SingleItemSelectionModel,
    onFitSelected: (FitHandle?) -> Unit,
    onEditingCancelled: () -> Unit
) {
    emptyCell(cellIndex = GridCols.STATE_ICON)  // No state icon
    ShipIconCell(ship)
    cell(cellIndex = GridCols.SHIP) {
        Text(ship.displayedNameWithAmount)
    }
    if (shipCost != null) {
        ShipCostCell(ship, shipCost)
    }
    val fitSelectorColSpan = 3
    cell(
        cellIndex = GridCols.FIT,
        colSpan = fitSelectorColSpan
    ) {
        FitSelector(
            shipType = ship.shipType,
            selectionModel = selectionModel,
            onFitSelected = onFitSelected,
            onEditingCancelled = onEditingCancelled
        )
    }

    // This is needed to limit the width of the fit selector
    val restColIndex = GridCols.FIT + fitSelectorColSpan
    emptyCell(
        cellIndex = restColIndex,
        colSpan = GridCols.COUNT - restColIndex
    )
}


/**
 * A ship type selection widget for [PointsCompositionRules] tournaments.
 */
@Composable
private fun PointsBasedShipTypeSelector(
    composition: Composition,
    rules: PointsCompositionRules,
    selectionModel: SingleItemSelectionModel,
    currentShipType: ShipType?,
    onShipTypeSelected: (ShipType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val usedShipTypesWithoutCurrent = remember(composition, currentShipType) {
        composition.activeOrNullShipTypes() - currentShipType
    }
    val marginalShipCostComputation = remember(rules, usedShipTypesWithoutCurrent) {
        rules.marginalShipCostComputation(usedShipTypesWithoutCurrent)
    }

    val autoSuggest = remember(rules, marginalShipCostComputation) {
        AutoSuggest { text ->
            TheorycrafterContext.autoSuggest.shipTypes(text)?.filter { rules.isShipLegal(it) }
        }.onEmptyQueryReturn {
            val usedPointsWithoutCurrent = rules.shipsCost(usedShipTypesWithoutCurrent).sum()
            val remainingPoints = rules.maxCompositionCost - usedPointsWithoutCurrent
            val marginalCostByShipType = rules.legalShips.associateWith {
                marginalShipCostComputation.marginalCostOf(it)
            }
            rules.legalShips
                .filter { marginalCostByShipType[it]!! <= remainingPoints }
                .sortedByDescending { marginalCostByShipType[it] }
        }
    }

    Selector(
        onItemSelected = onShipTypeSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { shipType, _ ->
            DefaultSuggestedEveItemTypeIcon(shipType)
            Text(text = shipType.shortName())
            Spacer(Modifier.weight(1f).widthIn(min = TheorycrafterTheme.spacing.medium))
            Text(text = marginalShipCostComputation.marginalCostOf(shipType).toString())
        },
        autoSuggestHorizontalAnchorPadding = DpRect(
            left = AutoSuggestHorizontalAnchorPaddingWithIcon.left,
            right = TheorycrafterTheme.spacing.horizontalEdgeMargin -
                    COST_COLUMN_PADDING.calculateEndPadding(LayoutDirection.Ltr),
            top = 0.dp,
            bottom = 0.dp
        ),
        hint = "Ship type",
        selectNextPrimaryRow = {
            selectionModel.selectNext()
        }
    )
}


/**
 * A ship type selection widget for [DraftCompositionRules] tournaments.
 */
@Composable
private fun PositionBasedShipTypeSelector(
    shipIndex: Int,
    rules: DraftCompositionRules,
    selectionModel: SingleItemSelectionModel,
    onShipTypeSelected: (ShipType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val position = rules.positions.getOrNull(shipIndex)
    val autoSuggest = remember(position, shipIndex) {
        val shipTypesForPosition = position?.legalShipTypes ?: rules.allLegalShipTypes
        val sortedShipTypesForPosition = shipTypesForPosition.sortedBy { it.name }
        AutoSuggest { text ->
            TheorycrafterContext.autoSuggest
                .shipTypes(text)
                ?.filter { it in shipTypesForPosition }
        }.onEmptyQueryReturn {
            sortedShipTypesForPosition
        }
    }

    Selector(
        onItemSelected = onShipTypeSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { shipType, _ ->
            DefaultSuggestedEveItemTypeIcon(shipType)
            Text(text = shipType.shortName())
        },
        autoSuggestHorizontalAnchorPadding = DpRect(
            left = AutoSuggestHorizontalAnchorPaddingWithIcon.left,
            right = -TheorycrafterTheme.spacing.horizontalEdgeMargin,
            top = 0.dp,
            bottom = 0.dp
        ),
        hint = if (position != null) "${position.prefix} ${position.name}" else "Ship type",
        selectNextPrimaryRow = {
            selectionModel.selectNext()
        }
    )
}


/**
 * A ship type selection widget for [DraftCompositionRules] tournaments.
 */
@Composable
private fun DoctrineShipTypeSelector(
    currentShip: Composition.Ship?,
    selectionModel: SingleItemSelectionModel,
    onShipSelected: (ShipType, amount: Int) -> Unit,
    onEditingCancelled: () -> Unit
) {
    var amount by remember { mutableIntStateOf(1) }
    val currentShipName = currentShip?.shipType?.shortName()
    Selector(
        onValueChange = {
            amount = parseItemWithAmountText(currentShipName, it).amount
        },
        onItemSelected = { onShipSelected(it, amount) },
        onEditingCancelled = onEditingCancelled,
        autoSuggest = TheorycrafterContext.autoSuggest.shipTypes,
        suggestedItemContent = { shipType, _ ->
            DefaultSuggestedEveItemTypeIcon(shipType)
            Text(text = shipType.shortName().withAmount(amount))
        },
        autoSuggestHorizontalAnchorPadding = DpRect(
            left = AutoSuggestHorizontalAnchorPaddingWithIcon.left,
            right = -TheorycrafterTheme.spacing.horizontalEdgeMargin,
            top = 0.dp,
            bottom = 0.dp
        ),
        autoSuggestItemToString = {
            // This is never displayed, but when the user selects an item, the result of this function is passed to
            // onValueChange, so we must return something that parses correctly
            it.name.withAmount(amount)
        },
        autoSuggestInputTransform = { parseItemWithAmountText(currentShipName, it).itemName },
        hint = "Ship type",
        selectNextPrimaryRow = {
            selectionModel.selectNext()
        }
    )
}



/**
 * A fit selector widget.
 */
@Composable
private fun FitSelector(
    shipType: ShipType,
    selectionModel: SingleItemSelectionModel,
    onFitSelected: (FitHandle?) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = remember(TheorycrafterContext.fits.handlesKey) {
        AutoSuggest { text ->
            val fits = TheorycrafterContext.queryFits(text) ?: TheorycrafterContext.fits.handles
            buildList {
                add(null)
                for (fit in fits) {
                    if (fit.shipTypeId == shipType.itemId)
                        add(fit)
                }
            }
        }
    }

    Selector(
        onItemSelected = onFitSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { fitHandle, _ ->
            Text(text = fitHandle?.name ?: "None")
        },
        autoSuggestHorizontalAnchorPadding = AutoSuggestHorizontalAnchorPadding,
        hint = "Fit name",
        selectNextPrimaryRow = {
            selectionModel.selectNext()
        }
    )
}



/**
 * The row displaying the "Empty Ship Slot".
 */
@Composable
private fun GridScope.EmptyShipSlotRow(
    composition: Composition,
    shipIndex: Int,
    selectionModel: SingleItemSelectionModel,
    actions: ShipSlotActions
) {
    val freeSlotTournament = composition.isFreeSlotTournament()
    val contextActions = if (!freeSlotTournament || (shipIndex == composition.visibleSlotCount - 1))
        emptyList()
    else
        remember(actions) {
            listOf(
                SlotContextAction.insertEmptySlot(actions),
                SlotContextAction.removeEmptySlot(actions),
            )
        }

    SlotRow(
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            EditedShipTypeRowContent(
                composition = composition,
                shipIndex = shipIndex,
                selectionModel = selectionModel,
                currentShip = null,
                onShipSelected = { shipType, amount ->
                    actions.setShip(shipType, amount)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyShipRowContent(
            composition = composition,
            shipIndex = shipIndex
        )
    }
}


/**
 * The content of a slot with no ship.
 */
@Composable
private fun GridScope.GridRowScope.EmptyShipRowContent(
    composition: Composition,
    shipIndex: Int,
) {
    emptyCell(GridCols.STATE_ICON)
    emptyCell(GridCols.SHIP_ICON)

    val rules = composition.tournament.rules.compositionRules
    val text = when {
        rules is DraftCompositionRules ->
            if (shipIndex in rules.positions.indices) {
                val pos = rules.positions[shipIndex]
                "${pos.prefix} ${pos.name}"
            }
            else
                "Replacement Ship"
        else -> "Empty Ship Slot"
    }
    cell(GridCols.SHIP, colSpan = 3) {
        Text(
            text = text,
            style = LocalTextStyle.current.copy(fontWeight = FontWeight.ExtraLight),
        )
    }
}


/**
 * Returns the active ships and their corresponding fits.
 */
@Composable
fun activeFitsByShip(composition: Composition): Map<Composition.Ship, Fit> {
    val fits = TheorycrafterContext.fits
    val fitHandleByShip by remember(composition) {
        derivedStateOf {
            composition.ships.mapNotNull {
                val fitId = it?.fitId ?: return@mapNotNull null
                val fitHandle = fits.handleById(fitId) ?: return@mapNotNull null
                Pair(it, fitHandle)
            }.toMap()
        }
    }

    val fitByShip = produceState(initialValue = emptyMap(), fitHandleByShip, fits.handlesKey) {
        value = withContext(Dispatchers.Default) {
            buildMap {
                for ((ship, fitHandle) in fitHandleByShip) {
                    val fit = fits.engineFitOfOrNull(fitHandle)
                    if (fit != null)
                        put(ship, fit)
                }
            }
        }
    }.value

    // We must filter the inactive and deleted ships last, so that when the active state of a ship changes,
    // the caller receives the updated list on the very first recomposition.
    // If instead we filter in fitHandleByShip, the first recomposition of the caller after a change in active state
    // (assuming it reads the active state) would happen with the previous list because produceState hasn't finished
    // producing the new list yet.
    return remember(fitByShip, fitHandleByShip) {
        derivedStateOf {
            fitByShip.filterKeys { ship ->
                ship.active && (fitHandleByShip[ship]?.isDeleted == false) // Also make sure the fit hasn't been deleted
            }
        }
    }.value
}


/**
 * A row with the composition summary (number of ships, cost etc.)
 */
@Composable
private fun SummationRow(
    composition: Composition,
    shipsCost: List<Int>?,
    modifier: Modifier
) {
    SimpleGridHeaderRow(
        modifier = modifier,
        columnWidths = ColumnWidths,
        defaultCellContentAlignment = ColumnAlignment::get
    ) {
        val rules = composition.tournament.rules.compositionRules
        val activeFitsByShip = activeFitsByShip(composition)

        EmptyCell(GridCols.STATE_ICON)
        EmptyCell(GridCols.SHIP_ICON)

        if (composition.isDoctrine) {
            val shipCount = composition.ships.sumOf { if (it?.active == true) it.amountOrOne else 0 }
            TextCell(
                index = GridCols.SHIP,
                text = "$shipCount ships",
            )
            EmptyCell(GridCols.COST)
        } else if (rules is PointsCompositionRules) {
            val shipCount = composition.ships.count { it?.active == true }
            val errorStyle = TextStyle(color = TheorycrafterTheme.colors.base().errorContent)
            TextCell(
                index = GridCols.SHIP,
                text = "$shipCount ships",
                style = if (shipCount > rules.maxCompositionSize) errorStyle else LocalTextStyle.current
            )

            if (shipsCost != null) {
                val totalShipCost = shipsCost.sum()
                TextCell(
                    index = GridCols.COST,
                    text = "$totalShipCost pts",
                    style = if (totalShipCost > rules.maxCompositionCost) errorStyle else LocalTextStyle.current,
                    modifier = defaultCellModifier.then(COST_COLUMN_PADDING_MODIFIER)
                )
            } else {
                EmptyCell(GridCols.COST)
            }
        } else {
            EmptyCell(GridCols.SHIP)
            EmptyCell(GridCols.COST)
        }

        EmptyCell(GridCols.FIT)

        TextCell(
            index = GridCols.EHP,
            text = activeFitsByShip.entries.sumOf { (ship, fit) ->
                ship.amountOrOne * fit.defenses.ehp
            }.asHitPoints(ehp = true)
        )

        EmptyCell(GridCols.ACTIVE_TANK)

        TextCell(
            index = GridCols.DPS,
            text = activeFitsByShip.entries.sumOf { (ship, fit) ->
                ship.amountOrOne * fit.firepower.totalDps
            }.asDps()
        )
    }
}


/**
 * Displays and allows editing the composition note.
 */
@Composable
private fun CompositionNotes(
    composition: Composition,
    modifier: Modifier
) {
    TheorycrafterTheme.TextField(
        modifier = modifier,
        value = composition.note,
        minLines = 2,
        onValueChange = { composition.note = it },
        placeholder = { Text("Notes") }
    )
}


/**
 * The selection model for the given composition.
 */
private class CompositionShipsSelectionModel(
    val composition: Composition,
): SingleItemSelectionModel(initialSelectedIndex = 0) {

    override val maxSelectableIndex: Int
        get() = composition.visibleSlotCount - 1

}