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

package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Text
import androidx.compose.material.icons.outlined.Check
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
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.BoosterType
import eve.data.asPercentageWithPrecisionAtMost
import theorycrafter.LocalTheorycrafterWindowManager
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Booster
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.ui.Icons
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.effectcolumn.displayedBoosterEffect
import theorycrafter.ui.tooltip
import theorycrafter.ui.widgets.EmptyIcon
import theorycrafter.ui.widgets.MenuItemHeading
import theorycrafter.ui.widgets.MenuSeparator
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.*


/**
 * The information regarding a booster that we remember in order to fit and remove it.
 */
private class BoosterInfo(
    private val type: BoosterType,
    private val isEnabled: Boolean
) {


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


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


}


/**
 * A [FitEditingAction] that replaces a booster in a slot.
 */
private class BoosterReplacement(
    private val fit: Fit,
    private val removed: BoosterInfo?,
    private val fitted: BoosterInfo?
): 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 fits the given booster in its slot.
 */
private fun replaceBoosterAction(
    fit: Fit,
    newBoosterType: BoosterType,
    preserveEnabledState: Boolean
): FitEditingAction? {
    val slotIndex = newBoosterType.slotIndex
    val currentBooster = fit.boosters.inSlot(slotIndex)
    if (currentBooster?.type == newBoosterType)
        return null

    return BoosterReplacement(
        fit = fit,
        removed = currentBooster?.let { BoosterInfo(it.type, it.enabled) },
        fitted = BoosterInfo(
            type = newBoosterType,
            isEnabled = if (preserveEnabledState && (currentBooster != null)) currentBooster.enabled else true
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes a booster from the fit.
 */
private fun removeBoosterAction(fit: Fit, slotIndex: Int): FitEditingAction? {
    val fittedBooster = fit.boosters.inSlot(slotIndex) ?: return null
    return BoosterReplacement(
        fit = fit,
        removed = BoosterInfo(fittedBooster.type, fittedBooster.enabled),
        fitted = null
    )
}


/**
 * Returns a [FitEditingAction] that toggles the enabled state of the booster.
 */
private fun toggleEnabledStateAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val booster = fit.boosters.inSlot(slotIndex)!!
    val newState = !booster.enabledState.value

    return object: FitEditingAction() {

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

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

    }
}


/**
 * A [FitEditingAction] that sets a side effect's state.
 */
private class SetSideEffectState(
    private val fit: Fit,
    private val slotIndex: Int,
    private val sideEffect: BoosterType.SideEffect,
    val prevState: Boolean,
    val newState: Boolean
): FitEditingAction() {

    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.performEdit() {
        fit.boosters.inSlot(slotIndex)!!.setSideEffectActive(sideEffect, newState)
    }

    context(FitEditorUndoRedoContext)
    override fun FittingEngine.ModificationScope.revertEdit() {
        fit.boosters.inSlot(slotIndex)!!.setSideEffectActive(sideEffect, prevState)
    }

}


/**
 * Returns a [FitEditingAction] that sets the state of the booster's side effects.
 */
private fun setSideEffectsActive(
    booster: Booster,
    sideEffectsActiveState: Collection<Pair<BoosterType.SideEffect, Boolean>>
): FitEditingAction? {
    val fit = booster.fit
    val actions = sideEffectsActiveState.map { (sideEffect, newState) ->
        val slotIndex = booster.type.slotIndex
        val currentState = booster.isSideEffectActive(sideEffect)
        SetSideEffectState(fit, slotIndex, sideEffect, prevState = currentState, newState = newState)
    }
    if (actions.all { it.newState == it.prevState })
        return null

    return compositeFitEditingAction(actions)
}


/**
 * Returns a [FitEditingAction] that toggles the given side effect's active state.
 */
private fun toggleSideEffectActive(booster: Booster, sideEffect: BoosterType.SideEffect): FitEditingAction {
    val currentState = booster.isSideEffectActive(sideEffect)
    return SetSideEffectState(
        fit = booster.fit,
        slotIndex = booster.type.slotIndex,
        sideEffect = sideEffect,
        prevState = currentState,
        newState = !currentState
    )
}


/**
 * Returns the auto-suggest for the given current booster type.
 */
@Composable
private fun BoosterType?.rememberAutoSuggest(carousel: Carousel<BoosterType>?): AutoSuggest<BoosterType> {
    if (this == null)
        return TheorycrafterContext.autoSuggest.boosterTypes

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

    val autoSuggest = TheorycrafterContext.autoSuggest.rememberForBoosterTypes(slot)

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


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


/**
 * A booster selection widget.
 */
@Composable
private fun GridScope.GridRowScope.BoosterSelectorRow(
    currentBooster: BoosterType?,
    carousel: Carousel<BoosterType>?,
    onBoosterSelected: (BoosterType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = currentBooster.rememberAutoSuggest(carousel).withActiveTournamentRules()
    val tournamentRules = TheorycrafterContext.tournaments.activeRules

    ItemSelectorRow(
        onItemSelected = onBoosterSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { boosterType, _ ->
            DefaultSuggestedEveItemTypeIcon(boosterType)
            Text(text = boosterType.name, Modifier.weight(1f))
            HSpacer(TheorycrafterTheme.spacing.medium)
            Text(text = boosterType.slotName())
            if (TheorycrafterContext.settings.prices.showInFitEditorSuggestedItems) {
                HSpacer(TheorycrafterTheme.spacing.medium)
                ItemPrice(
                    itemType = boosterType,
                    textAlign = TextAlign.End,
                    modifier = Modifier.width(TheorycrafterTheme.sizes.fitEditorSuggestedItemsPriceWidth)
                )
            }
        },
        hint = "Booster name",
        marketGroupsParent = TheorycrafterContext.eveData.marketGroups.boosters,
        itemFilter = {
            (it is BoosterType) &&
                    ((currentBooster == null) || (it.slotIndex == currentBooster.slotIndex)) &&
                    tournamentRules.isBoosterLegal(it)
        },
        showMarketInitially = currentBooster == null  // Because we show the carousel items in this case
    )
}


/**
 * The text we display to identify the booster slot.
 */
fun BoosterType.slotName() = "Slot $slot"


/**
 * The row for a slot with a (non-`null`) booster.
 */
@Composable
private fun GridScope.GridRowScope.BoosterSlotContent(
    booster: Booster,
    carousel: Carousel<BoosterType>,
    toggleEnabled: () -> Unit,
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        Icons.ItemEnabledState(
            enabled = booster.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(booster)
    }
    cell(cellIndex = GridCols.NAME) {
        CarouselSlotContent(
            carousel = carousel,
            itemType = booster.type,
            modifier = Modifier.fillMaxWidth(),
            text = { it.name }
        )
    }
    cell(cellIndex = GridCols.POWER, colSpan = 2) {
        SideEffectsWidget(booster)
    }
    cell(cellIndex = GridCols.RANGE) {
        Text(booster.type.slotName())
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedBoosterEffect(LocalFit.current, booster))
    }
    PriceCell(booster.type)
}


/**
 * A widget displaying the state of booster side effects, and allows changing them.
 */
@Composable
private fun SideEffectsWidget(booster: Booster) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxxsmall)
    ) {
        for (index in booster.type.sideEffects.indices) {
            SideEffectCircle(
                booster = booster,
                sideEffectIndex = index,
                modifier = Modifier.size(TheorycrafterTheme.sizes.fitEditorSlotRowHeight * 0.8f)
            )
        }
    }
}


/**
 * Returns the text we display to represent the given side effect.
 */
fun Booster.sideEffectDisplayText(sideEffect: BoosterType.SideEffect): String = buildString {
    append(sideEffectMagnitude(sideEffect).asPercentageWithPrecisionAtMost(1, withSign = true))
    append(" ")
    append(sideEffect.penalizedAttribute.displayName ?: sideEffect.penalizedAttribute.name)
}


/**
 * The circle inside [SideEffectsWidget] responsible for the active state of one side effect.
 */
@Composable
private fun SideEffectCircle(booster: Booster, sideEffectIndex: Int, modifier: Modifier) {
    val sideEffect = booster.type.sideEffects[sideEffectIndex]
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current

    val textColor = LocalContentColor.current
    val density = LocalDensity.current
    val textMeasurer = rememberTextMeasurer()

    val circleColor = TheorycrafterTheme.colors.base().primary
    val drawCircleInside = booster.isSideEffectActive(sideEffect)
    Box(
        modifier = modifier
            .graphicsLayer()
            .drawWithCache {
                val text = (sideEffectIndex + 1).toString()
                val textLayoutResult = textMeasurer.measure(text, TheorycrafterTheme.textStyles.sideEffectWidget)
                val textOffset = Offset(
                    x = (size.width - textLayoutResult.size.width) / 2,
                    y = (size.height - textLayoutResult.size.height) / 2
                )
                val circleStyle = if (drawCircleInside)
                    Fill
                else
                    Stroke(width = with(density) { 1.5.dp.toPx() })
                onDrawBehind {
                    drawCircle(circleColor, radius = size.minDimension / 2, style = circleStyle)
                    drawText(textLayoutResult, color = textColor, topLeft = textOffset)
                }
            }
            .onMousePress(consumeEvent = true) {
                undoRedoQueue.performAndAppend(
                    toggleSideEffectActive(booster, sideEffect)
                )
            }
            .tooltip(
                text = { booster.sideEffectDisplayText(sideEffect) }
            )
    )
}


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

    fun fitBooster(
        boosterType: BoosterType,
        preserveEnabledState: Boolean,
        triggerCarouselAnimation: Boolean = false
    ) {
        fit(boosterType, preserveEnabledState, triggerCarouselAnimation)
    }

}


/**
 * The [SlotContextAction] for pasting into a booster slot.
 */
private fun SlotContextAction.Companion.pasteBooster(
    clipboard: Clipboard,
    slotIndex: Int?,
    fitBooster: (BoosterType) -> Unit,
) = pasteFromClipboard(
    clipboard = clipboard,
    itemFromClipboardText = ::boosterFromClipboardText,
    pasteItem = {
        // Don't allow pasting a booster of a mismatching slot
        if ((slotIndex == null) || (slotIndex == it.slotIndex))
            fitBooster(it)
    }
)


/**
 * The row displaying a non-empty booster slot.
 */
@Composable
private fun GridScope.BoosterSlotRow(
    testTag: String,
    booster: Booster,
    actions: BoosterSlotActions
) {
    CompositionCounters.recomposed(testTag)

    val boosterType = booster.type
    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    val carousel = rememberBoosterCarousel(boosterType, tournamentRules)

    val clipboard = LocalClipboard.current
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    val windowManager = LocalTheorycrafterWindowManager.current
    val fit = LocalFit.current
    val editPriceOverrideAction = SlotContextAction.rememberEditPriceOverride(boosterType)
    val contextActions = remember(booster, actions, clipboard, undoRedoQueue, windowManager, fit, editPriceOverrideAction) {
        listOf(
            SlotContextAction.showInfo(windowManager, booster),
            SlotContextAction.Separator,
            SlotContextAction.cutToClipboard(clipboard, actions.clear, booster::clipboardText),
            SlotContextAction.copyToClipboard(clipboard, booster::clipboardText),
            SlotContextAction.pasteBooster(clipboard, boosterType.slotIndex) {
                actions.fitBooster(it, preserveEnabledState = false)
            },
            SlotContextAction.Separator,
            SlotContextAction.clear(actions.clear),
            SlotContextAction.toggleEnabledState(actions.toggleEnabled),
            SlotContextAction.addToCargo(undoRedoQueue, fit, boosterType),
            SlotContextAction.Separator,
            editPriceOverrideAction
        )
    }
    val sideEffectActions = sideEffectContextActions(booster)

    val keyShortcuts = Modifier
        .carouselShortcuts(carousel, boosterType) { newBoosterType ->
            actions.fitBooster(newBoosterType, preserveEnabledState = true, triggerCarouselAnimation = true)
        }
        .then(sideEffectActions.toModifier())

    val illegalityInTournamentReason = remember(tournamentRules, boosterType) {
        itemIllegalityInTournamentReason(boosterType, tournamentRules::isBoosterLegal)
    }

    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        modifierWhenNotEditing = keyShortcuts,
        contextActions = contextActions,
        invalidityReason = booster.illegalFittingReason ?: illegalityInTournamentReason,
        editedRowContent = { onEditingCompleted ->
            BoosterSelectorRow(
                currentBooster = boosterType,
                carousel = carousel,
                onBoosterSelected = {
                    val preserveEnabledState = carousel.items.contains(it)
                    actions.fitBooster(it, preserveEnabledState = preserveEnabledState)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        },
        extraContextMenuContent = { closeMenu -> SideEffectContextMenuItems(sideEffectActions, closeMenu) }
    ) {
        BoosterSlotContent(
            booster = booster,
            carousel = carousel,
            toggleEnabled = actions.toggleEnabled
        )
    }
}


/**
 * The [SlotContextAction]s for managing booster side effects.
 */
@Composable
private fun sideEffectContextActions(booster: Booster): List<SlotContextAction> {
    val fit = booster.fit
    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    val sideEffectActive = booster.type.sideEffects.map { booster.isSideEffectActive(it) }
    return remember(fit, undoRedoQueue, booster, sideEffectActive) {
        listOf(
            SlotContextAction(
                displayName = "Activate All",
                enabled = sideEffectActive.any { !it },
                icon = { Icons.EnableAll() },
                shortcut = FitEditorKeyShortcuts.ActivateAllBoosterSideEffects,
                action = {
                    setSideEffectsActive(booster, booster.type.sideEffects.map { it to true })?.let {
                        undoRedoQueue.performAndAppend(it)
                    }
                }
            ),
            SlotContextAction(
                displayName = "Deactivate All",
                enabled = sideEffectActive.any { it },
                icon = { Icons.DisableAll() },
                shortcut = FitEditorKeyShortcuts.DeactivateAllBoosterSideEffects,
                action = {
                    setSideEffectsActive(booster, booster.type.sideEffects.map { it to false })?.let {
                        undoRedoQueue.performAndAppend(it)
                    }
                }

            )
        ) + booster.type.sideEffects.mapIndexed { index, sideEffect ->
            SlotContextAction(
                displayName = booster.sideEffectDisplayText(sideEffect),
                icon = if (booster.isSideEffectActive(sideEffect))
                    { ->
                        Icon(
                            imageVector = TheorycrafterTheme.iconStyle.Check,
                            contentDescription = null
                        )
                    }
                else
                    EmptyIcon,
                shortcut = FitEditorKeyShortcuts.ToggleBoosterSideEffect.getOrNull(index),
                action = {
                    undoRedoQueue.performAndAppend(
                        toggleSideEffectActive(booster, sideEffect)
                    )
                }
            )
        }
    }
}


/**
 * The context menu items for toggling which booster side effects are active.
 */
@Suppress("UnusedReceiverParameter")
@Composable
private fun ColumnScope.SideEffectContextMenuItems(
    contextActions: List<SlotContextAction>,
    closeMenu: () -> Unit,
) {
    MenuSeparator()
    MenuItemHeading("Side Effects", extraTopPadding = false)

    val hasKeyShortcuts = contextActions.any { it.shortcuts.isNotEmpty() }
    for (contextAction in contextActions)
        MenuItem(contextAction, reserveSpaceForKeyShortcut = hasKeyShortcuts, closeMenu)
}


/**
 * The row displaying the "Empty Booster Slot".
 */
@Composable
private fun GridScope.EmptyBoosterSlotRow(
    fitBooster: (BoosterType) -> Unit,
) {
    val clipboard = LocalClipboard.current
    val contextActions = listOf(
        SlotContextAction.pasteBooster(clipboard, slotIndex = null, fitBooster = fitBooster),
    )

    SlotRow(
        modifier = Modifier
            .testTag(TestTags.FitEditor.EmptyBoosterRow),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            BoosterSelectorRow(
                currentBooster = null,
                carousel = null,
                onBoosterSelected = {
                    fitBooster(it)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyRowContent(text = "Empty booster slot")
    }
}


/**
 * Remembers and returns the [BoosterSlotActions] for the given booster slot.
 */
@Composable
private fun rememberBoosterSlotActions(
    fit: Fit,
    slotIndex: Int,
    undoRedoQueue: FitEditorUndoRedoQueue,
) = rememberSlotActions(fit, slotIndex, undoRedoQueue) {
    BoosterSlotActions(
        fit = { boosterType, preserveEnabledState, triggerCarouselAnimation ->
            if (stale)
                return@BoosterSlotActions
            replaceBoosterAction(fit, boosterType, preserveEnabledState)?.let {
                undoRedoQueue.performAndAppend(
                    it.withCarouselAnimation(triggerCarouselAnimation)
                )
            }
        },
        clear = {
            if (stale)
                return@BoosterSlotActions
            removeBoosterAction(fit, slotIndex)?.let {
                undoRedoQueue.performAndAppend(
                    undoRedoTogether(
                        it,
                        markStaleAction()
                    )
                )
            }
        },
        toggleEnabled = {
            if (stale)
                return@BoosterSlotActions
            undoRedoQueue.performAndAppend(
                toggleEnabledStateAction(fit, slotIndex)
            )
        },
    )
}


/**
 * The section for fitting boosters.
 */
@Composable
fun GridScope.BoosterSection(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
): Int {
    val fittedBoosters = fit.boosters.fitted
    if (!TheorycrafterContext.tournaments.activeRules.areBoostersLegal() && fittedBoosters.isEmpty())
        return 0

    var rowIndex = firstRowIndex

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

    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    for ((rowIndexInSection, booster) in fittedBoosters.withIndex()) {
        inRow(rowIndex++) {
            BoosterSlotRow(
                testTag = TestTags.FitEditor.boosterRow(rowIndexInSection),
                booster = booster,
                actions = rememberBoosterSlotActions(fit, booster.type.slotIndex, undoRedoQueue)
            )
        }
    }

    // An extra slot where the user can add boosters
    inRow(rowIndex++) {
        EmptyBoosterSlotRow(
            fitBooster = remember(fit, undoRedoQueue) {
                fun (boosterType: BoosterType) {
                    replaceBoosterAction(fit, boosterType, false)?.let {
                        undoRedoQueue.performAndAppend(it)
                    }
                }
            }
        )
    }

    return rowIndex - firstRowIndex
}
