/**
 * Keyboard event handling.
 */

package compose.input

import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.awtEventOrNull
import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.pointer.*
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.hostOs


/**
 * The required state of a keyboard modifier.
 */
enum class KeyboardModifierRequirement{


    /**
     * The key must be pressed.
     */
    PRESSED,


    /**
     * The key must not be pressed.
     */
    UNPRESSED,


    /**
     * The pressed state of the key is ignored.
     */
    IGNORED;


    /**
     * Whether the given pressed state matches this state.
     */
    fun matches(pressed: Boolean): Boolean = when (this){
        PRESSED -> pressed
        UNPRESSED -> !pressed
        IGNORED -> true
    }


}


/**
 * A matcher for which keyboard modifiers (ctrl, alt etc.) should be pressed.
 */
@Suppress("unused")
@Immutable
data class KeyboardModifierMatcher(


    /**
     * The required state of the CTRL modifier.
     */
    val ctrl: KeyboardModifierRequirement = KeyboardModifierRequirement.UNPRESSED,


    /**
     * The required state of the META modifier.
     */
    val meta: KeyboardModifierRequirement = KeyboardModifierRequirement.UNPRESSED,


    /**
     * The required state of the ALT modifier.
     */
    val alt: KeyboardModifierRequirement = KeyboardModifierRequirement.UNPRESSED,


    /**
     * The required state of the SHIFT modifier.
     */
    val shift: KeyboardModifierRequirement = KeyboardModifierRequirement.UNPRESSED


) {


    /**
     * Returns a new [KeyboardModifierMatcher] with the CTRL state set to "pressed".
     */
    fun ctrl() = KeyboardModifierMatcher(
        ctrl = KeyboardModifierRequirement.PRESSED,
        meta = this.meta,
        alt = this.alt,
        shift = this.shift
    )


    /**
     * Returns a new [KeyboardModifierMatcher] with the META state set to "pressed".
     */
    fun meta() = KeyboardModifierMatcher(
        ctrl = this.ctrl,
        meta = KeyboardModifierRequirement.PRESSED,
        alt = this.alt,
        shift = this.shift
    )


    /**
     * Returns a new [KeyboardModifierMatcher] with the ALT state set to "pressed".
     */
    fun alt() = KeyboardModifierMatcher(
        ctrl = this.ctrl,
        meta = this.meta,
        alt = KeyboardModifierRequirement.PRESSED,
        shift = this.shift
    )


    /**
     * Returns a new [KeyboardModifierMatcher] with the SHIFT state set to "pressed".
     */
    fun shift() = KeyboardModifierMatcher(
        ctrl = this.ctrl,
        meta = this.meta,
        alt = this.alt,
        shift = KeyboardModifierRequirement.PRESSED
    )


    /**
     * Returns a new [KeyboardModifierMatcher] with the "Command" key state set to "pressed".
     * The "Command" key is Meta on Mac OS X, Ctrl on other platforms.
     */
    fun cmd() = KeyboardModifierMatcher(
        ctrl = if (hostOs != OS.MacOS) KeyboardModifierRequirement.PRESSED else this.ctrl,
        meta = if (hostOs == OS.MacOS) KeyboardModifierRequirement.PRESSED else this.meta,
        alt = this.alt,
        shift = this.shift
    )


    /**
     * Returns whether the given [KeyEvent] has the keys required by this [KeyboardModifierMatcher] down.
     */
    internal fun matches(event: KeyEvent): Boolean = with(event){
        ctrl.matches(isCtrlPressed) &&
                meta.matches(isMetaPressed) &&
                alt.matches(isAltPressed) &&
                shift.matches(isShiftPressed)
    }


    /**
     * Returns whether the given [PointerEvent] has the keys required by this [KeyboardModifierMatcher] down.
     */
    internal fun matches(event: PointerEvent): Boolean{
        return with(event.keyboardModifiers){
            ctrl.matches(isCtrlPressed) &&
                    meta.matches(isMetaPressed) &&
                    alt.matches(isAltPressed) &&
                    shift.matches(isShiftPressed)
        }
    }


    companion object {


        /**
         * No keys are pressed.
         */
        val None = KeyboardModifierMatcher()


        /**
         * Only the ctrl key is pressed.
         */
        val Ctrl = KeyboardModifierMatcher(ctrl = KeyboardModifierRequirement.PRESSED)


        /**
         * Only the ctrl key is pressed.
         */
        fun ctrl() = Ctrl


        /**
         * Only the meta key is pressed.
         */
        val Meta = KeyboardModifierMatcher(meta = KeyboardModifierRequirement.PRESSED)


        /**
         * Only the meta key is pressed.
         */
        fun meta() = None.meta()


        /**
         * Only the alt key is pressed.
         */
        val Alt = KeyboardModifierMatcher(alt = KeyboardModifierRequirement.PRESSED)


        /**
         * Only the alt key is pressed.
         */
        fun alt() = None.alt()


        /**
         * Only the shift key is pressed.
         */
        val Shift = KeyboardModifierMatcher(shift = KeyboardModifierRequirement.PRESSED)


        /**
         * Only the shift key is pressed.
         */
        fun shift() = None.shift()


        /**
         * The pressed state of the shift key is ignored.
         */
        val IgnoreShift = KeyboardModifierMatcher(shift = KeyboardModifierRequirement.IGNORED)


        /**
         * The command modifier key: [Meta] on Mac OS X; [Ctrl] on all other systems.
         */
        val Command = KeyboardModifierMatcher().cmd()


        /**
         * Only the command modifier key is pressed.
         */
        fun cmd() = None.cmd()


    }


}


/**
 * Matches the key pressed, either by specifying the key itself, or the character it produces.
 */
@Immutable
private sealed interface ShortcutKeyMatcher{


    /**
     * Returns whether the given event matches.
     */
    fun matches(event: KeyEvent): Boolean


    /**
     * Matches the given physical key.
     */
    @JvmInline
    value class Key(val key: androidx.compose.ui.input.key.Key): ShortcutKeyMatcher {

        override fun matches(event: KeyEvent) = event.key == key

    }


    /**
     * Matches the given character.
     */
    @JvmInline
    value class Char(val char: kotlin.Char): ShortcutKeyMatcher {

        override fun matches(event: KeyEvent) = event.awtEventOrNull?.keyChar == char

    }


}


/**
 * Encapsulates a key shortcut: a key and a modifier.
 */
@Immutable
class KeyShortcut private constructor(


    /**
     * The key or char that activates the shortcut.
     */
    private val keyOrChar: ShortcutKeyMatcher,


    /**
     * The key modifiers that must be pressed for the shortcut to be activated.
     */
    private val keyModifier: KeyboardModifierMatcher


) {


    /**
     * Creates a shortcut that matches the given physical keyboard and modifiers.
     */
    constructor(
        key: Key,
        keyModifier: KeyboardModifierMatcher = KeyboardModifierMatcher.None
    ): this(ShortcutKeyMatcher.Key(key), keyModifier)


    /**
     * Creates a shortcut that matches the given character and keyboard modifiers.
     */
    constructor(
        char: Char,
        keyModifier: KeyboardModifierMatcher = KeyboardModifierMatcher.IgnoreShift
    ): this(ShortcutKeyMatcher.Char(char), keyModifier)


    /**
     * Returns whether the given key event matches this shortcut.
     */
    fun matches(keyEvent: KeyEvent): Boolean {
        return keyOrChar.matches(keyEvent) && keyModifier.matches(keyEvent)
    }


    /**
     * Returns string describing this shorcut that is suitable to be displayed in UI.
     */
    fun displayString(): String {
        val isMacOS = hostOs == OS.MacOS
        val parts = buildList {
            if (keyModifier.ctrl == KeyboardModifierRequirement.PRESSED)
                add(if (isMacOS) "⌃" else "Ctrl")
            if (keyModifier.alt == KeyboardModifierRequirement.PRESSED)
                add(if (isMacOS) "⌥" else "Alt")
            if (keyModifier.shift == KeyboardModifierRequirement.PRESSED)
                add(if (isMacOS) "⇧" else "Shift")
            if (keyModifier.meta == KeyboardModifierRequirement.PRESSED)
                add(if (isMacOS) "⌘" else "Win")
            add(
                when (keyOrChar) {
                    is ShortcutKeyMatcher.Key -> java.awt.event.KeyEvent.getKeyText(keyOrChar.key.nativeKeyCode)
                    is ShortcutKeyMatcher.Char -> keyOrChar.char.uppercase()
                }
            )
        }

        return parts.joinToString(separator = if (isMacOS) "" else "-")
    }


    override fun toString(): String {
        return displayString()
    }


    override fun equals(other: Any?): Boolean {
        if (this === other)
            return true
        if (other !is KeyShortcut)
            return false

        return (keyOrChar == other.keyOrChar) && (keyModifier == other.keyModifier)
    }


    override fun hashCode(): Int {
        var result = keyOrChar.hashCode()
        result = 31 * result + keyModifier.hashCode()
        return result
    }


    companion object {


        /**
         * Returns shortcuts matching any of the given keys.
         */
        fun anyOf(
            keys: Collection<Key>,
            keyModifier: KeyboardModifierMatcher = KeyboardModifierMatcher.None,
        ): Collection<KeyShortcut> = keys.map {
            KeyShortcut(it, keyModifier)
        }


        /**
         * The shortcuts matching the regular and numpad ENTER keys.
         */
        fun anyEnter(modifier: KeyboardModifierMatcher = KeyboardModifierMatcher.None) =
            anyOf(listOf(Key.Enter, Key.NumPadEnter), modifier)


        /**
         * The shortcut for the "cut to clipboard" action appropriate for the platform the app is running on.
         */
        val CutToClipboard = KeyShortcut(Key.X, KeyboardModifierMatcher.Command)


        /**
         * The shortcut for the "copy to clipboard" action appropriate for the platform the app is running on.
         */
        val CopyToClipboard = KeyShortcut(Key.C, KeyboardModifierMatcher.Command)


        /**
         * The shortcut for the "paste from clipboard" action appropriate for the platform the app is running on.
         */
        val PasteFromClipboard = KeyShortcut(Key.V, KeyboardModifierMatcher.Command)


        /**
         * The shortcut for closing the current window/tab/panel.
         */
        val CloseUi = KeyShortcut(Key.W, KeyboardModifierMatcher.Command)


        /**
         * The shortcut for closing all windows/tabs/panels.
         */
        val CloseAllUi = KeyShortcut(Key.W, KeyboardModifierMatcher().alt().cmd())


        /**
        * The shortcut for the ESCAPE key.
         */
        val Esc = KeyShortcut(Key.Escape)


        /**
         * The shortcut for undoing the last operation.
         */
        val Undo = KeyShortcut(Key.Z, KeyboardModifierMatcher.Command)


        /**
         * The shortcuts for redoing the last undone operation.
         */
        val Redo = listOf(
            KeyShortcut(
                key = Key.Z,
                keyModifier = KeyboardModifierMatcher.Command.shift()
            ),
            KeyShortcut(
                key = Key.Y,
                keyModifier = KeyboardModifierMatcher.Command
            )
        )


        /**
         * The shortcut for renaming items.
         */
        val RenameItem = KeyShortcut(Key.F2)


        /**
         * The shortcuts for deleting an item.
         */
        val DeleteItem = listOf(KeyShortcut(Key.Delete), KeyShortcut(Key.Backspace))


    }


}


/**
 * The context passed to a key shortcut action.
 */
interface KeyShortcutProcessingScope {


    /**
     * The key event that triggered the invocation of the action.
     */
    val keyEvent: KeyEvent


    /**
     * The shortcut that matched.
     */
    val matchedShortcut: KeyShortcut


    /**
     * Whether to consume the key event.
     */
    var consumeEvent: Boolean


}


/**
 * An implementation of [KeyShortcutProcessingScope]
 */
private class KeyShortcutProcessingScopeImpl(
    override val keyEvent: KeyEvent,
    override val matchedShortcut: KeyShortcut
): KeyShortcutProcessingScope {

    override var consumeEvent = true

}


/**
 * Returns a [Modifier] that runs the given action when one of the given shortcuts is pressed.
 */
fun Modifier.onKeyShortcut(


    /**
     * The shortcuts activating the [action].
     */
    shortcuts: Collection<KeyShortcut>,


    /**
     * Whether key detection is enabled.
     */
    enabled: Boolean = true,


    /**
     * Whether to detect the shortcut via [Modifier.onPreviewKeyEvent] or [Modifier.onKeyEvent].
     */
    onPreview: Boolean = false,


    /**
     * The function to run when the shortcut is pressed.
     */
    action: KeyShortcutProcessingScope.() -> Unit


): Modifier {
    if (!enabled)
        return this

    return (if (onPreview) this::onPreviewKeyEvent else this::onKeyEvent).invoke { keyEvent ->
        val matchedShortcut = shortcuts.firstOrNull { it.matches(keyEvent) }
        if (matchedShortcut == null)
            return@invoke false

        if (keyEvent.type == KeyEventType.KeyDown) {
            val scope = KeyShortcutProcessingScopeImpl(keyEvent, matchedShortcut)
            scope.action()
            return@invoke scope.consumeEvent
        }

        return@invoke false
    }
}


/**
 * Returns a [Modifier] tha runs the given action when the given key shortcut is used.
 */
fun Modifier.onKeyShortcut(


    /**
     * The shortcut.
     */
    shortcut: KeyShortcut,


    /**
     * Whether key detection is enabled.
     */
    enabled: Boolean = true,


    /**
     * Whether to detect the shortcut via [Modifier.onPreviewKeyEvent] or [Modifier.onKeyEvent].
     */
    onPreview: Boolean = false,


    /**
     * The function to run when the shortcut is pressed.
     */
    action: KeyShortcutProcessingScope.() -> Unit


): Modifier =
    this.onKeyShortcut(
        shortcuts = listOf(shortcut),
        enabled = enabled,
        onPreview = onPreview,
        action = action
    )


/**
 * Returns a [Modifier] tha runs the given action when the given key is pressed.
 */
fun Modifier.onKeyShortcut(


    /**
     * The key that activates the shortcut.
     */
    key: Key,


    /**
     * The key modifiers that must be pressed for the shortcut to be activated.
     */
    keyModifier: KeyboardModifierMatcher = KeyboardModifierMatcher.None,


    /**
     * Whether key detection is enabled.
     */
    enabled: Boolean = true,


    /**
     * Whether to detect the shortcut via [Modifier.onPreviewKeyEvent] or [Modifier.onKeyEvent].
     */
    onPreview: Boolean = false,


    /**
     * The function to run when the shortcut is pressed.
     */
    action: KeyShortcutProcessingScope.() -> Unit


): Modifier =
    this.onKeyShortcut(
        shortcut = KeyShortcut(key, keyModifier),
        enabled = enabled,
        onPreview = onPreview,
        action = action
    )


/**
 * Returns a [Modifier] tha runs the given action when the given character is typed.
 */
fun Modifier.onKeyShortcut(


    /**
     * The character that activates the shortcut.
     */
    char: Char,


    /**
     * The key modifiers that must be pressed for the shortcut to be activated.
     */
    keyModifier: KeyboardModifierMatcher = KeyboardModifierMatcher.IgnoreShift,


    /**
     * Whether key detection is enabled.
     */
    enabled: Boolean = true,


    /**
     * Whether to detect the shortcut via [Modifier.onPreviewKeyEvent] or [Modifier.onKeyEvent].
     */
    onPreview: Boolean = false,


    /**
     * The function to run when the shortcut is pressed.
     */
    action: KeyShortcutProcessingScope.() -> Unit


): Modifier =
    this.onKeyShortcut(
        shortcut = KeyShortcut(char, keyModifier),
        enabled = enabled,
        onPreview = onPreview,
        action = action
    )
