package theorycrafter.ui.settings

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.DropdownMenuState
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import compose.input.KeyShortcut
import compose.input.onKeyShortcut
import compose.input.onOpenContextMenu
import compose.utils.*
import compose.widgets.*
import eve.data.SkillType
import eve.data.TypeGroup
import eve.esi.models.GetCharactersCharacterIdSkillsOk
import kotlinx.coroutines.*
import theorycrafter.*
import theorycrafter.esi.*
import theorycrafter.ui.*
import theorycrafter.ui.tooltip
import theorycrafter.ui.widgets.*
import theorycrafter.utils.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds


/**
 * The pane for editing skill sets.
 */
@Composable
fun SkillSetsSettings() {
    val eveData = TheorycrafterContext.eveData
    val skillsByGroup = remember(eveData) {
        eveData.skillTypes
            .groupBy { it.groupId }
            .map { (groupId, skills) ->
                eveData.groups[groupId] to skills.sortedBy { it.name }
            }
            .toMap()
    }
    val groups = remember(skillsByGroup) {
        skillsByGroup.keys.sortedBy { it.name }
    }

    val dialogs = LocalStandardDialogs.current
    var showNewCustomSkillSetDialog by remember { mutableStateOf(false) }
    var showNewCharacterSkillSetDialog by remember { mutableStateOf(false) }
    var skillSetBeingReauthorized: SkillSetHandle? by remember { mutableStateOf(null) }
    var skillSetBeingUpdated: SkillSetHandle? by remember { mutableStateOf(null) }

    val coroutineScope = rememberCoroutineScope()

    val builtInSkillSets = TheorycrafterContext.skillSets.builtInHandles
    val userSkillSets = TheorycrafterContext.skillSets.userHandles
    val characterSkillSets by remember {
        derivedStateOf { userSkillSets.filter { it.isCharacterSkillSet } }
    }
    val customSkillSets by remember {
        derivedStateOf { userSkillSets.filter { !it.isCharacterSkillSet } }
    }
    val skillSetSelection = remember {
        SkillSetSelectionModel(builtInSkillSets, characterSkillSets, customSkillSets)
    }.also {
        it.characterSkillSets = characterSkillSets
        it.customSkillSets = customSkillSets
    }
    val skillSetBeingRenamed: MutableState<SkillSetHandle?> = remember { mutableStateOf(null) }

    fun onSetDefaultSkillSet(handle: SkillSetHandle) {
        coroutineScope.launch {
            TheorycrafterContext.skillSets.setDefaultSkillSet(handle)
            TheorycrafterContext.settings.defaultSkillSetId = handle.skillSetId
        }
    }

    fun onCreateCustomCopy(handle: SkillSetHandle) {
        coroutineScope.launch {
            val copyName = copyName(handle.name, allNames = customSkillSets.asSequence().map { it.name })
            val newSkillSet = TheorycrafterContext.skillSets.add(
                name = copyName,
                levelOfSkill = handle::levelOfSkill
            )
            skillSetBeingRenamed.value = newSkillSet
        }
    }

    fun onUpdateSkills(handle: SkillSetHandle) {
        if (!handle.isCharacterSkillSet) {
            dialogs.showErrorDialog("Only character skill sets can be updated")
            return
        }
        val millisBeforeNextUpdate = handle.lastUpdateTimeUtcMillis + 10.minutes.inWholeMilliseconds - System.currentTimeMillis()
        if (millisBeforeNextUpdate > 0) {
            dialogs.showErrorDialog("Please wait at least 10 minutes between updates." +
                    "\nYou can update in ${millisBeforeNextUpdate.milliseconds.inWholeSeconds.seconds}.")
            return
        }

        skillSetBeingUpdated = handle
    }

    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        // The separator between lists
        // Can't use Dp.Hairline because that won't take any space
        val separatorWidth = with(LocalDensity.current) { 1.dp / density }
        val separatorColor = LocalContentColor.current.copy(alpha = 0.5f)
        @Composable
        fun Separator() {
            Box(Modifier.width(separatorWidth).fillMaxHeight().background(separatorColor))
        }

        Row {
            val selectedSkillSet = skillSetSelection.selected

            Separator()

            // List of skill sets and "New Skill Set" button
            Column(
                modifier = Modifier
                    .fillMaxHeight()
                    .width(TheorycrafterTheme.sizes.skillSetsListWidth),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xsmall)
            ) {
                SkillSetList(
                    modifier = Modifier.weight(1f),
                    builtInSkillSets = builtInSkillSets,
                    characterSkillSets = characterSkillSets,
                    customSkillSets = customSkillSets,
                    selection = skillSetSelection,
                    onSetAsDefault = ::onSetDefaultSkillSet,
                    onCreateCustomCopy = ::onCreateCustomCopy,
                    onUpdateSkills = ::onUpdateSkills,
                    skillSetBeingRenamed = skillSetBeingRenamed,
                    onError = { dialogs.showErrorDialog(it) }
                )

                val buttonPadding = PaddingValues(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin)
                UpdateSkillLevelsButton(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(buttonPadding),
                    enabled = selectedSkillSet?.isCharacterSkillSet == true,
                    onClick = {
                        selectedSkillSet?.let(::onUpdateSkills)
                    }
                )
                SetAsDefaultButton(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(buttonPadding),
                    enabled = selectedSkillSet != null,
                    onClick = {
                        skillSetSelection.selected?.let {
                            onSetDefaultSkillSet(it)
                        }
                    }
                )
                NewSkillSetButton(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(buttonPadding)
                        .padding(bottom = TheorycrafterTheme.spacing.verticalEdgeMargin),
                    onCreateCustomSkillSet = { showNewCustomSkillSetDialog = true },
                    onCreateCharacterSkillSet = { showNewCharacterSkillSetDialog = true }
                )
            }
            Separator()
            if (selectedSkillSet == null)
                return@Row

            // List of skill groups
            val groupSelection = rememberSingleItemSelectionModel(groups)
            SkillGroupList(
                groups = groups,
                selection = groupSelection,
            )
            Separator()
            val selectedGroup = groupSelection.selectedItem() ?: return@Row

            // List of skills (in group)
            val skills = skillsByGroup[selectedGroup]!!
            val skillSelection = rememberSingleItemSelectionModel(skills)
            SkillList(
                modifier = Modifier.weight(1f),
                skillSet = selectedSkillSet,
                skills = skills,
                selection = skillSelection
            )
            if (selectedSkillSet.let { !it.isBuiltInSkillSet && !it.isCharacterSkillSet }) {
                Separator()
                val selectedSkill = skillSelection.selectedItem()
                if (selectedSkill == null) {
                    // When not displaying the skill levels, we still need to reserve room for it
                    HSpacer(TheorycrafterTheme.sizes.skillLevelsListWidth)
                }
                else {
                    // List of skill levels
                    SkillLevelList(
                        modifier = Modifier.width(TheorycrafterTheme.sizes.skillLevelsListWidth),
                        skillSet = selectedSkillSet,
                        skill = selectedSkill,
                        onSetSkillLevel = { skillSet, skill, level ->
                            when {
                                skillSet.isBuiltInSkillSet ->
                                    dialogs.showErrorDialog("Built-in skill sets can't be modified")
                                skillSet.isCharacterSkillSet ->
                                    dialogs.showErrorDialog("Character skill sets can't be modified")
                                else -> coroutineScope.launch {
                                    TheorycrafterContext.skillSets.setSkillLevel(skillSet, skill, level)
                                }
                            }
                        }
                    )
                }
            }
        }
    }

    // New custom skill set dialog
    if (showNewCustomSkillSetDialog) {
        NewCustomSkillSetDialog(
            onDismiss = { showNewCustomSkillSetDialog = false },
            createNewSkillSet = { name, template ->
                coroutineScope.launch {
                    TheorycrafterContext.skillSets.add(name, template::levelOfSkill)
                }
            }
        )
    }

    // New character skill dialog
    if (showNewCharacterSkillSetDialog) {
        CreateOrUpdateCharacterSkillSetDialog(
            updatedCharacterTokens = skillSetBeingReauthorized?.ssoTokens,
            onDismiss = {
                skillSetBeingReauthorized = null
                showNewCharacterSkillSetDialog = false
            },
            createNewSkillSet = { tokens, skillInfo ->
                val skillSetToUpdate = skillSetBeingReauthorized  // Capture it before onDismiss is called
                coroutineScope.launch {
                    val skillSets = TheorycrafterContext.skillSets
                    if (skillSetToUpdate != null) {
                        // Update the skill set and tokens
                        skillSets.setSkillLevels(skillSetToUpdate, skillInfo.skillTypesAndLevels())
                        skillSets.setEveSsoTokens(skillSetToUpdate, tokens)
                    } else {
                        // Create a new skill set
                        skillSets.add(tokens, skillInfo)
                    }
                }
            }
        )
    }

    // Updating the skill levels of a character skill set
    skillSetBeingUpdated?.let { handle ->
        ActionInProgressDialog(
            text = "Updating ${handle.name}'s skill levels",
            onStopAction = {
                skillSetBeingUpdated = null
            }
        )

        LaunchedEffect(handle) {
            try {
                val (tokenFailure, errorMessage) = updateSkillLevels(handle)
                if (tokenFailure) {
                    dialogs.showConfirmDialog(
                        text = "Error refreshing SSO token.\n" +
                                "Authorization for \"${handle.ssoTokens.characterName}\" may have expired.",
                        confirmText = "Re-authorize",
                        onConfirm = {
                            skillSetBeingReauthorized = handle
                            showNewCharacterSkillSetDialog = true
                        }
                    )
                } else if (errorMessage != null) {
                    dialogs.showErrorDialog(errorMessage)
                } else {
                    dialogs.showInfoDialog("Updated skill levels for '${handle.name}'")
                }
            } finally {
                skillSetBeingUpdated = null
            }
        }
    }
}


/**
 * The button to create a new skill set.
 */
@Composable
private fun NewSkillSetButton(
    modifier: Modifier,
    onCreateCustomSkillSet: () -> Unit,
    onCreateCharacterSkillSet: () -> Unit,
) {
    MenuButton(
        modifier = modifier.nonFocusable(),
        offset = DpOffsetY(y = TheorycrafterTheme.spacing.xxxsmall),
        content = { onClick ->
            TheorycrafterTheme.RaisedButtonWithText(
                modifier = Modifier.fillMaxWidth(),
                text = "New Skill Set",
                onClick = onClick
            )
        },
        menuContent = { onCloseMenu ->
            MenuItem(
                text = "From Character",
                onCloseMenu = onCloseMenu,
                action = onCreateCharacterSkillSet
            )
            MenuItem(
                text = "Custom Skill Set",
                onCloseMenu = onCloseMenu,
                action = onCreateCustomSkillSet
            )
        }
    )
}


/**
 * The button to set the default skill set.
 */
@Composable
private fun SetAsDefaultButton(
    modifier: Modifier,
    enabled: Boolean,
    onClick: () -> Unit
) {
    TheorycrafterTheme.RaisedButtonWithText(
        modifier = modifier.nonFocusable(),
        text = "Set as Default",
        onClick = onClick,
        enabled = enabled
    )
}


/**
 * The button to update skill levels.
 */
@Composable
private fun UpdateSkillLevelsButton(
    modifier: Modifier,
    enabled: Boolean,
    onClick: () -> Unit
) {
    TheorycrafterTheme.RaisedButtonWithText(
        modifier = modifier.nonFocusable(),
        text = "Update Skills",
        onClick = onClick,
        enabled = enabled
    )
}


/**
 * The dialog for creating a new custom skill set.
 */
@Composable
private fun NewCustomSkillSetDialog(
    onDismiss: () -> Unit,
    createNewSkillSet: (name: String, template: SkillSetHandle) -> Unit
) {
    val skillSets = TheorycrafterContext.skillSets.builtInHandles + TheorycrafterContext.skillSets.userHandles
    var name by remember { mutableStateOf("") }
    var template by remember { mutableStateOf(skillSets.first()) }

    val canCreateNewSkillSet by remember {
        derivedStateOf {
            name.isNotBlank()
        }
    }

    fun onConfirm() {
        if (!canCreateNewSkillSet)
            return
        createNewSkillSet(name, template)
    }

    InnerDialog(
        title = "New Custom Skill Set",
        confirmText = "Create",
        dismissText = "Cancel",
        onConfirm = ::onConfirm,
        onDismiss = onDismiss,
        confirmEnabled = canCreateNewSkillSet
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.medium)
        ) {
            TheorycrafterTheme.OutlinedTextField(
                value = name,
                onValueChange = { name = it },
                label = { Text("Name") },
                maxLines = 1,
                modifier = Modifier
                    .requestInitialFocus()
                    .onKeyShortcut(KeyShortcut.anyEnter(), onPreview = true) {
                        onConfirm()
                        onDismiss()
                    }
            )

            OutlinedDropdownField(
                items = skillSets,
                selectedItem = template,
                onItemSelected = { _, skillSet -> template = skillSet },
                itemToString = SkillSetHandle::name,
                label = "Base on Template",
                // Workaround for https://partnerissuetracker.corp.google.com/issues/303737196
                modifier = Modifier.height(height = 64.dp)
            )
        }
    }
}


/**
 * The dialog for creating a new skill set from an EVE character.
 */
@Composable
private fun CreateOrUpdateCharacterSkillSetDialog(
    updatedCharacterTokens: EveSsoTokens?,
    onDismiss: () -> Unit,
    createNewSkillSet: (ssoTokens: EveSsoTokens, skillInfo: GetCharactersCharacterIdSkillsOk) -> Unit
) {
    var tokensAndSkillInfo: Pair<EveSsoTokens, GetCharactersCharacterIdSkillsOk>? by remember { mutableStateOf(null) }
    val coroutineScope = rememberCoroutineScope()

    InnerDialog(
        title = if (updatedCharacterTokens == null) "Import EVE Character Skills" else "Re-authorize ${updatedCharacterTokens.characterName}",
        confirmText = if (updatedCharacterTokens == null) "Create Skill Set" else "Update ${updatedCharacterTokens.characterName}",
        dismissText = "Cancel",
        onConfirm = {
            tokensAndSkillInfo?.let { (tokens, skillInfo) ->
                createNewSkillSet(tokens, skillInfo)
            }
        },
        onDismiss = onDismiss,
        confirmEnabled = tokensAndSkillInfo != null
    ) {
        Column(
            modifier = Modifier.size(width = 400.dp, height = 400.dp)  // Make room for all steps right away
        ) {

            @Composable
            fun StepTitle(step: Int) {
                Text(
                    text = "Step $step",
                    style = TheorycrafterTheme.textStyles.mediumHeading,
                    modifier = Modifier.padding(
                        top = if (step == 1) TheorycrafterTheme.spacing.medium else TheorycrafterTheme.spacing.larger,
                        bottom = TheorycrafterTheme.spacing.xsmall
                    )
                )
            }

            val codeVerifier = remember { generateCodeVerifier() }
            val uriHandler = LocalUriHandler.current
            var loginButtonClicked by remember { mutableStateOf(false) }

            StepTitle(1)
            TheorycrafterTheme.RaisedButtonWithText(
                text = "Log in with EVE SSO",
                enabled = !loginButtonClicked,
                onClick = {
                    uriHandler.openUri(
                        authStartUrl(codeVerifier = codeVerifier, EsiScopes.ReadSkills)
                    )
                    loginButtonClicked = true
                },
            )

            var authCode by remember { mutableStateOf("") }
            if (!loginButtonClicked)
                return@Column

            StepTitle(2)
            Text(
                text = "Complete logging in at the Eve Online website." +
                        "\nWhen shown a code, copy it and paste below",
                modifier = Modifier.padding(vertical = TheorycrafterTheme.spacing.xsmall)
            )
            TheorycrafterTheme.OutlinedTextField(
                value = authCode,
                onValueChange = { authCode = it },
                label = { Text("Code") },
                modifier = Modifier.fillMaxWidth()
            )
            val authCodeEntered by remember {
                derivedStateOf { authCode.isNotBlank() }
            }
            if (!authCodeEntered)
                return@Column

            var processMessage: SkillObtainingProgress? by remember { mutableStateOf(null) }
            var obtainSkillsButtonClicked by remember { mutableStateOf(false) }
            val successColor = TheorycrafterTheme.colors.base().successContent
            val failureColor = TheorycrafterTheme.colors.base().errorContent
            StepTitle(3)
            TheorycrafterTheme.RaisedButtonWithText(
                text = "Retrieve Skills",
                enabled = !obtainSkillsButtonClicked,
                onClick = {
                    obtainSkillsButtonClicked = true
                    coroutineScope.launch(Dispatchers.IO) {
                        val result = obtainTokensAndCharacterSkills(
                            authCode = authCode,
                            codeVerifier = codeVerifier,
                            onProgress = { processMessage = it }
                        )
                        if (result != null) {
                            val responseTokens = result.first
                            val characterName = responseTokens.characterName
                            val characterId = responseTokens.characterId
                            if ((updatedCharacterTokens != null) && (updatedCharacterTokens.characterId != characterId)) {
                                processMessage = SkillObtainingProgress.message(
                                    buildAnnotatedString {
                                        withStyle(SpanStyle(color = failureColor, fontWeight = FontWeight.Bold)) {
                                            appendLine("Authorized wrong character: $characterName")
                                        }
                                        append("Close this dialog and try again, making sure to authorize \"${updatedCharacterTokens.characterName}\"")
                                    }
                                )
                            } else {
                                processMessage = SkillObtainingProgress.message(
                                    buildAnnotatedString {
                                        withStyle(SpanStyle(color = successColor, fontWeight = FontWeight.Bold)) {
                                            appendLine("Successfully retrieved $characterName's skills!")
                                        }
                                        if (updatedCharacterTokens == null)
                                            append("Press 'Create Skill Set' below to create the new skill set.")
                                        else
                                            append("Press 'Update' below to update the skills of $characterName")
                                    }
                                )
                                tokensAndSkillInfo = result
                            }
                        }
                    }
                }
            )

            // A message telling the user what's going on while we're talking to the EVE servers
            processMessage?.let {
                StepTitle(4)
                Text(
                    text = it.message,
                    color = TheorycrafterTheme.colors.invalidContent(valid = !it.isError),
                    modifier = Modifier.padding(vertical = TheorycrafterTheme.spacing.xsmall)
                )
                if (it.longErrorMessage != null) {
                    TheorycrafterTheme.TextField(
                        value = it.longErrorMessage,
                        onValueChange = {},
                        readOnly = true,
                        modifier = Modifier.fillMaxSize()
                    )
                }
            }

        }
    }
}


/**
 * Encapsulates the progress of obtaining the skill sets.
 */
private class SkillObtainingProgress(
    val message: AnnotatedString,
    val isError: Boolean,
    val longErrorMessage: String?
) {

    companion object {

        fun message(message: String) =
            SkillObtainingProgress(AnnotatedString(message), isError = false, longErrorMessage = null)

        fun message(message: AnnotatedString) =
            SkillObtainingProgress(message, isError = false, longErrorMessage = null)

        fun error(message: String, details: String?) =
            SkillObtainingProgress(AnnotatedString(message), isError = true, longErrorMessage = details)

    }

}


/**
 * Obtains the EVE SSO tokens and uses them to obtain the character's skill information.
 */
private suspend fun obtainTokensAndCharacterSkills(
    authCode: String,
    codeVerifier: String,
    onProgress: (SkillObtainingProgress) -> Unit
): Pair<EveSsoTokens, GetCharactersCharacterIdSkillsOk>? {

    onProgress(SkillObtainingProgress.message("Obtaining ESI tokens"))
    val tokensOrError = obtainEveSsoTokens(authCode = authCode, codeVerifier = codeVerifier).result
    currentCoroutineContext().ensureActive()

    if (tokensOrError.isFailure) {
        onProgress(
            SkillObtainingProgress.error(
                message = "Error retrieving ESI tokens",
                details = tokensOrError.failure()
            )
        )
        return null
    }
    val tokens = tokensOrError.value()

    val skillsInfo = obtainCharacterSkills(
        eveSsoTokens = tokens,
        onTokensUpdated = { },  // We just received this token, it should not be updated
        onProgress = onProgress
    ) ?: return null

    return tokens to skillsInfo
}


/**
 * Obtains the character's skill levels using the given SSO tokens.
 */
private suspend fun obtainCharacterSkills(
    eveSsoTokens: EveSsoTokens,
    onTokensUpdated: suspend (EveSsoTokens?) -> Unit,
    onProgress: (SkillObtainingProgress) -> Unit
): GetCharactersCharacterIdSkillsOk? {
    val skillsResult = withFreshEveSsoTokens(
        tokens = eveSsoTokens,
        onTokensUpdated = onTokensUpdated,
    ) { tokens ->
        val characterName = tokens.characterName
        onProgress(SkillObtainingProgress.message("Retrieving ${characterName}s skills"))

        runCatching {
            SkillsApi(tokens).getCharacterSkills(tokens.characterId)
        }.toValueOrError()
    }
    currentCoroutineContext().ensureActive()

    if (skillsResult.isFailure) {
        onProgress(
            SkillObtainingProgress.error("Error retrieving skills", details = skillsResult.failure())
        )
        return null
    }

    return skillsResult.value()
}


/**
 * Updates the given skill set's levels from ESI.
 *
 * The result value is a pair:
 * - The first, Boolean, value indicates if there was an error refreshing the SSO tokens.
 * - The second, String, value is an error message.
 *
 * Updating was successful only if the flag is `false` and the error message is `null`.
 */
private suspend fun updateSkillLevels(skillSet: SkillSetHandle): Pair<Boolean, String?> {
    val ssoTokens = skillSet.ssoTokens
    var errorMessage: String? = null
    val (skillInfo, newTokens) = withContext(Dispatchers.IO) {
        var newTokens: EveSsoTokens? = ssoTokens
        val skillInfo = obtainCharacterSkills(
            eveSsoTokens = ssoTokens,
            onTokensUpdated = { newTokens = it },
            onProgress = {
                if (it.isError) {
                    errorMessage = it.longErrorMessage ?: it.message.toString()
                }
            }
        )
        skillInfo to newTokens
    }

    if (newTokens == null) {
        return Pair(true, errorMessage)
    }

    // Store the refreshed token
    if (newTokens !== ssoTokens) {
        TheorycrafterContext.skillSets.setEveSsoTokens(skillSet, newTokens)
    }

    // Set the skill levels on the skill set
    return if (skillInfo != null) {
        val eveData = TheorycrafterContext.eveData
        val skillTypesAndLevels = skillInfo.skills.mapNotNull {
            // The response could include new skills which we don't know of yet
            val skillType = eveData.skillTypeOrNull(it.skillId) ?: return@mapNotNull null
            skillType to it.activeSkillLevel
        }
        TheorycrafterContext.skillSets.setSkillLevels(skillSet, skillTypesAndLevels)
        Pair(false, null)
    }
    else {
        Pair(
            false,
            "Error retrieving skill levels for ${ssoTokens.characterName}" +
                "\nIf this persists, try removing and re-adding the skill set" +
                (errorMessage?.let { "\nError message: $it" } ?: "")
        )
    }
}


/**
 * Returns the skill types and their levels of the given skills response.
 */
private fun GetCharactersCharacterIdSkillsOk.skillTypesAndLevels(): Collection<Pair<SkillType, Int>> {
    return with(TheorycrafterContext.eveData) {
        skills.map { skillType(it.skillId) to it.activeSkillLevel }
    }
}


/**
 * The list of skill sets.
 */
@Composable
private fun SkillSetList(
    modifier: Modifier,
    builtInSkillSets: List<SkillSetHandle>,
    characterSkillSets: List<SkillSetHandle>,
    customSkillSets: List<SkillSetHandle>,
    selection: SkillSetSelectionModel,
    onSetAsDefault: (SkillSetHandle) -> Unit,
    onCreateCustomCopy: (SkillSetHandle) -> Unit,
    onUpdateSkills: (SkillSetHandle) -> Unit,
    skillSetBeingRenamed: MutableState<SkillSetHandle?>,
    onError: (String) -> Unit
) {
    val defaultSkillSet = TheorycrafterContext.skillSets.default

    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    val contextMenuState by remember { mutableStateOf(DropdownMenuState()) }
    val dialogs = LocalStandardDialogs.current

    fun SkillSetHandle.canBeRenamed() = !isBuiltInSkillSet && !isCharacterSkillSet
    fun onRename(handle: SkillSetHandle) {
        when {
            handle.isBuiltInSkillSet -> onError("Built-in skill sets can't be renamed")
            handle.isCharacterSkillSet -> onError("Character skill sets can't be renamed")
            else -> skillSetBeingRenamed.value = handle
        }
    }

    fun SkillSetHandle.canBeDeleted() = !isBuiltInSkillSet && (this != TheorycrafterContext.skillSets.default)
    fun onDelete(handle: SkillSetHandle) {
        when {
            handle.isBuiltInSkillSet -> onError("Built-in skill sets can't be deleted")
            handle == TheorycrafterContext.skillSets.default -> onError("The default skill set can't be deleted")
            else -> {
                val fitsUsingSkillSet = TheorycrafterContext.fits.handles.count {
                    it.requireStoredFit().skillSetId == handle.skillSetId
                }
                dialogs.showConfirmDialog(
                    text = "Delete skill set \"${handle.name}\"?" +
                            "\n\n$fitsUsingSkillSet fit(s) are currently using this skill set." +
                            "\nThey will be switched to the default one.",
                    confirmText = "Delete",
                    onConfirm = {
                        coroutineScope.launch {
                            TheorycrafterContext.skillSets.delete(handle)
                        }
                    },
                )
            }
        }
    }

    val listFocusRequester = remember { FocusRequester() }
    LazyColumnExt(
        modifier = modifier
            .focusWhenClicked(listFocusRequester)
            .moveSelectionWithKeys(selection)
            .onKeyShortcut(KeyShortcut.DeleteItem) {
                selection.selected?.let(::onDelete)
            }
            .onKeyShortcut(KeyShortcut.RenameItem) {
                selection.selected?.let(::onRename)
            }
            .onOpenContextMenu { position ->
                listState.itemIndexAt(position.y)?.let { clickedIndex ->
                    selection.selectIndex(clickedIndex)
                    contextMenuState.status = DropdownMenuState.Status.Open(position)
                }
            },
        state = listState,
        selection = selection
    ) {
        item {
            SkillSetListSectionHeader("Built-In Skill Sets", isFirst = true)
        }
        items(builtInSkillSets) { handle ->
            SkillSetListItem(handle, isDefault = (handle == defaultSkillSet))
        }

        item {
            SkillSetListSectionHeader("Character Skill Sets", isFirst = false)
        }
        items(characterSkillSets) { handle ->
            SkillSetListItem(handle, isDefault = (handle == defaultSkillSet))
        }

        item {
            SkillSetListSectionHeader("Custom Skill Sets", isFirst = false)
        }
        items(customSkillSets) { handle ->
            if (handle == skillSetBeingRenamed.value) {
                SkillSetNameEditor(
                    skillSetName = handle.name,
                    onRename = { newName ->
                         coroutineScope.launch {
                             TheorycrafterContext.skillSets.setName(handle, newName)
                         }
                    },
                    onEditingFinished = {
                        skillSetBeingRenamed.value = null
                        listFocusRequester.requestFocus()
                    }
                )
            }
            else {
                SkillSetListItem(handle, isDefault = (handle == defaultSkillSet))
            }
        }
    }

    // Context menu
    ContextMenu(contextMenuState) {

        @Composable
        fun menuItem(
            text: String,
            enabled: Boolean = true,
            keyShortcut: KeyShortcut? = null,
            action: (SkillSetHandle) -> Unit
        ) {
            MenuItem(
                text = text,
                enabled = enabled,
                displayedKeyShortcut = keyShortcut,
                reserveSpaceForKeyShortcut = true,
                onCloseMenu = contextMenuState::close,
                action = {
                    selection.selected?.let {
                        action.invoke(it)
                    }
                }
            )
        }

        val item = selection.selected ?: return@ContextMenu
        menuItem("Set as Default", action = onSetAsDefault)
        menuItem("Create Custom Copy", action = onCreateCustomCopy)
        menuItem("Rename", enabled = item.canBeRenamed(), KeyShortcut.RenameItem, ::onRename)
        menuItem("Update Skills", enabled = item.isCharacterSkillSet, action = onUpdateSkills)
        menuItem("Delete", enabled = item.canBeDeleted(), KeyShortcut.DeleteItem.first(), ::onDelete)
    }
}


/**
 * The selection model for the skill sets list.
 */
private class SkillSetSelectionModel(
    private val builtInSkillSets: List<SkillSetHandle>,
    characterSkillSets: List<SkillSetHandle>,
    customSkillSets: List<SkillSetHandle>
): SingleItemSelectionModel(initialSelectedIndex = null) {


    /**
     * The character skill sets.
     */
    var characterSkillSets: List<SkillSetHandle> = characterSkillSets
        set(value) {
            updateSection(current = field, updated = value, selected) {
                field = value
            }
        }


    /**
     * The custom skill sets.
     */
    var customSkillSets: List<SkillSetHandle> = customSkillSets
        set(value) {
            updateSection(current = field, updated = value, selected) {
                field = value
            }
        }


    /**
     * Updates the values in the section, adjusting the selection as necessary.
     */
    private fun updateSection(
        current: List<SkillSetHandle>,
        updated: List<SkillSetHandle>,
        selected: SkillSetHandle?,
        setField: () -> Unit
    ) {
        val updateToSkillSet = if (updated.size > current.size)
            updated.last()  // A skill set was added; select it
        else if ((updated.size < current.size) && (selected == current.last())) {
            // The last skill set in this section has been removed
            if (updated.isNotEmpty())
                updated.lastOrNull()
            else if (current == customSkillSets)
                characterSkillSets.lastOrNull() ?: builtInSkillSets.last()
            else
                customSkillSets.firstOrNull() ?: builtInSkillSets.last()
        }
        else
            null

        setField()

        if (updateToSkillSet != null)
            select(updateToSkillSet)
    }


    override val maxSelectableIndex: Int
        get() = builtInSkillSets.size + characterSkillSets.size + customSkillSets.size + 2  // + 3 - 1


    override fun isSelectable(index: Int): Boolean {
        // The section headers are not selectable
        return (index != 0)
                && (index != builtInSkillSets.size + 1)
                && (index != builtInSkillSets.size + 1 + characterSkillSets.size + 1)
    }


    /**
     * The currently selected skill set handle.
     */
    val selected: SkillSetHandle?
        get() = run {
            var index = selectedIndex
            if (index == null)
                return@run null
            index -= 1  // Skip "built-in" header
            if (index in 0..builtInSkillSets.lastIndex)
                return@run builtInSkillSets[index]
            index -= 1 + builtInSkillSets.size  // Skip "character" header and built-in skill sets
            if (index in 0 .. characterSkillSets.lastIndex)
                return@run characterSkillSets[index]
            index -= 1 + characterSkillSets.size  // Skip "custom" header and character skill sets
            if (index in 0 .. customSkillSets.lastIndex)
                return@run customSkillSets[index]

            null
        }


    /**
     * Selects the given skill set.
     */
    fun select(skillSet: SkillSetHandle) {
        val indexInBuiltIn = builtInSkillSets.indexOf(skillSet)
        if (indexInBuiltIn != -1) {
            selectIndex(indexInBuiltIn + 1)
            return
        }

        val indexInCharacter = characterSkillSets.indexOf(skillSet)
        if (indexInCharacter != -1) {
            selectIndex(indexInCharacter + builtInSkillSets.size + 2)
            return
        }

        val indexInCustom = customSkillSets.indexOf(skillSet)
        if (indexInCustom != -1) {
            selectIndex(indexInCustom + builtInSkillSets.size + characterSkillSets.size + 3)
            return
        }

        throw IllegalArgumentException("Unknown skill set handle: $skillSet")
    }


}


/**
 * The padding of the list items in the skill settings lists.
 */
private val ListItemPadding = PaddingValues(
    horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin,
    vertical = TheorycrafterTheme.spacing.small
)


/**
 * The skill set name being edited.
 */
@Composable
private fun SkillSetNameEditor(
    skillSetName: String,
    onRename: (String) -> Unit,
    onEditingFinished: () -> Unit,
) {
    ItemNameEditor(
        itemName = skillSetName,
        modifier = Modifier
            .fillMaxWidth()
            .padding(ListItemPadding)
            .bringIntoViewWhenFocusedWithMargins(ListItemPadding),
        onRename = onRename,
        onEditingFinished = onEditingFinished
    )
}


/**
 * The section header in the skill sets list.
 */
@Composable
private fun SkillSetListSectionHeader(text: String, isFirst: Boolean) {
    Text(
        text = text,
        style = TheorycrafterTheme.textStyles.caption,
        color = LocalContentColor.current.copy(alpha = 0.5f),
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin)
            .padding(
                top = if (isFirst) TheorycrafterTheme.spacing.verticalEdgeMargin else TheorycrafterTheme.spacing.larger,
                bottom = TheorycrafterTheme.spacing.xxxsmall
            ),
    )
}


/**
 * The item in the skill sets list.
 */
@Composable
private fun SkillSetListItem(
    handle: SkillSetHandle,
    isDefault: Boolean
) {
    VerticallyCenteredRow(
        modifier = Modifier
            .highlightOnHover()
            .fillMaxWidth()
            .padding(ListItemPadding),
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(
            text = handle.name,
            modifier = Modifier
                .weight(1f)
        )
        if (isDefault) {
            Icons.DefaultSkillSet(
                modifier = Modifier
                    .tooltip("Default skill set")
            )
        }
    }
}


/**
 * The list of skill groups.
 */
@Composable
private fun SkillGroupList(
    groups: List<TypeGroup>,
    selection: ListSelectionModel
) {
    LazyColumnExt(
        selection = selection,
        modifier = Modifier
            .focusWhenClicked()
            .moveSelectionWithKeys(selection)
            .fillMaxHeight()
            .width(TheorycrafterTheme.sizes.skillGroupsListWidth)
    ) {
        items(groups) {
            SkillGroupItem(it)
        }
    }
}


/**
 * The item in the skill groups list.
 */
@Composable
private fun SkillGroupItem(group: TypeGroup) {
    Box(
        modifier = Modifier
            .highlightOnHover()
            .fillMaxWidth()
            .padding(ListItemPadding)
    ) {
        Text(group.name)
    }
}


/**
 * List of skills.
 */
@Composable
private fun SkillList(
    modifier: Modifier,
    skillSet: SkillSetHandle,
    skills: List<SkillType>,
    selection: ListSelectionModel,
) {
    LazyColumnExt(
        selection = selection,
        modifier = modifier
            .focusWhenClicked()
            .moveSelectionWithKeys(selection)
            .fillMaxHeight()
            .widthIn(min = TheorycrafterTheme.sizes.skillsListMinWidth)
    ) {
        items(skills) {
            SkillItem(it, skillSet.levelOfSkill(it))
        }
    }
}


/**
 * The item in the skills list.
 */
@Composable
private fun SkillItem(skillType: SkillType, level: Int) {
    VerticallyCenteredRow(
        modifier = Modifier
            .highlightOnHover()
            .fillMaxWidth()
            .padding(ListItemPadding),
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(
            text = skillType.name,
            modifier = Modifier
                .weight(1f)
        )
        Text(
            text = level.toString(),
            modifier = Modifier
                .padding(
                    start = TheorycrafterTheme.spacing.medium,
                    end = TheorycrafterTheme.spacing.medium  // Get away from the scrollbar
                )
        )
    }
}


/**
 * The list of skill levels.
 */
@Composable
private fun SkillLevelList(
    modifier: Modifier,
    skillSet: SkillSetHandle,
    skill: SkillType,
    onSetSkillLevel: (SkillSetHandle, SkillType, level: Int) -> Unit
) {
    val currentLevel by remember(skillSet, skill) {
        derivedStateOf {
            skillSet.levelOfSkill(skill)
        }
    }
    LazyColumnExt(
        modifier = modifier
            .focusWhenClicked()
            .fillMaxHeight(),
    ) {
        items(6) { level ->
            SkillLevelItem(
                level = level,
                selected = level == currentLevel,
                onSelected = {
                    onSetSkillLevel(skillSet, skill, level)
                }
            )
        }
    }
}


/**
 * The item in the skill levels list.
 */
@Composable
private fun SkillLevelItem(
    level: Int,
    selected: Boolean,
    onSelected: () -> Unit
) {
    VerticallyCenteredRow(
        modifier = Modifier
            .toggleable(
                value = selected,
                onValueChange = {
                    if (!selected && it)
                        onSelected()
                },
            )
            .fillMaxWidth()
            .highlightOnHover()
            .padding(
                horizontal = TheorycrafterTheme.spacing.large,
                vertical = TheorycrafterTheme.spacing.small
            ),
        horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxsmall)
    ) {
        Checkmark(checked = selected)
        SingleLineText(text = "Level $level")
    }
}