"""
Key press interaction
---------------------

This module provides a :class:`KeyPress` observable to
enable server side Python functions to subscribe to
key events.

In addtion there is also a middleware function, :meth:`navigate`,
that maps key actions to navigation actions, e.g.
`next_valid_time()` etc.

.. autoclass:: KeyPress
    :members:

.. autofunction:: navigate

.. autofunction:: press

"""
import bokeh.models
import forest.db.control
from forest.observe import Observable
from forest.export import export


__all__ = []


KEY_PRESS = "KEY_PRESS"


def press(code):
    """Key press action creator

    :param code: str representing browser event.code
    :returns: dict representing action
    """
    return {"kind": KEY_PRESS, "payload": {"code": code}}


@export
class KeyPress(Observable):
    """Key press server-side observable

    To add this to an existing `document`, add the
    hidden_button to the document and update `templates/index.html`
    to click the button on page load

    >>> key_press = KeyPress()
    >>> document.add_root(key_press.hidden_button)

    To observe the stream of actions generated by user key press
    events simply register a function via the `subscribe` method

    >>> key_press.subscribe(print)

    .. note:: KeyPress.hidden_button must be added to the
              document to allow JS hack to initialise callbacks
    """

    def __init__(self):
        self.source = bokeh.models.ColumnDataSource({"keys": []})
        self.source.on_change("data", self._on_change)
        custom_js = bokeh.models.CustomJS(
            args=dict(source=self.source),
            code="""
            if (typeof window.keyPressOn === 'undefined') {
                let interval = 150  // Key hold is about 30ms, double-click is about 200ms
                let throttle = function(callback, miliseconds) {
                    var waiting = false
                    return function() {
                        if (!waiting) {
                            callback.apply(null, arguments)
                            waiting = true
                            setTimeout(function() { waiting = false } , miliseconds)
                        }
                    }
                }

                let onkeydown = function(e) {
                    let keys = source.data['keys']
                    keys.push(e.code)
                    source.data = {
                        'keys': keys
                    }
                    source.change.emit()
                }
                document.onkeydown = throttle(onkeydown, interval)

                // Global to prevent multiple onkeydown callbacks
                window.keyPressOn = true
            }
        """,
        )
        self.hidden_button = bokeh.models.Button(
            css_classes=["keypress-hidden-btn"]
        )
        self.hidden_button.js_on_click(custom_js)
        super().__init__()

    def _on_change(self, attr, old, new):
        code = self.source.data["keys"][-1]
        self.notify(press(code))


def navigate(store, action):
    """Middleware to interpret key press events

    It implements the following mapping of
    actions to allow other parts of the
    system to interpret the action

    ========== =====================
    From       To
    ========== =====================
    ArrowRight Next valid time
    ArrowLeft  Previous valid time
    ArrowUp    Next initial time
    ArrowDown  Previous initial time
    ========== =====================

    .. note:: Non key press actions are passed on unaltered

    :param store: :class:`forest.redux.Store` instance
    :param action: incoming action
    """
    kind = action["kind"]
    if kind != KEY_PRESS:
        yield action
        return
    code = action["payload"]["code"].lower()
    if code == "arrowright":
        yield forest.db.control.next_valid_time()
    elif code == "arrowleft":
        yield forest.db.control.previous_valid_time()
    elif code == "arrowup":
        yield forest.db.control.next_initial_time()
    elif code == "arrowdown":
        yield forest.db.control.previous_initial_time()
