package theorycrafter.ui.fiteditor

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.DialogState
import androidx.compose.ui.window.WindowPosition
import compose.input.KeyShortcut
import compose.input.onKeyShortcut
import compose.utils.*
import compose.widgets.ContentWithScrollbar
import compose.widgets.DropdownField
import compose.widgets.FlatButtonWithText
import compose.widgets.rememberAppendSuffixTransformation
import eve.data.*
import kotlinx.coroutines.runBlocking
import theorycrafter.TheorycrafterContext
import theorycrafter.TheorycrafterDialogWindow
import theorycrafter.fits.mediumName
import theorycrafter.fits.mutatedWith
import theorycrafter.ui.OutlinedTextField
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.widgets.AttributeMutationSlider
import theorycrafter.utils.onGainedFocus
import theorycrafter.utils.onLostFocus
import theorycrafter.utils.withAllTextSelected
import java.awt.Toolkit
import kotlin.random.Random


/**
 * Extra options for configuring the mutation dialog.
 */
data class MutationDialogConfig(
    val cpuTickValue: Double? = null,
    val powerTickValue: Double? = null
)


/**
 * A dialog in which the user can mutate a module type.
 */
@Composable
fun ModuleMutationDialog(
    moduleType: ModuleType,
    config: MutationDialogConfig,
    replaceItemInFit: (ModuleType?) -> Unit,
    setMutatedAttributeValues: (Array<Pair<Attribute<*>, Double>>?) -> Unit,
    onCloseRequest: () -> Unit
) {
    ItemMutationDialog(
        itemType = moduleType,
        baseType = ModuleType::baseType,
        config = config,
        initiallyMutatedWith = { with(TheorycrafterContext.eveData) { mutatedWith(it) } },
        replaceItemInFit = replaceItemInFit,
        setMutatedAttributeValues = setMutatedAttributeValues,
        onCloseRequest = onCloseRequest,
    )
}


/**
 * A dialog in which the user can mutate a drone type.
 */
@Composable
fun DroneMutationDialog(
    droneType: DroneType,
    replaceItemInFit: (DroneType?) -> Unit,
    setMutatedAttributeValues: (Array<Pair<Attribute<*>, Double>>?) -> Unit,
    onCloseRequest: () -> Unit
) {
    ItemMutationDialog(
        itemType = droneType,
        baseType = DroneType::baseType,
        config = MutationDialogConfig(),
        initiallyMutatedWith = { with(TheorycrafterContext.eveData) { mutatedWith(it) }  },
        replaceItemInFit = replaceItemInFit,
        setMutatedAttributeValues = setMutatedAttributeValues,
        onCloseRequest = onCloseRequest,
    )
}


/**
 * A dialog in which the user can mutate an item.
 */
@Composable
private fun <T: EveItemType> ItemMutationDialog(
    itemType: T,
    baseType: T.() -> T,
    config: MutationDialogConfig,
    initiallyMutatedWith: T.(Mutaplasmid) -> T,
    replaceItemInFit: (T?) -> Unit,  // null means revert to the original type
    setMutatedAttributeValues: (Array<Pair<Attribute<*>, Double>>?) -> Unit,  // null means revert to original values
    onCloseRequest: () -> Unit,
) {
    val dialogState = rememberInitialDialogState()
    TheorycrafterDialogWindow(
        title = "Mutating ${itemType.name}",
        state = dialogState,
        onCloseRequest = onCloseRequest,
        onKeyEvent = closeDialog(onCloseRequest)
    ) {
        Surface(
            modifier = Modifier.fillMaxSize()
        ) {
            MutationEditor(
                itemType = itemType,
                baseType = baseType,
                config = config,
                initiallyMutatedWith = initiallyMutatedWith,
                replaceItemInFit = replaceItemInFit,
                setMutatedAttributeValues = setMutatedAttributeValues,
                onCloseRequest = onCloseRequest,
            )
        }
    }
}


/**
 * Returns a remembered [DialogState] of the mutation dialog, initializing it to some good values.
 */
@Composable
private fun rememberInitialDialogState(): DialogState {
    val window = LocalWindow.current
    val windowState = LocalWindowState.current
    return remember(window, windowState) {
        val dialogSize = TheorycrafterTheme.sizes.mutationDialogSize
        if (window == null) { // Running in a test
            return@remember DialogState(size = dialogSize)
        }

        val availableRect = run {
            val gfxConfig = window.graphicsConfiguration
            val screenBounds = gfxConfig.bounds
            val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gfxConfig)

            // AWT pixels are equal to Dp
            DpRect(
                left = screenInsets.left.dp,
                top = screenInsets.top.dp,
                right = screenBounds.width.dp - screenInsets.right.dp,
                bottom = screenBounds.height.dp - screenInsets.bottom.dp
            )
        }

        val windowRect = DpRect(
            origin = DpOffset(x = windowState.position.x, y = windowState.position.y),
            size = windowState.size
        )

        fun DialogState(x: Dp) = DialogState(
            size = dialogSize,
            position = WindowPosition(
                x = x,
                y = windowRect.top + (windowRect.height - dialogSize.height)/2,
            )
        )

        // If there's room to the right, prefer that
        if (windowRect.right + dialogSize.width <= availableRect.right) {
            return@remember DialogState(
                x = windowRect.right,
            )
        }

        // If there's room to the left, use that
        if (windowRect.left - dialogSize.width >= availableRect.left) {
            return@remember DialogState(
                x = windowRect.left - dialogSize.width,
            )
        }

        // If there's room to overlap the fit list, use that
        if (windowRect.left + TheorycrafterTheme.sizes.savedFitsPanelWidth - dialogSize.width >= availableRect.left) {
            return@remember DialogState(
                x = windowRect.left + TheorycrafterTheme.sizes.savedFitsPanelWidth - dialogSize.width,
            )
        }

        // If all else fails, position at the center of the window
        DialogState(
            x = windowRect.left + (windowRect.width - dialogSize.width)/2,
        )
    }
}


/**
 * The widget for editing an item's [Mutation].
 */
@Composable
private fun <T: EveItemType> MutationEditor(
    itemType: T,
    baseType: T.() -> T,
    config: MutationDialogConfig = MutationDialogConfig(),
    initiallyMutatedWith: T.(Mutaplasmid) -> T,
    replaceItemInFit: (T?) -> Unit,
    setMutatedAttributeValues: (Array<Pair<Attribute<*>, Double>>?) -> Unit,
    onCloseRequest: () -> Unit
) {
    val eveData = TheorycrafterContext.eveData
    var mutatedType by remember { mutableStateOf(itemType) }

    fun fitType(newMutatedType: T?) {
        mutatedType = newMutatedType ?: itemType
        replaceItemInFit(newMutatedType)
    }

    // Because the mutated values are not Compose state, we use this key to signal the AttributeMutationEditor
    // that it needs to update the mutated values
    var mutationChangeKey by remember { mutableIntStateOf(0) }

    Column(
        modifier = Modifier
            .padding(vertical = TheorycrafterTheme.spacing.verticalEdgeMargin),
    ) {
        val mutation = mutatedType.mutation
        MutaplasmidSelector(
            baseItemType = itemType.baseType(),
            mutaplasmid = mutation?.mutaplasmid,
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin),
            onMutaplasmidSelected = { mutaplasmid ->
                fitType(
                    if (mutaplasmid == null)
                        itemType.baseType()  // Revert to non-mutated module
                    else
                        itemType.baseType().initiallyMutatedWith(mutaplasmid)
                )
            }
        )

        if (mutation != null) {
            VSpacer(TheorycrafterTheme.spacing.xxxlarge)
            ContentWithScrollbar(
                modifier = Modifier.weight(1f)
            ) {
                Column(
                    modifier = Modifier.verticalScroll(scrollState),
                    verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxxlarge)
                ) {
                    val attributeMutations = remember(mutation) {
                        mutation.mutaplasmid.attributeMutations.values.sortedBy {
                            val attribute = eveData.attributes[it.attributeId]
                            attribute.displayName ?: attribute.name
                        }
                    }
                    for (attrMutation in attributeMutations) {
                        AttributeMutationEditor(
                            mutation = mutation,
                            attrMutation = attrMutation,
                            config = config,
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin),
                            onValueChange = { newAttributeValue ->
                                setMutatedAttributeValues(
                                    arrayOf(eveData.attributes[attrMutation.attributeId] to newAttributeValue)
                                )
                            },
                            mutationChangeKey = mutationChangeKey
                        )
                    }
                }
            }
        }
        else
            Spacer(Modifier.weight(1f))

        if (mutation != null) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = TheorycrafterTheme.spacing.large)
                    .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin),
                horizontalArrangement = Arrangement.End,
            ) {
                Text(text = "Mutate to:", modifier = Modifier.alignByBaseline())

                FlatButtonWithText("Best", modifier = Modifier.alignByBaseline()) {
                    setMutatedAttributeValues(mutation.bestValues())
                    mutationChangeKey += 1
                }

                FlatButtonWithText("Base", modifier = Modifier.alignByBaseline()) {
                    setMutatedAttributeValues(mutation.baseValues())
                    mutationChangeKey += 1
                }

                FlatButtonWithText("Worst", modifier = Modifier.alignByBaseline()) {
                    setMutatedAttributeValues(mutation.worstValues())
                    mutationChangeKey += 1
                }

                FlatButtonWithText("Random", modifier = Modifier.alignByBaseline()) {
                    setMutatedAttributeValues(mutation.randomValues())
                    mutationChangeKey += 1
                }
            }
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin),
            horizontalArrangement = Arrangement.End
        ) {
            FlatButtonWithText("Revert Changes") {
                runBlocking {
                    fitType(null)
                    setMutatedAttributeValues(null)
                    mutationChangeKey += 1
                }
            }
            FlatButtonWithText("Close") {
                onCloseRequest()
            }
        }
    }
}


/**
 * Returns the attribute values for the best possible mutation.
 */
private fun Mutation.bestValues() = selectValues { _, range, _, highIsGood ->
    if (highIsGood)
        range.endInclusive
    else
        range.start
}


/**
 * Returns the base attribute values.
 */
private fun Mutation.baseValues() = selectValues { _, range, baseValue, _ ->
    baseValue.coerceIn(range)
}


/**
 * Returns the attribute values for the worst possible mutation.
 */
private fun Mutation.worstValues() = selectValues { _, range, _, highIsGood ->
    if (highIsGood)
        range.start
    else
        range.endInclusive
}


/**
 * Returns random attribute values.
 */
private fun Mutation.randomValues() = selectValues { _, range, _, _ ->
    range.random()
}


/**
 * Selects mutated attribute values using the given function.
 */
private fun Mutation.selectValues(
    selector: (
        attribute: Attribute<*>,
        range: ClosedFloatingPointRange<Double>,
        baseValue: Double,
        highIsGood: Boolean
    ) -> Double
): Array<Pair<Attribute<*>, Double>> {
    return baseValueByAttribute.map { (attribute, baseValue) ->
        val attrMutation = mutaplasmid.attributeMutations[attribute.id]!!
        val valueRange = attrMutation.attributeValueRange(baseValue)
        val highIsGood = attrMutation.highIsGood(attribute)
        val value = selector(attribute, valueRange, baseValue, highIsGood)
        attribute to value
    }.toTypedArray()
}


/**
 * The dropdown for selecting the mutating mutaplasmid.
 */
@Composable
private fun MutaplasmidSelector(
    baseItemType: EveItemType,
    mutaplasmid: Mutaplasmid?,
    onMutaplasmidSelected: (Mutaplasmid?) -> Unit,
    modifier: Modifier
) {
    val mutaplasmids = remember(baseItemType) {
        listOf(null) + TheorycrafterContext.eveData.mutaplasmidsMutating(baseItemType)
    }
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.Start,
        verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xsmall)
    ) {
        Text("Mutaplasmid")
        DropdownField(
            items = mutaplasmids,
            selectedItem = mutaplasmid,
            itemToString = { it?.mediumName ?: "None" },
            modifier = Modifier.fillMaxWidth(),
            onItemSelected = { _, mutaplasmid ->
                onMutaplasmidSelected(mutaplasmid)
            }
        )
    }
}


/**
 * The widget for editing the mutated attribute value.
 *
 * The purpose of this function is to obtain and normalize the values and pass them to
 * [NormalizedAttributeMutationEditor].
 */
@Composable
private fun AttributeMutationEditor(
    mutation: Mutation,
    attrMutation: AttributeMutation,
    config: MutationDialogConfig,
    onValueChange: (Double) -> Unit,
    modifier: Modifier,
    mutationChangeKey: Int,
) {
    val eveData = TheorycrafterContext.eveData
    val attributeId = attrMutation.attributeId
    val attribute = eveData.attributes[attributeId]
    val attributeUnit = attribute.unit

    val initialValue = mutation.mutatedAttributeValue(attributeId)!!
    val baseValue = remember(mutation, attrMutation) {
        mutation.baseValueByAttribute[attribute]!!
    }
    val valueRange = remember(attrMutation, baseValue) {
        attrMutation.attributeValueRange(baseValue)
    }

    val normalizingInverts = attributeUnit?.normalizingInverts == true
    fun Double.normalize() = attributeUnit?.rawToNormalValue?.invoke(this) ?: this
    fun Double.denormalize() = attributeUnit?.normalToRawValue?.invoke(this)?.coerceIn(valueRange) ?: this
    fun ClosedFloatingPointRange<Double>.normalize() =
        if (normalizingInverts)
            endInclusive.normalize() .. start.normalize()
        else
            start.normalize() .. endInclusive.normalize()

    val highIsGood = attrMutation.highIsGood(attribute)

    NormalizedAttributeMutationEditor(
        attribute = attribute,
        highIsGood = highIsGood xor normalizingInverts,
        attrMutation = attrMutation,
        initialValue = initialValue.normalize(),
        baseValue = baseValue.normalize(),
        valueRange = valueRange.normalize(),
        config = config,
        onValueChange = { onValueChange(it.denormalize()) },
        modifier = modifier,
        mutationChangeKey = mutationChangeKey
    )
}


/**
 * Appends the given attribute's suffix to the given string.
 */
private fun String.withSuffixOf(attribute: Attribute<*>): String {
    val suffix = attribute.unit?.normalSuffix
    return if (suffix == null)
        this
    else
        "$this$NNBSP$suffix"
}


/**
 * The widget for editing the mutated attribute value.
 *
 * The values have been normalized for display.
 */
@Composable
private fun NormalizedAttributeMutationEditor(
    attribute: Attribute<*>,
    highIsGood: Boolean,
    attrMutation: AttributeMutation,
    initialValue: Double,
    baseValue: Double,
    valueRange: ClosedFloatingPointRange<Double>,
    config: MutationDialogConfig,
    onValueChange: (Double) -> Unit,
    modifier: Modifier,
    mutationChangeKey: Int,
) {
    var value by remember(attrMutation, initialValue, mutationChangeKey) {
        mutableDoubleStateOf(initialValue)
    }

    fun Double.toDisplayedText() = this.normalAttributeValueToString(attribute)

    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.Start,
        verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xsmall)
    ) {
        var textFieldValue by remember(attrMutation, mutationChangeKey, initialValue) {
            mutableStateOf(TextFieldValue(text = value.toDisplayedText()))
        }

        fun applyTextFieldValue() {
            val textValue = textFieldValue.text.toFloatOrNull()
            if (textValue == null)
                textFieldValue = TextFieldValue(value.toDisplayedText())
            else {
                value = textValue.toDouble().coerceIn(valueRange)
                onValueChange(value)
            }
        }

        VerticallyCenteredRow(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            val worstValueText = (if (highIsGood) valueRange.start else valueRange.endInclusive).toDisplayedText()
            val bestValueText = (if (highIsGood) valueRange.endInclusive else valueRange.start).toDisplayedText()
            Column(verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxxsmall)) {
                Text(attribute.displayName ?: attribute.name)
                Row {
                    Text(
                        text = worstValueText.withSuffixOf(attribute),
                        color = TheorycrafterTheme.colors.mutationColorBad,
                        fontWeight = FontWeight.Medium
                    )
                    Text(" – ")  // En-dash, not a minus
                    Text(
                        text = bestValueText.withSuffixOf(attribute),
                        color = TheorycrafterTheme.colors.mutationColorGood,
                        fontWeight = FontWeight.Medium
                    )
                }
            }

            val interactionSource = remember { MutableInteractionSource() }
            TheorycrafterTheme.OutlinedTextField(
                value = textFieldValue,
                onValueChange = { textFieldValue = it },
                singleLine = true,
                textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
                interactionSource = interactionSource,
                visualTransformation = rememberAppendSuffixTransformation {
                    attribute.unit?.normalSuffix?.let { "${NNBSP}${it}" } ?: ""
                },
                modifier = Modifier
                    .width(TheorycrafterTheme.sizes.mutatedAttributeValueTextFieldWidth)
                    .onKeyShortcut(KeyShortcut.anyEnter(), onPreview = true) {
                        applyTextFieldValue()
                    },
                scaleHackFactor = 0.5f
            )
            interactionSource.onGainedFocus {
                textFieldValue = textFieldValue.withAllTextSelected()
            }
            interactionSource.onLostFocus {
                applyTextFieldValue()
            }
        }

        val attributes = TheorycrafterContext.eveData.attributes
        val extraTickValue = when {
            (attribute == attributes.power) && (config.powerTickValue != null) -> config.powerTickValue
            (attribute == attributes.cpu) && (config.cpuTickValue != null) -> config.cpuTickValue
            else -> null
        }
        AttributeMutationSlider(
            baseValue = baseValue,
            highIsGood = highIsGood,
            modifier = Modifier.fillMaxWidth(),
            value = value,
            onValueChange = {
                value = it.coerceIn(valueRange)
                textFieldValue = TextFieldValue(value.toDisplayedText())
                onValueChange(value)
            },
            valueRange = valueRange,
            extraTickValue = extraTickValue,
        )
    }
}


/**
 * Returns a random value in the given range.
 */
fun ClosedRange<Double>.random() = start + Random.nextDouble() * (endInclusive - start)


