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

package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import compose.input.MouseButton
import compose.input.onMousePress
import compose.utils.HSpacer
import compose.widgets.GridScope
import eve.data.ImplantSet
import eve.data.ImplantType
import theorycrafter.LocalTheorycrafterWindowManager
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.fitting.Implant
import theorycrafter.ui.Icons
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.effectcolumn.displayedImplantEffect
import theorycrafter.ui.shortName
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*


/**
 * The information regarding an implant that we remember in order to fit and remove it.
 */
private class ImplantInfo(
    private val type: ImplantType,
    private val isEnabled: Boolean
) {


    /**
     * Fits the implant to the given fit.
     */
    fun fitTo(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            val implant = fit.fitImplant(type)
            implant.setEnabled(isEnabled)
        }
    }


    /**
     * Removes the drone group from the given fit.
     */
    fun removeFrom(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            fit.removeImplant(type.slotIndex)
        }
    }


}


/**
 * A [FitEditingAction] that replaces an implant in a slot.
 */
private class ImplantReplacement(
    private val fit: Fit,
    private val removed: ImplantInfo?,
    private val fitted: ImplantInfo?
): FitEditingAction() {

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

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

}


/**
 * Returns a [FitEditingAction] that replaces an already fitted implant with a new one.
 */
fun replaceImplantAction(
    fit: Fit,
    newImplantType: ImplantType,
    preserveEnabledState: Boolean
): FitEditingAction? {
    val slotIndex = newImplantType.slotIndex
    val currentImplant = fit.implants.inSlot(slotIndex)
    if (currentImplant?.type == newImplantType)
        return null

    return ImplantReplacement(
        fit = fit,
        removed = currentImplant?.let { ImplantInfo( it.type, it.enabled) },
        fitted = ImplantInfo(
            type = newImplantType,
            isEnabled = if (preserveEnabledState && (currentImplant != null)) currentImplant.enabled else true
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes an implant from the fit.
 */
private fun removeImplantAction(fit: Fit, slotIndex: Int): FitEditingAction? {
    val implant = fit.implants.inSlot(slotIndex) ?: return null
    return ImplantReplacement(
        fit = fit,
        removed = ImplantInfo(
            type = implant.type,
            isEnabled = implant.enabled
        ),
        fitted = null
    )
}


/**
 * Returns a [FitEditingAction] that sets the enabled state of the implant.
 */
private fun toggleEnabledStateAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val implant = fit.implants.inSlot(slotIndex)!!
    val newState = !implant.enabled

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.implants.inSlot(slotIndex)!!.setEnabled(newState)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.implants.inSlot(slotIndex)!!.setEnabled(!newState)
        }

    }
}


/**
 * Returns the auto-suggest for the given current implant type.
 *
 * The returned [AutoSuggest] will return [ImplantType]s and [ImplantSet]s.
 */
@Composable
private fun ImplantType?.rememberAutoSuggest(carousel: Carousel<ImplantType>?): AutoSuggest<Any> {
    if (this == null) {
        return TheorycrafterContext.autoSuggest.implantSets + TheorycrafterContext.autoSuggest.implantTypes
    }

    carousel!!  // When the implant type is non-null, the carousel is expected to also be

    val autoSuggest = TheorycrafterContext.autoSuggest.rememberForImplantTypes(slot)

    return remember(autoSuggest, carousel) {
        autoSuggest.onEmptyQueryReturn { carousel.items }
    }
}


/**
 * Filters the given implant autosuggest based on the active tournament rules.
 */
@Composable
private fun AutoSuggest<Any>.withActiveTournamentRules(): AutoSuggest<Any> {
    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    return if (tournamentRules == null)
        this
    else remember(this, tournamentRules) {
        this.filterResults {  result ->
            when (result) {
                is ImplantType -> tournamentRules.isImplantLegal(result)
                is ImplantSet -> result.implantTypes.all { tournamentRules.isImplantLegal(it) }
                else -> false
            }
        }
    }
}


/**
 * An implant selection widget.
 */
@Composable
private fun GridScope.GridRowScope.ImplantSelectorRow(
    currentImplant: ImplantType?,
    carousel: Carousel<ImplantType>?,
    onImplantSelected: (ImplantType) -> Unit,
    onImplantSetSelected: (ImplantSet) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = currentImplant.rememberAutoSuggest(carousel).withActiveTournamentRules()
    val tournamentRules = TheorycrafterContext.tournaments.activeRules

    SelectorRow(
        onItemSelected = {
            when (it) {
                is ImplantType -> onImplantSelected(it)
                is ImplantSet -> onImplantSetSelected(it)
            }
        },
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        autoSuggestItemToString = {
            when (it) {
                is ImplantType -> it.name
                is ImplantSet -> it.displayName
                else -> "Unknown implant"
            }
        },
        suggestedItemContent = { item, _ ->
            when (item) {
                is ImplantType -> {
                    DefaultSuggestedEveItemTypeIcon(item)
                    Text(text = item.shortName(), Modifier.weight(1f))
                    HSpacer(TheorycrafterTheme.spacing.medium)
                    Text(text = item.slotName())
                }
                is ImplantSet -> {
                    DefaultSuggestedIconContainer {
                        PirateSetIcon(Modifier.fillMaxSize())
                    }
                    Text(text = item.displayName, Modifier.weight(1f))
                }
            }
            if (TheorycrafterContext.settings.prices.showInFitEditorSuggestedItems) {
                HSpacer(TheorycrafterTheme.spacing.medium)

                val prices = TheorycrafterContext.eveItemPrices
                val priceInfo = when (item) {
                    is ImplantType -> prices?.priceInfoOf(item)
                    is ImplantSet -> prices?.priceInfoOf(item.implantTypes)
                    else -> null
                }
                Price(
                    priceInfo = priceInfo,
                    textAlign = TextAlign.End,
                    modifier = Modifier.width(TheorycrafterTheme.sizes.fitEditorSuggestedItemsPriceWidth)
                )
            }
        },
        hint = "Implant or pirate set name",
        marketGroups = with(TheorycrafterContext.eveData.marketGroups) { childrenOf(implants) },
        itemFilter = {
            (it is ImplantType) &&
                    ((currentImplant == null) || (it.slotIndex == currentImplant.slotIndex)) &&
                    tournamentRules.isImplantLegal(it)
        },
        showMarketInitially = currentImplant == null  // Because we show the carousel items in this case
    )
}


/**
 * The icon for implant sets.
 */
@Composable
private fun PirateSetIcon(modifier: Modifier) {
    val marketGroups = TheorycrafterContext.eveData.marketGroups
    val attributeEnhancers = marketGroups.attributeEnhancers
    val skillHardwiring = marketGroups.skillHardwiring
    Box(modifier) {
        Icons.MarketGroup(
            marketGroup = attributeEnhancers,
            modifier = Modifier
                .fillMaxSize()
                .offset(x = 1.dp, y = 2.dp)
        )
        Icons.MarketGroup(
            marketGroup = skillHardwiring,
            modifier = Modifier
                .fillMaxSize()
                .offset(x = (-1).dp, y = (-2).dp)
        )
    }
}


/**
 * The text we display to identify the implant slot.
 */
private fun ImplantType.slotName() = "Slot $slot"


/**
 * The row for a slot with a (non-`null`) implant.
 */
@Composable
private fun GridScope.GridRowScope.ImplantSlotContent(
    implant: Implant,
    carousel: Carousel<ImplantType>,
    onToggleEnabled: () -> Unit,
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        Icons.ItemEnabledState(
            enabled = implant.enabled,
            modifier = Modifier
                .onMousePress(consumeEvent = true) {  // Consume to prevent selecting the row
                    onToggleEnabled()
                }
                .onMousePress(MouseButton.Middle, consumeEvent = true) {  // Consume just in case
                    onToggleEnabled()
                }
        )
    }
    cell(cellIndex = GridCols.TYPE_ICON) {
        TypeIconCellContent(implant)
    }
    cell(cellIndex = GridCols.NAME, colSpan = 3) {
        CarouselSlotContent(
            carousel = carousel,
            itemType = implant.type,
            modifier = Modifier.fillMaxWidth(),
            text = { it.shortName() }
        )
    }
    cell(cellIndex = GridCols.RANGE) {
        Text(implant.type.slotName())
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedImplantEffect(LocalFit.current, implant))
    }
    PriceCell(implant.type)
}


/**
 * Bundles the actions passed to [ImplantSlotRow].
 */
private class ImplantSlotActions(
    private val fit: (ImplantType, preserveEnabledState: Boolean, triggerCarouselAnimation: Boolean) -> Unit,
    val clear: () -> Unit,
    val toggleEnabled: () -> Unit,
) {

    fun fitImplant(
        implantType: ImplantType,
        preserveEnabledState: Boolean,
        triggerCarouselAnimation: Boolean = false
    ) {
        fit(implantType, preserveEnabledState, triggerCarouselAnimation)
    }

}


/**
 * The [SlotContextAction] for pasting into an implant slot.
 */
private fun SlotContextAction.Companion.pasteImplant(
    clipboard: Clipboard,
    slotIndex: Int?,
    fitImplant: (ImplantType) -> Unit,
) = pasteFromClipboard(
    clipboard = clipboard,
    itemFromClipboardText = ::implantFromClipboardText,
    pasteItem = {
        // Don't allow pasting an implant of a mismatching slot
        if ((slotIndex == null) || (slotIndex == it.slotIndex))
            fitImplant(it)
    }
)


/**
 * The row displaying a non-empty implant slot.
 */
@Composable
private fun GridScope.ImplantSlotRow(
    testTag: String,
    implant: Implant,
    actions: ImplantSlotActions
) {
    CompositionCounters.recomposed(testTag)

    val implantType = implant.type
    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    val carousel = rememberImplantCarousel(implantType, tournamentRules)

    val clipboard = LocalClipboard.current
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    val windowManager = LocalTheorycrafterWindowManager.current
    val fit = LocalFit.current
    val editPriceOverrideAction = SlotContextAction.rememberEditPriceOverride(implantType)
    val contextActions = remember(implant, actions, clipboard, undoRedoQueue, windowManager, fit, editPriceOverrideAction) {
        listOf(
            SlotContextAction.showInfo(windowManager, implant),
            SlotContextAction.Separator,
            SlotContextAction.cutToClipboard(clipboard, actions.clear, implant::clipboardText),
            SlotContextAction.copyToClipboard(clipboard, implant::clipboardText),
            SlotContextAction.pasteImplant(clipboard, implantType.slotIndex) {
                actions.fitImplant(it, preserveEnabledState = false)
            },
            SlotContextAction.Separator,
            SlotContextAction.clear(actions.clear),
            SlotContextAction.toggleEnabledState(actions.toggleEnabled),
            SlotContextAction.addToCargo(undoRedoQueue, fit, implantType),
            SlotContextAction.Separator,
            editPriceOverrideAction
        )
    }
    val keyShortcuts = remember(carousel, implantType, actions){
        Modifier
            .carouselShortcuts(carousel, implantType) {
                actions.fitImplant(it, preserveEnabledState = true, triggerCarouselAnimation = true)
            }
    }

    val illegalityInTournamentReason = remember(tournamentRules, implantType) {
        itemIllegalityInTournamentReason(implantType, tournamentRules::isImplantLegal)
    }

    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        modifierWhenNotEditing = keyShortcuts,
        contextActions = contextActions,
        invalidityReason = implant.illegalFittingReason ?: illegalityInTournamentReason,
        editedRowContent = { onEditingCompleted ->
            ImplantSelectorRow(
                currentImplant = implantType,
                carousel = carousel,
                onImplantSelected = {
                    val preserveEnabledState = carousel.items.contains(it)
                    actions.fitImplant(it, preserveEnabledState = preserveEnabledState)
                    onEditingCompleted()
                },
                onImplantSetSelected = { },  // A pirate set can't be fitted in a non-empty slot
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        ImplantSlotContent(
            implant = implant,
            carousel = carousel,
            onToggleEnabled = actions.toggleEnabled,
        )
    }
}


/**
 * The row displaying the "Empty Implant Slot".
 */
@Composable
private fun GridScope.EmptyImplantSlotRow(
    fitImplant: (ImplantType) -> Unit,
    fitImplantSet: (ImplantSet) -> Unit,
) {
    val clipboard = LocalClipboard.current
    val contextActions = remember(clipboard, fitImplant) {
        listOf(
            SlotContextAction.pasteImplant(clipboard, slotIndex = null, fitImplant = fitImplant)
        )
    }

    SlotRow(
        modifier = Modifier
            .testTag(TestTags.FitEditor.EmptyImplantRow),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            ImplantSelectorRow(
                currentImplant = null,
                carousel = null,
                onImplantSelected = {
                    fitImplant(it)
                    onEditingCompleted()
                },
                onImplantSetSelected = {
                    fitImplantSet(it)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyRowContent(text = "Empty implant slot")
    }
}


/**
 * Remembers and returns the [ImplantSlotActions] for the given implant slot.
 */
@Composable
private fun rememberImplantSlotActions(
    fit: Fit,
    slotIndex: Int,
    undoRedoQueue: FitEditorUndoRedoQueue
) = rememberSlotActions(fit, slotIndex, undoRedoQueue) {
    ImplantSlotActions(
        fit = { implantType, preserveEnabledState, triggerCarouselAnimation ->
            if (stale)
                return@ImplantSlotActions
            replaceImplantAction(fit, implantType, preserveEnabledState)?.let {
                undoRedoQueue.performAndAppend(
                    it.withCarouselAnimation(triggerCarouselAnimation)
                )
            }
        },
        clear = {
            if (stale)
                return@ImplantSlotActions
            removeImplantAction(fit, slotIndex)?.let {
                undoRedoQueue.performAndAppend(
                    undoRedoTogether(
                        it,
                        markStaleAction()
                    )
                )
            }
        },
        toggleEnabled = {
            if (stale)
                return@ImplantSlotActions
            undoRedoQueue.performAndAppend(
                toggleEnabledStateAction(fit, slotIndex)
            )
        },
    )
}


/**
 * The section for fitting implants.
 */
@Composable
fun GridScope.ImplantSection(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
): Int {
    var rowIndex = firstRowIndex

    SectionTitleRow(
        rowIndex = rowIndex++,
        isFirst = isFirst,
        text = AnnotatedString("Implants")
    )

    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    for ((rowIndexInSection, implant) in fit.implants.fitted.withIndex()) {
        inRow(rowIndex++) {
            ImplantSlotRow(
                testTag = TestTags.FitEditor.implantRow(rowIndexInSection),
                implant = implant,
                actions = rememberImplantSlotActions(fit, implant.type.slotIndex, undoRedoQueue)
            )
        }
    }

    // An extra slot where the user can add implants
    inRow(rowIndex++) {
        EmptyImplantSlotRow(
            fitImplant = remember(fit, undoRedoQueue) {
                fun (implantType: ImplantType) {
                    replaceImplantAction(fit, implantType, preserveEnabledState = false)?.let {
                        undoRedoQueue.performAndAppend(it)
                    }
                }
            },
            fitImplantSet = remember(fit, undoRedoQueue) {
                fun (pirateSet: ImplantSet) {
                    val actions = pirateSet.implantTypes.mapNotNull {
                        replaceImplantAction(fit, it, preserveEnabledState = false)
                    }
                    if (actions.isNotEmpty())
                        undoRedoQueue.performAndAppend(groupUndoRedoActions(actions))
                }
            }
        )
    }

    return rowIndex - firstRowIndex
}
