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

package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import compose.utils.HSpacer
import compose.widgets.GridScope
import compose.widgets.SingleLineText
import eve.data.*
import eve.data.typeid.isNaniteRepairPaste
import theorycrafter.LocalTheorycrafterWindowManager
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.CargoItem
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.fitting.maxLoadedChargeAmount
import theorycrafter.fitting.utils.associateWithIndex
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.shortName
import theorycrafter.ui.tooltip
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*


/**
 * The information regarding a cargo item that we remember in order to add and remove it.
 */
private class CargoItemInfo(
    private val slotIndex: Int,
    private val type: EveItemType,
    private val amount: Int,
) {


    /**
     * Adds the cargo item to the fit.
     */
    fun addTo(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            fit.addCargoItem(type, amount = amount, index = slotIndex)
        }
    }


    /**
     * Removes the cargo item from the fit.
     */
    fun removeFrom(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            fit.removeCargoItem(fit.cargohold.contents[slotIndex])
        }
    }

}


/**
 * A [FitEditingAction] that replaces a cargo item at a given slot with another.
 */
private class CargoItemReplacement(
    private val fit: Fit,
    private val removed: CargoItemInfo?,
    private val added: CargoItemInfo?
): FitEditingAction() {

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

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

}


/**
 * Returns a [FitEditingAction] that adds a new cargo item to the fit.
 */
fun addCargoItemAction(fit: Fit, itemType: EveItemType, amount: Int): FitEditingAction {
    val slotIndex = fit.cargohold.contents.size

    return CargoItemReplacement(
        fit = fit,
        removed = null,
        added = CargoItemInfo(slotIndex, itemType, amount)
    )
}


/**
 * The per-item information for adding to the cargohold in bulk.
 */
private class BulkCargoAdditionInfo(
    val slotIndex: Int,
    val editExistingItem: Boolean,
    val type: EveItemType,
    val amount: Int
)


/**
 * Returns a [FitEditingAction] that adds cargo to the fit in bulk.
 *
 * If a [CargoItem] with the corresponding item type already exists, it is added to instead of creating a new item.
 */
fun bulkAddCargoAction(fit: Fit, itemTypesAndAmounts: Collection<Pair<EveItemType, Int>>): FitEditingAction {
    val currentItemsSlotByType = fit.cargohold.contents
        .map { it.type }
        .associateWithIndex()
    var addedSlotIndex = fit.cargohold.contents.size
    val additionInfos = itemTypesAndAmounts.map { (itemType, amount) ->
        val existingCargoItemSlot = currentItemsSlotByType[itemType]
        val slotIndex = existingCargoItemSlot ?: addedSlotIndex++
        BulkCargoAdditionInfo(slotIndex, editExistingItem = existingCargoItemSlot != null, itemType, amount)
    }

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            for (info in additionInfos) {
                if (info.editExistingItem) {
                    val item = fit.cargohold.contents[info.slotIndex]
                    item.setAmount(item.amount + info.amount)
                }
                else {
                    fit.addCargoItem(itemType = info.type, amount = info.amount, index = info.slotIndex)
                }
            }
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            for (info in additionInfos.asReversed()) {
                if (info.editExistingItem) {
                    val item = fit.cargohold.contents[info.slotIndex]
                    item.setAmount(item.amount - info.amount)
                }
                else {
                    fit.removeCargoItem(fit.cargohold.contents[info.slotIndex])
                }
            }
        }

    }
}


/**
 * Returns a [FitEditingAction] that replaces a cargo item with a new one.
 */
private fun replaceCargoItemAction(fit: Fit, slotIndex: Int, itemType: EveItemType, amount: Int): FitEditingAction? {
    val cargoItem = fit.cargohold.contents[slotIndex]
    if ((cargoItem.type == itemType) && (cargoItem.amount == amount))
        return null

    return CargoItemReplacement(
        fit = fit,
        removed = CargoItemInfo(slotIndex, cargoItem.type, cargoItem.amount),
        added = CargoItemInfo(slotIndex, itemType, amount)
    )
}


/**
 * Returns a [FitEditingAction] that removes a cargo item from the fit.
 */
private fun removeCargoItemAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val cargoItem = fit.cargohold.contents[slotIndex]
    return CargoItemReplacement(
        fit = fit,
        removed = CargoItemInfo(slotIndex, cargoItem.type, cargoItem.amount),
        added = null
    )
}


/**
 * Returns a [FitEditingAction] that sets the amount of the given cargo item.
 */
fun setCargoAmountAction(fit: Fit, slotIndex: Int, amount: Int): FitEditingAction? {
    val cargoItem = fit.cargohold.contents[slotIndex]
    val prevAmount = cargoItem.amount
    if (prevAmount == amount)
        return null

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.cargohold.contents[slotIndex].setAmount(amount)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.cargohold.contents[slotIndex].setAmount(prevAmount)
        }

    }
}


/**
 * Returns a [FitEditingAction] that moves the cargo item from [currentIndex] to [newIndex].
 */
fun moveCargoItemAction(fit: Fit, currentIndex: Int, newIndex: Int): FitEditingAction? {
    if (currentIndex == newIndex)
        return null

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.cargohold.contents[currentIndex].moveTo(newIndex)
            selectionModel?.selectCargoSlot(newIndex)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.cargohold.contents[newIndex].moveTo(currentIndex)
            selectionModel?.selectCargoSlot(currentIndex)
        }

    }
}


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


/**
 * A cargo item selection widget.
 */
@Composable
private fun GridScope.GridRowScope.CargoItemSelectorRow(
    currentItem: CargoItem?,
    onItemSelected: (EveItemType, amount: Int) -> Unit,
    onEditingCancelled: () -> Unit
) {
    var editingAmountOnly by remember { mutableStateOf(false) }
    val autoSuggest = TheorycrafterContext.autoSuggest.cargoItemTypes
        .withActiveTournamentRules()
        .filterResults {
            // When editing only the amount, filter out everything except the current item.
            // This is needed because the autosuggest can match other items even when given the full item name.
            !editingAmountOnly || (it == currentItem?.type)
        }
    var amount by remember { mutableIntStateOf(1) }
    val fit = LocalFit.current

    ItemSelectorRow(
        onValueChange = {
            val parsedItem = parseItemWithAmountText(currentItem?.name, it)
            amount = parsedItem.amount
            editingAmountOnly = parsedItem.hasAmountOnly
        },
        onItemSelected = { onItemSelected(it, amount) },
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        autoSuggestItemToString = {
            // This is never displayed, but when the user selects an item, the result of this function is passed to
            // onValueChange, so we must return something that parses correctly
            it.name.withAmount(amount)
        },
        autoSuggestInputTransform = { parseItemWithAmountText(currentItem?.name, it).itemName },
        suggestedItemsFooter = {
            FitEditorSuggestedItemsFooter {
                SingleLineText("Available:")

                HSpacer(TheorycrafterTheme.spacing.larger)

                FitResourceAvailableInAutoSuggestFooter(
                    resource = fit.cargohold.capacity,
                    currentItemNeed = currentItem?.volume,
                    valueToText = { it.asVolume(withUnits = true) },
                )

                if (TheorycrafterContext.settings.prices.showInFitEditorSuggestedItems) {
                    Spacer(Modifier.width(TheorycrafterTheme.sizes.fitEditorSuggestedItemsPriceWidth))
                }
            }
        },
        suggestedItemContent = { itemType, _ ->
            DefaultSuggestedEveItemTypeIcon(itemType)
            Text(text = itemType.name.withAmount(amount), Modifier.weight(1f))
            HSpacer(TheorycrafterTheme.spacing.medium)
            ItemResourceNeedInSuggestedItemsRow(
                resource = fit.cargohold.capacity,
                resourceNeed = itemType.volume * amount,
                currentItemNeed = currentItem?.volume,
                valueToText = { it.asVolume(withUnits = true) },
            )

            if (TheorycrafterContext.settings.prices.showInFitEditorSuggestedItems) {
                ItemPrice(
                    itemType = itemType,
                    amount = amount,
                    textAlign = TextAlign.End,
                    modifier = Modifier.width(TheorycrafterTheme.sizes.fitEditorSuggestedItemsPriceWidth)
                )
            }
        },
        hint = if (currentItem == null)
            "Amount x Item name"
        else
            "Updated amount or Amount x Item name",
        marketGroups = with(TheorycrafterContext.eveData.marketGroups) {
            // This should be synced with EveData.cargoItemTypes
            remember {
                listOf(
                    ammoAndCharges,
                    deployableStructures,
                    drones,
                    filaments,
                    implants,
                    boosters,
                    rigs,
                    shipEquipment,
                    ships,
                    skills,
                    subsystems,
                )
            }
        },
        marketGroupsTitle = "Cargo",
        itemFilter = TheorycrafterContext.tournaments.activeRules?.let { rules ->
            { rules.isCargoItemLegal(it) }
        },
        // There's no carousel auto-suggest here, but for consistency with other item types, only show the mini market
        // if the slot content is empty. Also to not distract the user from the ability to update the amount of an
        // existing by typing just an amount.
        showMarketInitially = currentItem == null
    )
}


/**
 * The text we display for the name of a cargo item.
 */
fun CargoItem.displayedText(): String {
    val displayName = when(val itemType = type) {
        is ImplantType -> itemType.shortName()
        else -> itemType.name
    }
    return "${amount}x $displayName"
}


/**
 * The row for a slot with a (non-`null`) cargo item.
 */
@Composable
private fun GridScope.GridRowScope.CargoSlotContent(cargoItem: CargoItem) {
    emptyCell(cellIndex = GridCols.STATE_ICON)
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(cargoItem)
    }
    cell(cellIndex = GridCols.NAME, colSpan = 3) {
        SingleLineText(cargoItem.displayedText())
    }
    cell(cellIndex = GridCols.RANGE) {
        when (val type = cargoItem.type) {
            is BoosterType -> Text(text = type.slotName())
            is ChargeType -> {
                val canLoadCharge by remember(cargoItem) {
                    derivedStateOf {
                        with(TheorycrafterContext.eveData) {
                            val modules = cargoItem.fit.modules.all
                            type.isNaniteRepairPaste() || modules.any { it.type.canLoadCharge(type) }
                        }
                    }
                }
                if (!canLoadCharge) {
                    SingleLineText(
                        text = "Not usable",
                        color = TheorycrafterTheme.colors.base().warningContent,
                        modifier = Modifier
                            .tooltip("No fitted modules can load this charge")
                    )
                }
            }
        }
    }
    cell(cellIndex = GridCols.EFFECT) {
        SingleLineText(
            text = cargoItem.volume.asVolume(),
            overflow = TextOverflow.Visible,
        )
    }
    PriceCell(cargoItem.type, amount = cargoItem.amount)
}


/**
 * Bundles the actions passed to [CargoSlotRow].
 */
private class CargoSlotActions(
    val put: (EveItemType, Int) -> Unit,
    val clear: () -> Unit,
    val addAmount: (Int) -> Unit,
    val moveTo: (Int) -> Unit,
    val canMoveBy: (Int) -> Boolean,
    val moveBy: (Int) -> Unit,
) {

    fun canMoveUp() = canMoveBy(-1)

    fun moveUp() = moveBy(-1)

    fun canMoveDown() = canMoveBy(1)

    fun moveSlotDown() = moveBy(1)

}


/**
 * Computes the amount of the given item to put into the cargohold when pasting and the amount has not been specified.
 */
private fun amountToAddWhenPasting(fit: Fit, itemType: EveItemType): Int {
    if (itemType is ChargeType) {
        return fit.modules.all.sumOf { module ->
            maxLoadedChargeAmount(module.type, itemType) ?: 0
        }
    }

    return 1
}


/**
 * The [SlotContextAction] for pasting into a cargo slot.
 */
private fun SlotContextAction.Companion.pasteCargo(
    clipboard: Clipboard,
    fit: Fit,
    putItem: (EveItemType, Int) -> Unit,
) = pasteFromClipboard(
    clipboard = clipboard,
    itemFromClipboardText = ::cargoItemFromClipboardText,
    pasteItem = { (itemType, amount) ->
        val actualAmount = amount ?: amountToAddWhenPasting(fit, itemType)
        putItem(itemType, actualAmount)
    }
)


/**
 * The row displaying a non-empty cargo slot.
 */
@Composable
private fun GridScope.CargoSlotRow(
    testTag: String,
    cargoItem: CargoItem,
    slotIndex: Int,
    actions: CargoSlotActions
) {
    CompositionCounters.recomposed(testTag)

    val fit = LocalFit.current
    val windowManager = LocalTheorycrafterWindowManager.current
    val clipboard = LocalClipboard.current
    val editPriceOverrideAction = SlotContextAction.rememberEditPriceOverride(cargoItem.type)
    val contextActions = remember(fit, cargoItem, actions, clipboard, editPriceOverrideAction) {
        listOf(
            SlotContextAction.showInfo(windowManager, cargoItem),
            SlotContextAction.Separator,
            SlotContextAction.cutToClipboard(clipboard, actions.clear, cargoItem::clipboardText),
            SlotContextAction.copyToClipboard(clipboard, cargoItem::clipboardText),
            SlotContextAction.pasteCargo(clipboard, fit, putItem = actions.put),
            SlotContextAction.Separator,
            SlotContextAction.moveSlotUp(enabled = actions.canMoveUp(), actions::moveUp),
            SlotContextAction.moveSlotDown(enabled = actions.canMoveDown(), actions::moveSlotDown),
            SlotContextAction.Separator,
            SlotContextAction.clear(actions.clear),
            SlotContextAction.addOneItem(actions.addAmount),
            SlotContextAction.removeOneItem(actions.addAmount, showInContextMenu = true),
            SlotContextAction.Separator,
            editPriceOverrideAction,
        )
    }

    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    val illegalityInTournamentReason = remember(tournamentRules, cargoItem.type) {
        itemIllegalityInTournamentReason(cargoItem.type, tournamentRules::isCargoItemLegal)
    }

    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        modifierWhenNotEditing = Modifier
            .dragCargoToReorder(cargoItem, slotIndex, actions),
        contextActions = contextActions,
        invalidityReason = cargoItem.illegalFittingReason ?: illegalityInTournamentReason,
        editedRowContent = { onEditingCompleted ->
            CargoItemSelectorRow(
                currentItem = cargoItem,
                onItemSelected = { itemType, amount ->
                    actions.put(itemType, amount)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        CargoSlotContent(cargoItem)
    }
}


/**
 * The row displaying the "Empty Cargo Slot".
 */
@Composable
private fun GridScope.EmptyCargoSlotRow(
    putCargo: (EveItemType, Int) -> Unit,
) {
    val fit = LocalFit.current

    val clipboard = LocalClipboard.current
    val contextActions = remember(fit, putCargo, clipboard) {
        listOf(
            SlotContextAction.pasteCargo(clipboard, fit, putCargo)
        )
    }

    SlotRow(
        modifier = Modifier
            .testTag(TestTags.FitEditor.EmptyCargoholdRow),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            CargoItemSelectorRow(
                currentItem = null,
                onItemSelected = { itemType, amount ->
                    putCargo(itemType, amount)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyRowContent(text = "Empty cargo slot")
    }
}


/**
 * The title for the cargohold section.
 */
@Composable
private fun cargoholdSectionTitle(fit: Fit) = buildAnnotatedString {
    append("Cargo")

    val (text, isValid) = resourceUseText(
        resource = fit.cargohold.capacity,
        valueToText = { value, withUnits -> value.asVolume(withUnits = withUnits) }
    )

    appendSectionTitleExtraInfo {
        append("$text capacity".withValidUsageStyle(isValid, TheorycrafterTheme.colors.base().errorContent))
    }
}


/**
 * Remembers and returns the [CargoSlotActions] for the given cargo slot.
 */
@Composable
private fun rememberCargoSlotActions(
    fit: Fit,
    slotIndex: Int,
    undoRedoQueue: FitEditorUndoRedoQueue,
) = rememberSlotActions(fit, slotIndex, undoRedoQueue) {
    CargoSlotActions(
        put = { itemType, amount ->
            if (stale)
                return@CargoSlotActions

            replaceCargoItemAction(fit, slotIndex, itemType, amount)?.let {
                undoRedoQueue.performAndAppend(it)
            }
        },
        clear = {
            if (stale)
                return@CargoSlotActions

            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    removeCargoItemAction(fit, slotIndex),
                    markStaleAction()
                )
            )
        },
        addAmount = { amount ->
            if (stale)
                return@CargoSlotActions

            val cargoItem = fit.cargohold.contents[slotIndex]
            setCargoAmountAction(
                fit = fit,
                slotIndex = slotIndex,
                amount = (cargoItem.amount + amount).coerceAtLeast(1)
            )?.let {
                undoRedoQueue.performAndAppend(it)
            }
        },
        moveTo = { index ->
            if (stale)
                return@CargoSlotActions
            moveCargoItemAction(fit, currentIndex = slotIndex, newIndex = index)?.let {
                undoRedoQueue.performAndAppend(
                    action = it,
                    restoreSelection = false
                )
            }
        },
        canMoveBy = { offset ->
            val targetSlot = slotIndex + offset
            return@CargoSlotActions targetSlot in 0 until fit.cargohold.contents.size
        },
        moveBy = { offset ->
            if (stale)
                return@CargoSlotActions
            moveCargoItemAction(fit, currentIndex = slotIndex, newIndex = slotIndex + offset)?.let {
                undoRedoQueue.performAndAppend(
                    action = it,
                    restoreSelection = false
                )
            }
        }
    )
}


/**
 * Returns a modifier for dragging the given cargo item to place it at a new slot.
 */
context(GridScope)
@Composable
private fun Modifier.dragCargoToReorder(
    cargoItem: CargoItem,
    slotIndex: Int,
    actions: CargoSlotActions,
): Modifier {
    val selectionModel = LocalSlotSelectionModel.current
    val cargoRows by remember(selectionModel) {
        selectionModel.cargoholdSlotRowsState
    }

    return this.dragRowToReorder(
        draggableContent = { DraggedItemSlotRepresentation(cargoItem.displayedText()) },
        canMoveToRow = { rowIndex ->
            cargoRows?.contains(rowIndex) ?: false
        },
        onDrop = { _, targetRowIndex ->
            val targetSlotIndex = (targetRowIndex - cargoRows!!.first).let {
                if (it <= slotIndex) it else it - 1
            }
            actions.moveTo(targetSlotIndex)
        }
    )
}


/**
 * The section for adding cargohold items.
 */
@Composable
fun GridScope.CargoholdSection(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
): Int {
    val cargoholdCapacity = fit.cargohold.capacity.total
    if (cargoholdCapacity == 0.0)
        return 0

    var rowIndex = firstRowIndex

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

    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    val cargoItems = fit.cargohold.contents
    for ((slotIndex, cargoItem) in cargoItems.withIndex()) {
        inRow(rowIndex++) {
            CargoSlotRow(
                testTag = TestTags.FitEditor.cargoholdRow(slotIndex),
                cargoItem = cargoItem,
                slotIndex = slotIndex,
                actions = rememberCargoSlotActions(fit, slotIndex, undoRedoQueue)
            )
        }
    }

    // An extra slot where the user can add cargo items
    inRow(rowIndex++) {
        EmptyCargoSlotRow(
            putCargo = remember(fit, undoRedoQueue) {
                fun (itemType: EveItemType, amount: Int) {
                    undoRedoQueue.performAndAppend(
                        addCargoItemAction(fit, itemType, amount)  // CargoItemSelector always provides an amount
                    )
                }
            },
        )
    }

    return rowIndex - firstRowIndex
}
