package theorycrafter.ui.tournaments

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.DropdownMenuState
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import compose.input.*
import compose.utils.*
import compose.widgets.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.painterResource
import theorycrafter.TestTags
import theorycrafter.TheorycrafterContext
import theorycrafter.formats.loadCompositionFromXml
import theorycrafter.storage.StoredFit
import theorycrafter.storage.createDuplicateFitDetector
import theorycrafter.tournaments.Composition
import theorycrafter.tournaments.StoredComposition
import theorycrafter.tournaments.Tournament
import theorycrafter.ui.*
import theorycrafter.ui.fiteditor.FitEditorKeyShortcuts
import theorycrafter.ui.widgets.*
import theorycrafter.utils.*
import java.io.File


/**
 * The panel displaying the list of compositions.
 */
@Composable
fun CompositionsPanel(
    tournament: Tournament,
    onShowComposition: (Composition) -> Unit,
    focusCompositionEditor: () -> Unit,
) {
    var newlyCreatedComposition: Composition? by remember { mutableStateOf(null) }

    var searchText by remember { mutableStateOf("") }.also {
        if (newlyCreatedComposition != null)
            it.value = ""
    }
    val queryIsEmpty = searchText.isEmpty()

    val filteredComps by remember(tournament.compositions) {
        derivedStateOf {
            val query = searchText
            if (query.isEmpty())
                tournament.compositions
            else
                tournament.compositions.filter { it.name.contains(query, ignoreCase = true) }
        }
    }

    val lazyListState = rememberLazyListState()
    val listState = remember(tournament, filteredComps, lazyListState, queryIsEmpty) {
        CompositionsListState(tournament, filteredComps, lazyListState, queryIsEmpty)
    }

    val selectionModel = listState.selectionModel
    val coroutineScope = rememberCoroutineScope()
    val dialogs = LocalStandardDialogs.current
    val shortcutsManager = LocalKeyShortcutsManager.current

    // Select the newly created composition and start renaming it
    LaunchedEffect(listState, newlyCreatedComposition) {
        if (newlyCreatedComposition != null) {
            selectionModel.selectItem(newlyCreatedComposition)
            listState.compositionBeingRenamed = newlyCreatedComposition
        }
    }

    // When the filtered list changes, select the first item
    LaunchedEffect(filteredComps) {
        if (filteredComps.isEmpty())
            selectionModel.clearSelection()
        else if (newlyCreatedComposition == null)
            selectionModel.selectIndex(0)
    }

    // When a selected fit is removed from the end, select the last one instead
    LaunchedEffect(listState.compositions.size) {
        val selectedIndex = selectionModel.selectedIndex
        if ((selectedIndex != null) && (selectedIndex > listState.compositions.lastIndex))
            selectionModel.selectIndex(listState.compositions.lastIndex)
    }

    fun onCompositionCreated(composition: Composition) {
        newlyCreatedComposition = composition
        onShowComposition(composition)
    }

    fun onCreateNewComposition() {
        val newComp = tournament.addEmptyComposition("New ${tournament.compositionLabel(capitalized = true)}")
        onCompositionCreated(newComp)
    }
    shortcutsManager.register(CreateNewCompositionKeyShorcut, ::onCreateNewComposition)

    val clipboard = LocalClipboard.current
    val snackbarHost = LocalSnackbarHost.current
    fun onPasteCompositionFromClipboard() {
        coroutineScope.launch {
            val text = clipboard.getText()
            if (text == null) {
                snackbarHost.showSnackbar("No text in the clipboard")
                return@launch
            }
            val composition = kotlin.runCatching {
                with(TheorycrafterContext.eveData) {
                    compositionFromClipboardText(text, tournament)
                }
            }.getOrNull()
            if (composition == null) {
                snackbarHost.showSnackbar("No ${tournament.compositionLabel()} detected in the clipboard")
                return@launch
            }

            val newComp = tournament.addComposition(composition)
            onCompositionCreated(newComp)
        }
    }
    shortcutsManager.register(PasteCompositionFromClipboardKeyShortcut, ::onPasteCompositionFromClipboard)

    var showImportFileSelectionDialog by remember { mutableStateOf(false) }
    fun importCompositionFrom(file: File) {
        coroutineScope.launch {
            val duplicateFitDetectorDeferred = async { createDuplicateFitDetector(TheorycrafterContext.fits) }
            val result = withContext(Dispatchers.IO) {
                file.reader().use {
                    loadCompositionFromXml(eveData = TheorycrafterContext.eveData, reader = it)
                }
            }
            if (result.ships.isEmpty()) {
                dialogs.showErrorDialog("The specified file does not contain a valid composition")
                return@launch
            }
            val duplicateFitDetector = duplicateFitDetectorDeferred.await()
            var existingFits = 0
            var newFits = 0
            val dbFitIdByCompFitId = result.fitsByLocalId
                .mapValues { (_, storedFit) ->
                    val existingFit = duplicateFitDetector.findDuplicateOf(storedFit)
                    if (existingFit != null) {
                        existingFits += 1
                        existingFit.id
                    } else {
                        // Create new fit
                        newFits += 1
                        val tags = TheorycrafterContext.settings.newFitTags()
                        val fit = TheorycrafterContext.fits.add(
                            storedFit.copy(StoredFit.FitTimes.Unchanged, tags = storedFit.tags + tags)
                        )
                        fit.fitId
                    }
                }
            val ships = buildList {
                for (ship in result.ships) {
                    val fitId = dbFitIdByCompFitId[ship.localFitId]
                    add(
                        StoredComposition.StoredCompositionShip(
                            shipTypeId = ship.shipTypeId,
                            amount = ship.amount,
                            fitId = fitId,
                            active = ship.active
                        )
                    )
                }
            }
            val storedComposition = StoredComposition(
                name = result.name ?: file.name.substringBeforeLast("."),
                note = result.note ?: "",
                ships = ships
            )
            val newComp = tournament.addComposition(storedComposition)

            launch {
                snackbarHost.showSnackbar("Imported $newFits new fits and used $existingFits existing ones")
            }
            onCompositionCreated(newComp)
        }
    }

    fun onEditCompositionName(composition: Composition) {
        listState.compositionBeingRenamed = composition
    }

    fun onDuplicateComposition(composition: Composition) {
        coroutineScope.launch {
            val copyName = copyName(composition.name, allNames = tournament.compositions.asSequence().map { it.name })
            val newComp = tournament.duplicateComposition(composition, name = copyName)
            onCompositionCreated(newComp)
        }
    }

    fun onDeleteComposition(composition: Composition) {
        dialogs.showConfirmDialog(
            text = "Delete ${composition.name}?",
            confirmText = "Delete",
            onConfirm = {
                coroutineScope.launch {
                    tournament.deleteCompositions(listOf(composition))
                }
            }
        )
    }

    fun onMoveComposition(fromIndex: Int, toIndex: Int) {
        val isSelected = selectionModel.selectedIndex == fromIndex
        tournament.moveComposition(fromIndex, toIndex)
        if (isSelected)
            selectionModel.selectIndex(toIndex)
    }

    val compositionExporter = rememberCompositionExporter()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .moveSelectionWithKeys(
                selectionModel = selectionModel,
                itemsInPage = lazyListState::itemsInPage,
                onPreview = true
            )
            .onKeyShortcut(Key.F2, onPreview = true) {
                selectionModel.selectedItem()?.let {
                    onEditCompositionName(it)
                }
            }
            .onKeyShortcut(KeyShortcut.DeleteItem) {
                selectionModel.selectedItem()?.let {
                    onDeleteComposition(it)
                }
            }
            .onKeyShortcut(KeyShortcut.anyEnter(), onPreview = true) {
                // Ignore and don't consume ENTER when editing a composition's name.
                if (listState.compositionBeingRenamed != null) {
                    consumeEvent = false
                    return@onKeyShortcut
                }
                selectionModel.selectedItem()?.let {
                    onShowComposition(it)
                }
            }
            .onKeyShortcut(KeyShortcut.CopyToClipboard, onPreview = true) {
                selectionModel.selectedItem()?.let {
                    compositionExporter.copyToClipboard(it)
                }
            }
            .onKeyShortcut(MoveCompositionUpKeyShortcut,  onPreview = true) {
                if (!listState.isQueryEmpty)
                    return@onKeyShortcut
                selectionModel.selectedIndex?.let { index ->
                    if (index > 0)
                        onMoveComposition(index, index - 1)
                }
            }
            .onKeyShortcut(MoveCompositionDownKeyShortcut,  onPreview = true) {
                if (!listState.isQueryEmpty)
                    return@onKeyShortcut
                selectionModel.selectedIndex?.let { index ->
                    if (index < tournament.compositions.lastIndex)
                        onMoveComposition(index, index + 1)
                }
            }
    ) {
        CompositionsSearchField(
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = TheorycrafterTheme.spacing.small)
                .padding(horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin),
            tournament = tournament,
            searchText = searchText,
            onSearched = {
                searchText = it
            }
        )

        val compositionsListFocusRequester = remember { FocusRequester() }
        CompositionsList(
            state = listState,
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .focusRequester(compositionsListFocusRequester),
            onShowComposition = onShowComposition,
            onRenameComposition = { comp, name ->
                comp.name = name
            },
            onEditingNameFinished = { comp ->
                listState.compositionBeingRenamed = null
                if (comp == newlyCreatedComposition) {
                    newlyCreatedComposition = null
                    focusCompositionEditor()
                }
                else {
                    compositionsListFocusRequester.requestFocus()
                }
            },
            onCopyComposition = compositionExporter::copyToClipboard,
            onCopyCompositionWithOptions = compositionExporter::copyToClipboardWithOptions,
            onExportFitsToXml = compositionExporter::exportToXml,
            onEditCompositionName = ::onEditCompositionName,
            onDuplicateComposition = ::onDuplicateComposition,
            onDeleteComposition = ::onDeleteComposition,
            onMoveComposition = ::onMoveComposition,
        )

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(
                    horizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin,
                    vertical = TheorycrafterTheme.spacing.medium
                ),
            horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.small, Alignment.End)
        ) {
            val tooltipPlacement = EasyTooltipPlacement.ElementTopCenter(
                offset = DpOffsetY(y = -TheorycrafterTheme.spacing.xxsmall)
            )
            TheorycrafterTheme.RaisedButtonWithText(
                text = "New ${tournament.compositionLabel(capitalized = true)}",
                onClick = ::onCreateNewComposition,
                modifier = Modifier
                    .testTag(TestTags.CompsList.NewCompositionButton)
                    .nonFocusable()
                    .thenIf(listState.compositionBeingRenamed == null) {
                        // The tooltip can obscure the name editing text field
                        tooltip(
                            text = "Create new ${tournament.compositionLabel()}",
                            keyShortcut = CreateNewCompositionKeyShorcut,
                            placement = tooltipPlacement
                        )
                }
            )

            MenuButton(
                modifier = Modifier.nonFocusable(),
                offset = DpOffsetY(y = TheorycrafterTheme.spacing.xxxsmall),
                content = { onClick ->
                    TheorycrafterTheme.RaisedButtonWithText(
                        text = "Import",
                        onClick = onClick,
                        modifier = Modifier
                            .thenIf(listState.compositionBeingRenamed == null) {
                                tooltip(
                                    text = "Import ${tournament.compositionLabel()}",
                                    keyShortcut = null,
                                    placement = tooltipPlacement
                                )
                            }
                    )
                },
                menuContent = { onCloseMenu ->

                    @Composable
                    fun menuItem(
                        text: String,
                        icon: (@Composable () -> Unit)? = null,
                        keyShortcut: KeyShortcut? = null,
                        action: () -> Unit) {
                        MenuItem(
                            text = text,
                            icon = icon,
                            displayedKeyShortcut = keyShortcut,
                            reserveSpaceForKeyShortcut = true,
                            keyShortcutDescriptorWidth = TheorycrafterTheme.sizes.menuItemKeyShortcutWidthMedium,
                            onCloseMenu = onCloseMenu,
                            action = action
                        )
                    }

                    menuItem(
                        text = "Import from XML File…",
                        icon = { Icons.ImportFromFile() },
                        action = { showImportFileSelectionDialog = true }
                    )
                    menuItem(
                        text = "Paste from Clipboard",
                        icon = { Icons.Paste() },
                        keyShortcut = PasteCompositionFromClipboardKeyShortcut,
                        action = ::onPasteCompositionFromClipboard
                    )
                }
            )
        }
    }

    if (showImportFileSelectionDialog) {
        NativeReadFilePickerDialog(
            title = "Select XML composition file",
            filenameFilter = extensionFilenameFilter(ignoreCase = true, "xml"),
            onCompletedSelecting = { file ->
                if (file != null) {
                    importCompositionFrom(file)
                }
                showImportFileSelectionDialog = false
            }
        )
    }
}


/**
 * The compositions search field.
 */
@Composable
private fun CompositionsSearchField(
    modifier: Modifier,
    tournament: Tournament,
    searchText: String,
    onSearched: (String) -> Unit
) {
    SearchField(
        modifier = modifier,
        textFieldModifier = Modifier.requestInitialFocus(),
        searchText = searchText,
        placeholderText = "${tournament.compositionLabel(capitalized = true)} name",
        focusKeyShortcut = KeyShortcut(Key.F, KeyboardModifierMatcher.Command),
        onSearched = onSearched
    )
}


/**
 * The state object for the [CompositionsList].
 */
private class CompositionsListState(


    /**
     * The tournament.
     */
    val tournament: Tournament,


    /**
     * The compositions to display.
     */
    val compositions: List<Composition>,


    /**
     * The state of the actual lazy list displaying the compositions.
     */
    val lazyListState: LazyListState,


    /**
     * Whether the composition query is empty.
     */
    val isQueryEmpty: Boolean


) {


    /**
     * The selection model.
     */
    val selectionModel = SingleItemListSelectionModel(compositions)


    /**
     * The composition currently being renamed; `null` if none.
     */
    var compositionBeingRenamed: Composition? by mutableStateOf(null)


}


/**
 * The list of compositions itself.
 */
@Composable
private fun CompositionsList(
    state: CompositionsListState,
    modifier: Modifier = Modifier,
    onShowComposition: (Composition) -> Unit,
    onRenameComposition: (Composition, String) -> Unit,
    onEditingNameFinished: (Composition) -> Unit,
    onCopyComposition: (Composition) -> Unit,
    onCopyCompositionWithOptions: (Composition) -> Unit,
    onExportFitsToXml: (Composition) -> Unit,
    onEditCompositionName: (Composition) -> Unit,
    onDuplicateComposition: (Composition) -> Unit,
    onDeleteComposition: (Composition) -> Unit,
    onMoveComposition: (fromIndex: Int, toIndex: Int) -> Unit
) {
    val tournament = state.tournament
    val compositions = state.compositions
    val selectionModel = state.selectionModel

    if (compositions.isEmpty()) {
        val label = tournament.compositionLabel(plural = true)
        Text(
            text = if (state.isQueryEmpty)
                "No $label yet"
            else
                "No matching $label",
            textAlign = TextAlign.Center,
            modifier = modifier
                .fillMaxWidth()
                .padding(top = TheorycrafterTheme.spacing.xxlarge),
        )

        return
    }

    // Need to Box up so that ContextMenu has the same bounds as the list
    Box(modifier) {
        val contextMenuState by remember { mutableStateOf(DropdownMenuState()) }

        ScrollShadow(state.lazyListState, top = true, bottom = true)
        LazyColumnExt(
            state = state.lazyListState,
            selection = selectionModel,
            selectionBackground = TheorycrafterTheme.colors.selectedItemBackground(),
            modifier = Modifier
                .onOpenContextMenuGesture { position ->
                    state.lazyListState.itemIndexAt(position.y)?.let { clickedIndex ->
                        selectionModel.selectIndex(clickedIndex)
                        contextMenuState.status = DropdownMenuState.Status.Open(position)
                    }
                }
                .focusWhenClicked()
        ) {
            items(count = compositions.size) { index ->
                val composition = compositions[index]
                if (composition == state.compositionBeingRenamed) {
                    CompNameEditor(
                        composition = composition,
                        modifier = Modifier.fillMaxWidth(),
                        onRename = { name ->
                            onRenameComposition(composition, name)
                        },
                        onEditingFinished = {
                            onEditingNameFinished(composition)
                        }
                    )
                }
                else {
                    CompositionListItem(
                        composition = composition,
                        modifier = Modifier
                            .fillMaxWidth()
                            .onMousePress(clickCount = ClickCount.DOUBLE, consumeEvent = true) {
                                onShowComposition(composition)
                            }
                            .highlightOnHover()
                    )
                }
            }
        }

        ContextMenu(contextMenuState) {

            val index = selectionModel.selectedIndex ?: return@ContextMenu
            val composition = compositions[index]

            @Composable
            fun menuItem(
                text: String,
                enabled: Boolean = true,
                icon: (@Composable () -> Unit)? = EmptyIcon,
                keyShortcut: KeyShortcut? = null,
                action: () -> Unit
            ) {
                MenuItem(
                    text = text,
                    enabled = enabled,
                    icon = icon,
                    displayedKeyShortcut = keyShortcut,
                    reserveSpaceForKeyShortcut = true,
                    onCloseMenu = contextMenuState::close,
                    action = action
                )
            }

            menuItem("Copy", icon = { Icons.Copy() }, keyShortcut = KeyShortcut.CopyToClipboard) {
                onCopyComposition(composition)
            }
            menuItem("Copy with Options…", icon = { Icons.CopyWithOptions() }, keyShortcut = null) {
                onCopyCompositionWithOptions(composition)
            }
            menuItem("Export to XML File…", icon = { Icons.Share() }, keyShortcut = null) {
                onExportFitsToXml(composition)
            }

            MenuSeparator()

            menuItem(
                text = "Move up",
                enabled = state.isQueryEmpty && (index > 0),
                icon = { Icons.MoveUp() },
                keyShortcut = FitEditorKeyShortcuts.MoveSlotUpKey
            ) {
                onMoveComposition(index, index - 1)
            }

            menuItem(
                text = "Move down",
                enabled = state.isQueryEmpty && (index < compositions.lastIndex),
                icon = { Icons.MoveDown() },
                keyShortcut = FitEditorKeyShortcuts.MoveSlotDownKey
            ) {
                onMoveComposition(index, index + 1)
            }

            MenuSeparator()

            menuItem("Rename", icon = { Icons.Edit() }, keyShortcut =  KeyShortcut.RenameItem) {
                onEditCompositionName(composition)
            }
            menuItem("Duplicate", icon = { Icons.Duplicate() }, keyShortcut = null) {
                onDuplicateComposition(composition)
            }
            menuItem("Delete", icon = { Icons.Delete() }, keyShortcut = KeyShortcut.DeleteItem.first()) {
                onDeleteComposition(composition)
            }
        }
    }
}


/**
 * The layout for a composition item, both when displayed and edited.
 */
@Composable
private fun CompositionListItemLayout(
    composition: Composition,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    VerticallyCenteredRow(
        modifier = modifier
            .height(TheorycrafterTheme.sizes.compositionListItemHeight)
            .padding(horizontal = CompNameItemPaddingHorizontal),
    ) {
        CompositionIcon(composition)
        HSpacer(TheorycrafterTheme.spacing.small)
        content()
    }
}


/**
 * An item in the compositions list.
 */
@Composable
private fun CompositionListItem(
    composition: Composition,
    modifier: Modifier = Modifier
) {
    CompositionListItemLayout(composition, modifier) {
        Text(
            text = composition.name,
            maxLines = 2
        )
    }
}


/**
 * The icon for a composition.
 */
@Composable
private fun CompositionIcon(composition: Composition) {
    val iconShipTypes = remember(composition) {
        derivedStateOf {
            composition.tournament.rules.compositionRules.compositionIconShipTypes(composition)
        }
    }.value
    val tournamentIcon = composition.tournament.descriptor.icon

    // Not sure whether to show 1 or 2 icons. For now only show one.
    Box(Modifier.size(TheorycrafterTheme.sizes.eveTypeIconMedium)) {
        val shipType = iconShipTypes.firstOrNull()
        if (shipType != null) {
            Icons.EveItemType(
                itemType = shipType,
                modifier = Modifier.fillMaxSize()
            )
        } else if (tournamentIcon != null) {
            Image(
                painter = painterResource(tournamentIcon),
                contentDescription = null,
                modifier = Modifier
                    .fillMaxSize()
                    .clip(TheorycrafterTheme.eveTypeRenderingClip),
            )
        }
    }
}


/**
 * The horizontal padding of the composition name item.
 */
private val CompNameItemPaddingHorizontal = TheorycrafterTheme.spacing.horizontalEdgeMargin


/**
 * The composition name being edited.
 */
@Composable
private fun CompNameEditor(
    composition: Composition,
    modifier: Modifier,
    onRename: (String) -> Unit,
    onEditingFinished: () -> Unit,
) {
    CompositionListItemLayout(
        composition = composition,
        modifier = modifier
            .bringIntoViewWhenFocusedWithMargins(),
    ) {
        ItemNameEditor(
            itemName = composition.name,
            onRename = onRename,
            onEditingFinished = onEditingFinished
        )
    }
}


/**
 * The key shortcut to create a new composition.
 */
private val CreateNewCompositionKeyShorcut = MainWindowKeyShortcuts.CreateNewFit


/**
 * The key shortcut to paste a composition from the clipboard.
 */
private val PasteCompositionFromClipboardKeyShortcut = MainWindowKeyShortcuts.PasteFitFromClipboard


/**
 * The key shortcut to move a composition up by one index.
 */
private val MoveCompositionUpKeyShortcut = FitEditorKeyShortcuts.MoveSlotUpKey


/**
 * The key shortcut to move a composition down by one index.
 */
private val MoveCompositionDownKeyShortcut = FitEditorKeyShortcuts.MoveSlotDownKey
