package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.fillMaxWidth
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 compose.input.MouseButton
import compose.input.onMousePress
import compose.widgets.GridScope
import eve.data.EnvironmentType
import theorycrafter.LocalTheorycrafterWindowManager
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Environment
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.ui.Icons
import theorycrafter.ui.fiteditor.effectcolumn.displayedEnvironmentEffect
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.AutoSuggest
import theorycrafter.utils.CompositionCounters
import theorycrafter.utils.onEmptyQueryReturn
import theorycrafter.utils.undoRedoTogether


/**
 * The information regarding an environment that we remember in order to add and remove it.
 */
private class EnvironmentInfo(
    private val index: Int,
    private val type: EnvironmentType,
    private val isEnabled: Boolean,
) {

    /**
     * Adds the environment to the fit.
     */
    fun addTo(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            val env = fit.addEnvironment(type, index = index)
            env.setEnabled(isEnabled)
        }
    }

    /**
     * Removes the environment from the fit.
     */
    fun removeFrom(scope: FittingEngine.ModificationScope, fit: Fit) {
        with(scope) {
            fit.removeEnvironment(fit.environments[index])
        }
    }

}


/**
 * A [FitEditingAction] that replaces an environment at a given slot with another.
 */
private class EnvironmentReplacement(
    private val fit: Fit,
    private val removed: EnvironmentInfo?,
    private val added: EnvironmentInfo?
): 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 environment item to the fit.
 */
fun addEnvironmentAction(fit: Fit, environmentType: EnvironmentType): FitEditingAction {
    val slotIndex = fit.environments.size

    return EnvironmentReplacement(
        fit = fit,
        removed = null,
        added = EnvironmentInfo(slotIndex, environmentType, isEnabled = true)
    )
}


/**
 * Returns a [FitEditingAction] that replaces an environment with a new one.
 */
private fun replaceEnvironmentAction(
    fit: Fit,
    slotIndex: Int,
    environmentType: EnvironmentType,
    preserveEnabledState: Boolean
): FitEditingAction? {
    val environment = fit.environments[slotIndex]
    if (environment.type == environmentType)
        return null

    return EnvironmentReplacement(
        fit = fit,
        removed = EnvironmentInfo(
            index = slotIndex,
            type = environment.type,
            isEnabled = environment.enabled
        ),
        added = EnvironmentInfo(
            index = slotIndex,
            type = environmentType,
            isEnabled = if (preserveEnabledState) environment.enabled else true
        )
    )
}


/**
 * Returns a [FitEditingAction] that removes an environment from the fit.
 */
private fun removeEnvironmentAction(fit: Fit, slotIndex: Int): FitEditingAction {
    val environment = fit.environments[slotIndex]
    return EnvironmentReplacement(
        fit = fit,
        removed = EnvironmentInfo(slotIndex, environment.type, environment.enabled),
        added = null
    )
}


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

    return object: FitEditingAction() {

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.performEdit() {
            fit.environments[slotIndex].setEnabled(newState)
        }

        context(FitEditorUndoRedoContext)
        override fun FittingEngine.ModificationScope.revertEdit() {
            fit.environments[slotIndex].setEnabled(!newState)
        }

    }
}


/**
 * The row for a slot with a (non-`null`) environment.
 */
@Composable
private fun GridScope.GridRowScope.EnvironmentSlotContent(
    environment: Environment,
    carousel: Carousel<EnvironmentType>,
    toggleEnabled: () -> Unit,
) {
    cell(cellIndex = GridCols.STATE_ICON) {
        Icons.ItemEnabledState(
            enabled = environment.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(environment)
    }

    cell(cellIndex = GridCols.NAME, colSpan = GridCols.EFFECT - GridCols.NAME) {
        CarouselSlotContent(
            carousel = carousel,
            itemType = environment.type,
            modifier = Modifier.fillMaxWidth(),
            text = { it.name }
        )
    }
    cell(cellIndex = GridCols.EFFECT) {
        TextAndTooltipCell(displayedEnvironmentEffect(LocalFit.current, environment))
    }
    EmptyPriceCell()
}


/**
 * Bundles the actions passed to [EnvironmentSlotRow].
 */
private class EnvironmentSlotActions(
    private val set: (EnvironmentType, preserveEnabledState: Boolean, triggerCarouselAnimation: Boolean) -> Unit,
    val clear: () -> Unit,
    val toggleEnabled: () -> Unit,
) {

    fun setEnvironment(
        envType: EnvironmentType,
        preserveEnabledState: Boolean,
        triggerCarouselAnimation: Boolean = false
    ) {
        set(envType, preserveEnabledState, triggerCarouselAnimation)
    }

}


/**
 * Returns the auto-suggest for the given current environment type.
 */
@Composable
private fun EnvironmentType?.rememberAutoSuggest(carousel: Carousel<EnvironmentType>?): AutoSuggest<EnvironmentType> {
    val autoSuggest = TheorycrafterContext.autoSuggest.environmentTypes

    if (this == null)
        return autoSuggest

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

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


/**
 * An environment selection widget.
 */
@Composable
private fun GridScope.GridRowScope.EnvironmentSelectorRow(
    currentEnvironment: EnvironmentType?,
    carousel: Carousel<EnvironmentType>?,
    onEnvironmentSelected: (EnvironmentType) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = currentEnvironment.rememberAutoSuggest(carousel)

    ItemSelectorRow(
        onItemSelected = onEnvironmentSelected,
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { envType, _ ->
            DefaultSuggestedEveItemTypeIcon(envType)
            Text(text = envType.name)
        },
        hint = "Environment name",
    )
}


/**
 * The [SlotContextAction] for pasting into an environment slot.
 */
private fun SlotContextAction.Companion.pasteEnvironment(
    clipboard: Clipboard,
    set: (EnvironmentType) -> Unit,
) = pasteFromClipboard(
    clipboard = clipboard,
    itemFromClipboardText = ::environmentFromClipboardText,
    pasteItem = { envType ->
        set(envType)
    }
)


/**
 * The row displaying a non-empty environment slot.
 */
@Composable
private fun GridScope.EnvironmentSlotRow(
    testTag: String,
    environment: Environment,
    actions: EnvironmentSlotActions
) {
    CompositionCounters.recomposed(testTag)

    val envType = environment.type
    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    val carousel = rememberEnvironmentCarousel(envType, TheorycrafterContext.tournaments.activeRules)

    val fit = LocalFit.current
    val windowManager = LocalTheorycrafterWindowManager.current
    val clipboard = LocalClipboard.current
    val contextActions = remember(fit, environment, actions, clipboard) {
        listOf(
            SlotContextAction.showInfo(windowManager, environment),
            SlotContextAction.Separator,
            SlotContextAction.cutToClipboard(clipboard, actions.clear, environment::clipboardText),
            SlotContextAction.copyToClipboard(clipboard, environment::clipboardText),
            SlotContextAction.pasteEnvironment(clipboard) {
                actions.setEnvironment(it, preserveEnabledState = false)
            },
            SlotContextAction.Separator,
            SlotContextAction.clear(actions.clear),
            SlotContextAction.toggleEnabledState(actions.toggleEnabled),
        )
    }

    val keyShortcuts = Modifier
        .carouselShortcuts(carousel, envType) { newEnvType ->
            actions.setEnvironment(newEnvType, preserveEnabledState = true, triggerCarouselAnimation = true)
        }

    val illegalityInTournamentReason =
        if (!tournamentRules.hasEnvironments()) "Active tournament does not allow environments" else null

    SlotRow(
        modifier = Modifier
            .testTag(testTag),
        modifierWhenNotEditing = keyShortcuts,
        contextActions = contextActions,
        invalidityReason = environment.illegalFittingReason ?: illegalityInTournamentReason,
        editedRowContent = { onEditingCompleted ->
            EnvironmentSelectorRow(
                currentEnvironment = envType,
                carousel = carousel,
                onEnvironmentSelected = {
                    val preserveEnabledState = carousel.items.contains(it)
                    actions.setEnvironment(it, preserveEnabledState = preserveEnabledState)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EnvironmentSlotContent(
            environment = environment,
            carousel = carousel,
            toggleEnabled = actions.toggleEnabled
        )
    }
}


/**
 * The row displaying the "Empty Environment Slot".
 */
@Composable
private fun GridScope.EmptyEnvironmentSlotRow(
    addEnvironment: (EnvironmentType) -> Unit,
) {
    val fit = LocalFit.current

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

    SlotRow(
        modifier = Modifier
            .testTag(TestTags.FitEditor.EmptyEnvironmentRow),
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            EnvironmentSelectorRow(
                currentEnvironment = null,
                carousel = null,
                onEnvironmentSelected = { env ->
                    addEnvironment(env)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        }
    ) {
        EmptyRowContent(text = "Empty environment slot")
    }
}


/**
 * Remembers and returns the [EnvironmentSlotActions] for the given slot.
 */
@Composable
private fun rememberEnvironmentSlotActions(
    fit: Fit,
    slotIndex: Int,
    undoRedoQueue: FitEditorUndoRedoQueue,
) = rememberSlotActions(fit, slotIndex, undoRedoQueue) {
    EnvironmentSlotActions(
        set = { envType, preserveEnabledState, triggerCarouselAnimation ->
            if (stale)
                return@EnvironmentSlotActions

            replaceEnvironmentAction(fit, slotIndex, envType, preserveEnabledState)?.let {
                undoRedoQueue.performAndAppend(
                    it.withCarouselAnimation(triggerCarouselAnimation)
                )
            }
        },
        clear = {
            if (stale)
                return@EnvironmentSlotActions

            undoRedoQueue.performAndAppend(
                undoRedoTogether(
                    removeEnvironmentAction(fit, slotIndex),
                    markStaleAction()
                )
            )
        },
        toggleEnabled = {
            if (stale)
                return@EnvironmentSlotActions
            undoRedoQueue.performAndAppend(
                toggleEnabledStateAction(fit, slotIndex)
            )
        },
    )
}


/**
 * The section for applying environment effects to the fit.
 */
@Composable
fun GridScope.EnvironmentEffectsSection(
    firstRowIndex: Int,
    isFirst: Boolean = false,
    fit: Fit,
): Int {
    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    if (!tournamentRules.hasEnvironments() && fit.environments.isEmpty())
        return 0

    var rowIndex = firstRowIndex

    SectionTitleRow(
        rowIndex = rowIndex++,
        isFirst = isFirst,
        text = AnnotatedString("Environmental Effects")
    )

    val undoRedoQueue = LocalFitEditorUndoRedoQueue.current
    for ((slotIndex, environment) in fit.environments.withIndex()) {
        inRow(rowIndex++) {
            EnvironmentSlotRow(
                testTag = TestTags.FitEditor.environmentRow(slotIndex),
                environment = environment,
                actions = rememberEnvironmentSlotActions(fit, slotIndex, undoRedoQueue)
            )
        }
    }

    // An extra slot where the user can add environment effects
    inRow(rowIndex++) {
        EmptyEnvironmentSlotRow(
            addEnvironment = remember(fit, undoRedoQueue) {
                fun (environmentType: EnvironmentType) {
                    undoRedoQueue.performAndAppend(
                        addEnvironmentAction(fit, environmentType)
                    )
                }
            }
        )
    }

    return rowIndex - firstRowIndex
}
