package compose.widgets

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import compose.input.KeyShortcut
import compose.input.onKeyShortcut
import compose.utils.Zero
import compose.utils.rememberDropdownMenuPositionProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.time.DurationUnit
import kotlin.time.measureTime


/**
 * A text field with an auto-suggest dropdown menu.
 */
@Composable
fun <T> AutoSuggestTextField(


    /**
     * The initially selected value.
     */
    initialValue: T? = null,


    /**
     * A function that takes the text currently typed into the text field and returns the list of suggested values.
     */
    autoSuggest: suspend (String) -> List<T>?,


    /**
     * A function that returns the text to put in the input text field when the given item is selected.
     * Note that this should return values compatible with the [autoSuggest] function. That is,
     * `autoSuggest(itemToString(item))` should always return a list that contains `item`.
     */
    itemToString: (T) -> String,


    /**
     * The function called when the value in the text field changes.
     */
    onValueChange: ((String) -> Unit)? = null,


    /**
     * Transforms the input text before it is passed to the auto-suggest function.
     */
    inputTextTransform: ((String) -> String)? = null,


    /**
     * Called before the given item is chosen from the list of suggestions.
     * Returns whether it can, in fact be chosen from the list of suggestions.
     */
    onBeforeSuggestionChosen: ((T) -> Boolean)? = null,


    /**
     * Called when an item has been chosen from the list of suggestions (after [onValueChange]).
     * The function is given the chosen item and the user action that triggered the choosing.
     */
    onSuggestionChosen: ((T, SuggestionChosenAction) -> Unit)? = null,


    /**
     * The key shortcuts that cause the currently selected value to be "chosen".
     */
    chooseSuggestedValueKeyShortcuts: Collection<KeyShortcut> = KeyShortcut.anyEnter(),


    /**
     * The modifier to apply to the [AutoSuggestTextField].
     */
    modifier: Modifier = Modifier,


    /**
     * Padding around (outside) the anchor that "inflates" its size.
     *
     * The dropdown menu will avoid overlapping this inflated anchor, and also align itself with its edges.
     * Note: this is [DpRect], not [PaddingValues], to allow negative values.
     */
    anchorPadding: DpRect = DpRect.Zero,


    /**
     * Decorates the contents of the dropdown menu.
     *
     * It takes the contents of the dropdown menu and possibly wraps them in more UI.
     */
    dropdownMenuDecorator: @Composable (
        suggestedValues: List<T>?,
        suggestionsContent: @Composable (Modifier) -> Unit
    ) -> Unit = @Composable { _, content -> content(Modifier) },


    /**
     * The background color to use for the selected item in the dropdown.
     */
    selectedItemBackground: Color = MaterialTheme.colors.secondary,


    /**
     * Provides the UI that displays the item in the suggestions list.
     */
    suggestedItemContent: @Composable RowScope.(T, String) -> Unit = { _, text -> defaultTextItemContent(text) },


    /**
     * Provides the suggestions menu items.
     */
    menuItemProvider: AutoSuggestMenuItemProvider = LocalAutoSuggestMenuItemProvider.current,


    /**
     * The modifier to apply to [menuItemProvider].
     */
    menuItemModifier: @Composable (T) -> Modifier = { Modifier.fillMaxWidth() },


    /**
     * Provides the text input field.
     */
    textFieldProvider: AutoSuggestTextFieldProvider


) {
    var textFieldValue by remember {
        mutableStateOf(
            TextFieldValue(if (initialValue != null) itemToString(initialValue) else "")
        )
    }
    var textFieldWidthPx by remember { mutableIntStateOf(0) }
    var showSuggestions by remember { mutableStateOf(true) }
    var hasFocus by remember { mutableStateOf(false) }
    val expanded by remember {
        derivedStateOf { showSuggestions && hasFocus }
    }

    val inputText = textFieldValue.text
    val autoSuggestInput = if (inputTextTransform != null) inputTextTransform(inputText) else inputText
    val suggestedValues by produceState<List<T>?>(
        initialValue = null,
        key1 = autoSuggest,
        key2 = autoSuggestInput,
    ) {
        withContext(Dispatchers.Default) {
            measureTime {
                value = autoSuggest.invoke(autoSuggestInput)
            }.let {
                println("Autosuggest for \"$autoSuggestInput\" took ${it.toString(DurationUnit.MILLISECONDS)}")
            }
        }
    }

    val suggestionsListState = rememberLazyListState()

    fun selectSuggestedValue(index: Int, action: SuggestionChosenAction) {
        val values = suggestedValues
        if ((values == null) || (index !in values.indices))
            return

        val value = values[index]
        val text = itemToString(value)

        if ((onBeforeSuggestionChosen != null) && !onBeforeSuggestionChosen(value))
            return

        textFieldValue = textFieldValue.copy(
            text = text,
            selection = TextRange(text.length)
        )
        showSuggestions = false
        onValueChange?.invoke(text)
        onSuggestionChosen?.invoke(value, action)
    }

    val suggestionsSelectionModel = remember(suggestedValues) {
        if (suggestedValues.isNullOrEmpty())
            RangeSingleItemSelectionModel.Empty
        else
            RangeSingleItemSelectionModel(
                initialSelectedIndex = 0,
                selectableRange = suggestedValues!!.indices
            )
    }

    Box(
        modifier = modifier
            .moveSelectionWithKeys(
                selectionModel = suggestionsSelectionModel,
                itemsInPage = { if (expanded) suggestionsListState.itemsInPage() else 1 },
                onPreview = true,
            )
            .onKeyShortcut(chooseSuggestedValueKeyShortcuts, onPreview = true) {
                val index = suggestionsSelectionModel.selectedIndex ?: return@onKeyShortcut
                selectSuggestedValue(index, SuggestionChosenAction.KeyPressed(matchedShortcut))
            }
    ) {
        textFieldProvider.provideTextField(
            value = textFieldValue,
            onValueChange = {
                if (textFieldValue.text != it.text)
                    showSuggestions = true
                textFieldValue = it
                onValueChange?.invoke(it.text)
            },
            modifier = Modifier
                .fillMaxWidth()
                .onGloballyPositioned {
                    textFieldWidthPx = it.size.width
                }
                .onFocusChanged {
                    hasFocus = it.hasFocus
                }
            )

        val popupPositionProvider = rememberDropdownMenuPositionProvider(anchorPadding)
        if (expanded) {
            val dropDownWidthDp = with(LocalDensity.current) {
                textFieldWidthPx.toDp() +
                        anchorPadding.left +
                        anchorPadding.right
            }
            Popup(
                popupPositionProvider = popupPositionProvider,
                onDismissRequest = { showSuggestions = false },
            ) {
                Surface(
                    modifier = Modifier.width(dropDownWidthDp),
                    elevation = AutoSuggestDropdownElevation
                ) {
                    dropdownMenuDecorator(suggestedValues) { dropdownModifier ->
                        val values = suggestedValues
                        if (values.isNullOrEmpty())
                            return@dropdownMenuDecorator

                        LazyColumnExt(
                            state = suggestionsListState,
                            selection = suggestionsSelectionModel,
                            selectionBackground = selectedItemBackground,
                            modifier = dropdownModifier
                        ) {
                            items(values.size) { index ->
                                val item = values[index]
                                menuItemProvider.provideDropdownMenuItem(
                                    onClick = {
                                        selectSuggestedValue(index, SuggestionChosenAction.Click)
                                    },
                                    modifier = menuItemModifier(item),
                                ) {
                                    val text = itemToString(item)
                                    suggestedItemContent(item, text)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}


/**
 * The elevation of the dropdown menu in [AutoSuggestTextField].
 */
val AutoSuggestDropdownElevation = 6.dp


/**
 * Specifies the user action that caused an item to be selected in [AutoSuggestTextField].
 */
sealed class SuggestionChosenAction {


    /**
     * The item was selected by clicking it.
     */
    data object Click: SuggestionChosenAction()


    /**
     * The item was selected by pressing the given keyboard shortcut
     */
    class KeyPressed(val shortcut: KeyShortcut): SuggestionChosenAction()


}


/**
 * The interface for the function that provides the text input field.
 */
fun interface AutoSuggestTextFieldProvider {


    @Composable
    fun provideTextField(


        /**
         * The text input field's value.
         */
        value: TextFieldValue,


        /**
         * The function to invoke when the value changes.
         */
        onValueChange: (TextFieldValue) -> Unit,


        /**
         * The modifier to apply to the input field.
         */
        modifier: Modifier


    )


}


/**
 * The interface for the function that provides the suggestions dropdown menu items.
 */
fun interface AutoSuggestMenuItemProvider {


    @Composable
    fun provideDropdownMenuItem(


        /**
         * The function to call when clicked.
         */
        onClick: () -> Unit,


        /**
         * The modifier to apply to it.
         */
        modifier: Modifier,


        /**
         * The content
         */
        content: @Composable RowScope.() -> Unit)


}


/**
 * The [CompositionLocal] for the provider of auto-suggest menu items.
 */
val LocalAutoSuggestMenuItemProvider = staticCompositionLocalOf { DefaultAutoSuggestMenuItemProvider }


/**
 * The default provider of a suggestion menu item.
 */
private val DefaultAutoSuggestMenuItemProvider = AutoSuggestMenuItemProvider { onClick, modifier, content ->
    DropdownMenuItem(
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}


/**
 * An implementation of the UI for the suggested item content that simply displays the text in a [Text].
 */
@Composable
fun defaultTextItemContent(text: String){
    Text(text)
}
